#ifndef PSX_GPU_GL_RENDERER_H #define PSX_GPU_GL_RENDERER_H /* GL backend context + present entry points, called from main.cpp's window * setup and present path when [video] renderer = "opengl". The backend's * rasterization vtable is obtained separately via gl_backend_get() * (gpu_render.h). SDL_Window is forward-declared (SDL typedefs it from this * same struct tag) so this header needs no SDL include. */ #include struct SDL_Window; #ifdef __cplusplus extern "C" { #endif /* Create the GL context on a window made with SDL_WINDOW_OPENGL. * Returns 1 on success, 0 to fall back to the SDL_Renderer present path. */ int gl_renderer_init_context(struct SDL_Window *win); /* Select a retained immutable bank for the next textured submission; zero * selects live VRAM. Emulation/GL owning thread only. Returns 0 if unavailable. */ int gl_renderer_select_texture_bank(uint16_t id); /* Retain texture indices while sampling current guest CLUTs (fades/animation). */ int gl_renderer_select_texture_bank_live_clut(uint16_t id); int gl_renderer_texture_banks_supported(void); /* Set the GL swap interval / vsync mode (1=vsync, 0=immediate, -1=adaptive). * Safe before or after context creation; applies live when a context exists. */ void gl_renderer_set_swap_interval(int interval); /* Presentation-only temporal blending. High-refresh sub-presents blend the two * most recent stable display images on the owning render thread/context; this * does not generate motion vectors or true intermediate object positions. */ void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz, double source_hz, int blend_mode); void gl_renderer_set_interpolation_suspended(int suspended); /* Change only the blend mode (0 linear, 1 change-adaptive, 2 hold), keeping * the history and schedule; takes effect at the next present. */ void gl_renderer_set_interpolation_blend(int blend_mode); /* Blend source: 0 = every guest VBlank is a source frame (default), * 1 = only real display flips are (psx_mod_set_frame_interpolation_source). */ void gl_renderer_set_interpolation_source(int source); void gl_renderer_interpolation_source_diag(int *source, uint32_t *flip_period, uint64_t *captures, uint64_t *duplicates); /* Render passes (render_pass.c; docs/RENDER_PASSES.md). The presenter is * ready for passes: GL raster, interpolation live with the FLIP source. */ int gl_renderer_pass_ready(void); /* Why not: 0 ready, PSX_MOD_RENDER_PASS_NO_PRESENTER or _BACKEND * (mod_plugins.h). A backend mode that cannot host passes reports BACKEND. */ uint32_t gl_renderer_pass_unavailable(void); /* Debug: make the backend decline passes (PSX_RENDER_PASS_REFUSE=1 at start, * render_pass_refuse over TCP), as a renderer mode without them would. */ void gl_renderer_pass_force_refuse(int on); /* Phases the presenter will show for the frame the next flip displays. */ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks, uint32_t shown_after_vblanks, uint32_t *alpha_q16, uint32_t max, uint32_t *wanted); /* Open the VRAM transaction for rect; open_gen first captures the game's own * image of it as a new generation. reuse_backup: the caller guarantees that * no guest code ran since the previous pass ended (its restore left VRAM as * backed up), so the backup of that pass is reused when it covers the same * rect at the same scale. Returns 0 when refused (nothing changed). */ int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen, uint32_t period_vblanks, int reuse_backup); /* Generations opened from now on are for a frame already on screen (1) or * for the next flip (0, default); render_pass_gen_flip_matches. */ void gl_renderer_pass_set_flip_shown(int shown); /* Capture the drawn rect at alpha_q16 (keep) and roll the rect back. */ void gl_renderer_pass_end(uint32_t alpha_q16, int keep); uint32_t gl_renderer_pass_leaks(void); int gl_renderer_pass_verify_vram(void); void gl_renderer_pass_note_cost(uint64_t ticks); /* Present output deadlines that fell due while guest code ran. */ void gl_renderer_pass_service_presents(void); /* promotions, presents, blended presents, expired, unmatched flips, presents * made between VBlanks, smoothed pass cost (us), images in the shown frame, * presents past the frame's planned end (the next flip was late; the newest * image held), stale pass-cost estimates measured again. */ void gl_renderer_pass_diag(uint64_t out[10]); /* Out-of-rect VRAM writes journaled and rolled back (lifetime count). */ uint64_t gl_renderer_pass_journaled(void); /* Passes that reused the previous pass's VRAM backup (lifetime count). */ uint64_t gl_renderer_pass_backups_reused(void); /* Pass image textures allocated now (both generations) and their bytes. */ uint32_t gl_renderer_pass_image_textures(uint64_t *bytes); /* Debug: dump the images of the next `generations` shown frames as PNGs. */ void gl_renderer_pass_dump_arm(const char *dir, int generations); uint64_t gl_renderer_perf_ticks(void); uint64_t gl_renderer_perf_frequency(void); int gl_renderer_interpolation_owns_cadence(void); void gl_renderer_interpolation_diag(int *enabled, int *suspended, int *history_frames, double *host_hz, double *target_hz, uint64_t *swaps); /* Cumulative CPU-upload diagnostics: calls, rects, pixels, conversion ticks, * texture-upload ticks, FBO-draw ticks. Active only with PSX_RUNTIME_PERF_DIAG. */ void gl_renderer_runtime_diag(uint64_t out[6]); /* Present an ARGB8888 image (BGRA byte order) as a letterboxed quad + swap. * Used for 24-bit (FMV) frames and the PSX_GL_FORCE_CPU_PRESENT diagnostic. * force_4_3 = pillarbox at native 4:3 even on a wide display aspect (FMVs * are authored 4:3 and get no GTE squash to compensate the stretch). * content_w: if 0 < content_w < src_w, only columns [0, content_w) are shown * (left-aligned in the letterbox; the rest stays cleared black). Used to hide * a trailing depth24 margin without changing CRTC width / stretching. */ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linear, int force_4_3, int content_w); /* Bezel art shown in the letterbox/pillarbox margins. Takes RGBA8 pixels; the * caller owns them and may free them on return. Passing NULL clears it. * Returns 0 only if a texture could not be created. */ int gl_renderer_set_bezel(const void *rgba, int w, int h); int gl_renderer_has_bezel(void); /* Clear to black + swap (display-disabled frame). */ void gl_renderer_present_blank(void); /* §33: re-present the last Live frame captured before Swap (or from a VRAM * snapshot when interpolation owned the last present). Used during rollback * resim so the window keeps a wall-clock present cadence without reading * mid-resim VRAM. Returns 1 if a Swap happened, 0 if no hold is available. */ int gl_renderer_present_hold_last(void); /* Sync the authoritative FBO down to CPU VRAM if the GPU side is ahead (else * a no-op). Screenshots and the debug server call this before reading CPU * VRAM. Do NOT use before 24-bit (FMV) scanout — a full readback can clobber * packed RGB888 MDEC bytes in the CPU mirror (use flush_cpu_uploads instead). */ void gl_renderer_sync_cpu(void); /* Land pending CPU→VRAM uploads into the FBO without reading the FBO back. * Safe before 24-bit (FMV) CPU scanout of the mirror. */ void gl_renderer_flush_cpu_uploads(void); /* Mark the whole display dirty, drop present-path latches, reset frame- * interpolation history, and force the next several SwapWindow calls even if * VRAM tiles match the last present. Call after savestate restore so a * reloaded identical frame still reaches the window (double/triple buffer). */ void gl_renderer_invalidate_present(void); /* After savestate restore: push the CPU VRAM mirror into the GL FBO. Needed * when the load happened while GP1 depth24 was on — the normal path skips * framebuffer-sized uploads, which also skipped the full-VRAM boot_state * blit and left post-FMV menus without texture pages. */ void gl_renderer_restage_vram_after_savestate(void); /* Netplay dual-raster: every GP0 also writes software VRAM @ 1× (snaps / * digests / GPUREAD authority) while the OpenGL hr FBO keeps settings-scale * SSAA for present-only. Never enables glReadPixels; CPU stays current. */ void gl_renderer_set_cpu_auth_dual(int on); /* FMV present reconstruction, settings.toml [video] fmv_filter. Takes the * config enum VIDEO_FMV_FILTER_* (0 nearest, 1 bilinear, 2 sharp, 3 bicubic). * Only consulted while video antialiasing is on; AA off is always nearest. */ void gl_renderer_set_fmv_filter(int cfg_value); int gl_renderer_cpu_auth_dual(void); /* Post-savestate freeze probe: skip/swap/dirty-mark counters (GL present path). * take() returns deltas since the previous take/reset. Safe no-ops when GL is * inactive. rect_dirty tests the current present-tile dirty bits. */ void gl_renderer_present_probe_reset(void); void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta, uint64_t *dirty_mark_delta, int *force_remaining); int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h); /* THE present path for 15-bit frames: blit the display region straight from * the authoritative VRAM FBO into a letterboxed rect (no readback). * Deterministic — used for every 15-bit frame. linear = filter on scale. * force_4_3 pins to native 4:3 (15-bit MDEC FMV frames on a wide aspect). */ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, int force_4_3); /* GPU-direct native-wide present: blit the displayed buffer's wide FBO (key = * disp_x) straight to the window, no CPU readback. Returns 0 if no wide surface * exists for disp_x (caller falls back to the CPU readout path). */ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear); /* Display aspect for the present letterbox (default 4:3). A wide aspect * stretches the 4:3 frame; pair with gte_set_display_aspect (cpu_state.h) * for the widescreen field-of-view hack. */ void gl_renderer_set_display_aspect(int num, int den); /* Scanline post-process (host display setting). on toggles the effect; strength * (0..1) is the depth of the dark gap between PS1 scanlines. Applied at the * native display-line pitch in the present/interpolation shaders, and faded in * with output scale so it never shimmers on a sub-2x window. gl_renderer_get_ * scanlines returns the on flag and (via out-param) the current strength. */ void gl_renderer_set_scanlines(int on, float strength); int gl_renderer_get_scanlines(float *strength); /* Presentation-only gamma adjustment. gamma = 1.0 is the identity; values * above 1.0 lift shadow detail and values below 1.0 darken it. The adjustment * is applied once to game content in the final GL presentation pass, including * temporal interpolation, but not to the bezel, host OSD, black margins, or an * already-composed hold-last image. Non-finite and out-of-range values are * clamped to a safe range. Safe to call before GL context creation. */ void gl_renderer_set_post_gamma(float gamma); float gl_renderer_get_post_gamma(void); /* Select full native-wide mirror rendering instead of the centre-splice fast * path. Textured edge expansion needs the complete mirror surface. */ void gl_renderer_set_wide_fast(int on); /* Internal-resolution scale state of the live GL context (0 before init). */ typedef struct GlScaleInfo { int requested; /* scale requested before context init (gr_set_scale) */ int effective; /* scale the hr surface was allocated at */ int max_scale; /* largest scale this context could allocate */ int max_dim; /* min(GL_MAX_TEXTURE_SIZE, _RENDERBUFFER_SIZE, _VIEWPORT_DIMS) */ int clamp_reason; /* PSX_GL_SCALE_* mask (gl_scale_limits.h) */ int alloc_retries; /* FBO allocations that failed and stepped the scale down */ int budget_mib; /* memory budget for the hr surface (0 = none) */ int fbo_w, fbo_h; /* hr surface size in pixels */ int drawable_w, drawable_h; /* window drawable in pixels */ /* Windowed high-resolution mode (true 8K on 16384-limit GPUs): the hr * surface stays at hr_scale (1) and the displayed columns are kept at * `effective` scale in window_tiles surfaces (more than one when * side-by-side buffers are too wide for one). window_x/_w/_fbo_* describe * the tile holding the last presented display. */ int hr_scale; int windowed; int window_x, window_w; int window_fbo_w, window_fbo_h; int window_grows; /* tile allocations so far */ int window_tiles; int window_mib; /* all tiles */ } GlScaleInfo; int gl_renderer_scale_info(GlScaleInfo *out); /* Narrow a native-wide display aspect num:den to the widest whose surface * this context can allocate at its internal scale (psx_gl_fit_wide_aspect; * disp_w is the display width the margins derive from). Returns 1 when it * changed num:den; 0, leaving it alone, before init or with no known limit. */ int gl_renderer_fit_wide_aspect(int disp_w, int *num, int *den); /* Read the display rect at internal resolution from the hr FBO: (w*S) x (h*S) * ARGB8888, top row first. Returns the pixel count, 0 if unavailable or larger * than cap_px. Debug/verification only (screenshot_hires under GL). */ int gl_renderer_read_display_hires(int x, int y, int w, int h, uint32_t *out, int cap_px, int *ow, int *oh); void gl_renderer_shutdown(void); /* Diagnostics (debug server): read GPU-side VRAM without touching the CPU * array; report coherency flags + dirty rects. fbo_peek returns 0 when the * GL pipeline is inactive. */ int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out); void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]); /* Always-on coherency event ring (debug server "gl_coh_ring"): every upload * flush, fill, copy, draw bbox, pack, full readback, present, and probe * perturbation, with rect + frame. An op that flushes internally records its * own event AFTER the FLUSH it caused (the event after a FLUSH = trigger). */ enum { GL_COH_FLUSH = 1, /* CPU->FBO upload flush (pending box) */ GL_COH_FILL = 2, /* GP0(02) fill rect */ GL_COH_COPY_SRC = 3, /* GP0(80) copy, source rect */ GL_COH_COPY = 4, /* GP0(80) copy, dest rect */ GL_COH_DRAW = 5, /* drawn prim bbox (clipped to draw area) */ GL_COH_PACK = 6, /* hr FBO -> raw mirror pack (dirty box) */ GL_COH_ENSURE = 7, /* full FBO -> CPU VRAM readback */ GL_COH_PRESENT = 8, /* 15-bit present blit (display rect) */ GL_COH_UPLOAD = 9, /* bulk CPU->VRAM transfer_in dest rect */ GL_COH_PEEK = 10, /* gl_fbo_peek probe (perturbs: flushes) */ GL_COH_DIFF = 11, /* gl_vram_diff probe (perturbs: flushes) */ }; typedef struct { uint32_t frame; uint8_t kind; int16_t x0, y0, x1, y1; /* native VRAM coords, inclusive */ } GlCohEvent; uint64_t gl_renderer_coh_total(void); /* Fetch event by absolute sequence number; 0 if evicted or out of range. */ int gl_renderer_coh_get(uint64_t seq, GlCohEvent *out); /* Always-on present ring (debug server "gl_present_ring"): EVERY SwapWindow — * including blank (display-disabled) and CPU-quad presents, which the coherency * ring does not record — with the path taken, source display rect, letterbox * dest rect, a glGetError sample, wall-clock ms, and a backbuffer pixel sampled * at the letterbox centre right before the swap (the ground truth for "did this * swap present black"). */ enum { GL_PRES_VRAM = 0, /* 15-bit FBO blit present (gl_renderer_present_vram) */ GL_PRES_WIDE = 1, /* native-wide FBO blit present */ GL_PRES_CPU = 2, /* CPU-readout quad present (24-bit FMV / forced) */ GL_PRES_BLANK = 3, /* display-disabled black present */ GL_PRES_INTERP = 4, /* host-refresh interpolation sub-present */ }; typedef struct { uint32_t frame; /* s_frame_count at swap */ uint32_t t_ms; /* SDL_GetTicks() at swap */ uint8_t path; /* GL_PRES_* */ uint8_t px_r, px_g, px_b; /* backbuffer sample at letterbox centre */ uint16_t glerr; /* glGetError() drained just before the swap */ int16_t dx, dy, w, h; /* source display rect (native px; 0 for blank) */ int16_t lx, ly, lw, lh; /* letterbox dest rect (window px) */ uint8_t src_r, src_g, src_b, src_valid; /* blit SOURCE (hr FBO) sample * at the display-rect centre */ } GlPresEvent; uint64_t gl_renderer_pres_total(void); int gl_renderer_pres_get(uint64_t seq, GlPresEvent *out); /* frame_perf: aggregate the per-frame GPU/CPU phase-timing ring (debug server * "frame_perf"). wide_filter: -1 = all frames, 0 = 4:3 present, 1 = native-wide. * Fills out[13]: [0]=count, [1]=total_ms avg, [2]=total_ms max, [3]=emu_cpu_ms avg * (frame minus the present call), [4]=present_wall_ms avg, [5]=scene_gpu_ms avg, * [6]=scene_gpu_ms max, [7]=present_gpu_ms avg, [8]=present_gpu_ms max, * [9]=scene primitives/frame avg (pre double-draw), [10]=mirror_gpu_ms avg (of * scene_gpu, the native-wide mirror passes; GL_TIMESTAMP pairs), [11]=mirror_gpu_ms * max, [12]=mirror passes/frame avg, [13]=CPU wall in flush_tex_batch avg, * [14]=CPU wall in glb_wide_* avg, [15]=batches/frame avg, [16]=wide target * sets/frame avg, [17]=wide FBO creations/frame avg. GPU phases are true * GL_TIME_ELAPSED times (CPU-overhead independent). Returns the count. */ int gl_renderer_perf_aggregate(int wide_filter, double out[18]); /* Native-wide mirror ablation (perf attribution, debug cmd gl_ws_ablate): * 0 = normal, 1 = skip the whole mirror pass (incl. wide_clear), 2 = full mirror * state churn without the draw calls, 3 = mirror draws stay on the hr FBO (no * per-pass FBO rebind; diagnostic only — corrupts both surfaces' content). */ void gl_renderer_set_ws_ablate(int mode); int gl_renderer_get_ws_ablate(void); /* Cumulative textured fraction of scene primitives since boot (flat vs textured * batching decision). Sets *out_tex_frac; returns total prim count. */ uint64_t gl_renderer_perf_prim_split(double *out_tex_frac); /* Cumulative textured-batch diagnostics: total, then flushes caused by * isolation, blend-mode, mask, filter, backdrop-gate, texture-window, capacity. */ void gl_renderer_batch_diag(uint64_t out[8]); /* Texture-window batching ([video] texture_window_batching; default off). * On: textured prims with different GP0(E2h) texture windows share a batch * (each vertex carries its window), except while mask checking is on. The * image is the same either way; only the number of draws changes. Safe to * call before GL context creation. */ void gl_renderer_set_texture_window_batching(int on); int gl_renderer_get_texture_window_batching(void); #ifdef __cplusplus } #endif #endif /* PSX_GPU_GL_RENDERER_H */