Files
libretroadmin 1d95b38136 C89: the PowerVR's registers, memory and timing, Holly's interrupts, and the DMA controller
holly_intc, pvr_regs, pvr_sb_regs, pvr_mem, spg and the SH-4's DMAC
become C. Their headers - and the ones they need, _vmem.h, sh4_mem.h,
Renderer_if.h, ta.h, maple_if.h - are made to compile as both, with
the C++-only parts (templates, the Renderer class, the maple devices)
behind __cplusplus. The video memory areas' templates are functions
per size and half; SetREP takes the frame's size from its caller.

A C file's variable is declared once, in its header: serialize.cpp,
ta_ctx.cpp and the others no longer declare their own, which gave
them C++ linkage that MSVC does not link to C.
tools/c89/linkage_check.py finds such declarations after a build and
runs in the Headless workflow. The tests that wrap a function by name
take the C names.
2026-11-12 08:44:36 +01:00

585 lines
18 KiB
C++

#include "Renderer_if.h"
#include "rend/last_picture.h"
#include "ta.h"
#include "hw/pvr/pvr_mem.h"
#include "rend/TexCache.h"
#include "rend/sorter.h"
#include "rend/rtt_watch.h"
#include "hw/mem/_vmem.h"
#include "cheats.h"
#include "spg.h"
// (hw/sh4/sh4_core.h, which has a name for every register of the SH4)
#include "hw/sh4/sh4_if.h" /* (RestoreHostRoundingMode, HostFloatModeBack) */
/*
rendv3 ideas
- multiple backends
- ESish
- OpenGL ES2.0
- OpenGL ES3.0
- OpenGL 3.1
- OpenGL 4.x
- Direct3D 10+ ?
- correct memory ordering model
- resource pools
- threaded TA
- threaded rendering
- RTTs
- framebuffers
- overlays
PHASES
- TA submission (memops, dma)
- TA parsing (defered, rend thread)
- CORE render (in-order, defered, rend thread)
submission is done in-order
- Partial handling of TA values
- Gotchas with TA contexts
parsing is done on demand and out-of-order, and might be skipped
- output is only consumed by renderer
render is queued on RENDER_START, and won't stall the emulation or might be skipped
- VRAM integrity is an issue with out-of-order or delayed rendering.
- selective vram snapshots require TA parsing to complete in order with REND_START / REND_END
Complications
- For some apis (gles2, maybe gl31) texture allocation needs to happen on the gpu thread
- multiple versions of different time snapshots of the same texture are required
- TA parsing vs frameskip logic
Texture versioning and staging
A memory copy of the texture can be used to temporary store the texture before upload to vram
This can be moved to another thread
If the api supports async resource creation, we don't need the extra copy
Texcache lookups need to be versioned
rendv2x hacks
- Only a single pending render. Any renders while still pending are dropped (before parsing)
- wait and block for parse/texcache. Render is async
*/
extern int screen_width;
extern int screen_height;
u32 VertexCount=0;
u32 FrameCount=1;
Renderer* renderer;
static Renderer* fallback_renderer;
bool renderer_changed = false; // Signals the renderer interface to switch renderer
/* No renderer could be set up on the frontend's context (its device was
* refused, say): the frames run and none is drawn, until the context is
* made again (renderer_changed). Not tried again each frame. */
static bool renderer_failed = false;
#if !defined(TARGET_NO_THREADS)
#include "libretro/emu_baton.h"
extern EmuBaton emu_baton;
#endif
u32 fb_w_cur = 1;
bool rend_compute_wanted = false; // rend/rend_method.h
int max_idx,max_mvo,max_op,max_pt,max_tr,max_vtx,max_modt, ovrn;
bool pend_rend = false;
static bool render_called = false; /* a render started since the last vblank (rend_vblank) */
static bool frame_produced = false; /* a picture to show made in this run (for is_dupe) */
static bool skip_screen = false; /* rend_skip_screen */
/* a renderer was made again in place of another (rend_present_kept_picture()) */
static bool kept_picture_due = false;
u32 fb_watch_addr_start;
u32 fb_watch_addr_end;
bool fb_dirty;
TA_context* _pvrrc;
void rend_create_renderer()
{
switch (settings.pvr.rend)
{
default:
case 0:
NOTICE_LOG(PVR, "Creating Open GL per-triangle/strip renderer");
renderer = rend_GLES2();
break;
#if defined(HAVE_GL4)
case 3:
NOTICE_LOG(PVR, "Creating Open GL per-pixel renderer");
renderer = rend_GL4();
fallback_renderer = rend_GLES2();
break;
#endif
#ifdef HAVE_VULKAN
case 4:
NOTICE_LOG(PVR, "Creating Vulkan per-triangle/strip renderer");
renderer = rend_Vulkan();
break;
case 5:
NOTICE_LOG(PVR, "Creating Vulkan per-pixel renderer");
renderer = rend_OITVulkan();
break;
#endif
#ifdef HAVE_D3D11
case 6:
NOTICE_LOG(PVR, "Creating Direct3D 11 per-triangle/strip renderer");
renderer = rend_D3D11();
break;
case 7:
NOTICE_LOG(PVR, "Creating Direct3D 11 per-pixel renderer");
renderer = rend_OITD3D11();
fallback_renderer = rend_D3D11();
break;
#endif
#ifdef HAVE_D3D9
case 8:
NOTICE_LOG(PVR, "Creating Direct3D 9 per-triangle/strip renderer");
renderer = rend_D3D9();
break;
#endif
#ifdef HAVE_D3D12
case 9:
NOTICE_LOG(PVR, "Creating Direct3D 12 per-triangle/strip renderer");
renderer = rend_D3D12();
break;
case 10:
NOTICE_LOG(PVR, "Creating Direct3D 12 per-pixel renderer");
renderer = rend_OITD3D12();
fallback_renderer = rend_D3D12();
break;
#endif
}
}
/* The last two framebuffers the game rendered to. A game flips between two
* and draws over what it drew there two frames before. When it renders to
* a third - after the BIOS has handed over, or on a change of video mode -
* what the console has there is not what was on screen: the renderers draw
* into one picture, though, and whatever the game does not draw over would
* be left from the frame before. The picture is cleared then, to the
* border colour. (Upstream: the BIOS's last frame showing above and below
* the picture in San Francisco Rush 2049.) */
static u32 fb_addr_history[2] = { 1, 1 };
void rend_init_renderer()
{
/* (the translucent sort's room, and a render's read back, before the
* first frame) */
SortReserve();
rtt_watch_reserve();
if (!renderer->Init())
{
delete renderer;
renderer = NULL;
if (fallback_renderer == NULL || !fallback_renderer->Init())
{
/* (not the frontend taken down with the core: no frame is drawn
* until there is a context a renderer can be set up on) */
delete fallback_renderer;
fallback_renderer = NULL;
renderer_failed = true;
ERROR_LOG(PVR, "Renderer initialization failed: no frame is drawn until the video context is made again");
return;
}
INFO_LOG(PVR, "Selected renderer initialization failed. Falling back to default renderer.");
renderer = fallback_renderer;
fallback_renderer = NULL; // avoid double-free
}
}
/* The context is going (libretro thread, the machine standing still). */
void rend_keep_picture()
{
if (renderer != NULL)
renderer->KeepPicture();
int w, h;
if (last_picture(&w, &h) != NULL)
NOTICE_LOG(RENDERER, "%d x %d picture kept for the next context", w, h);
}
bool rend_present_kept_picture()
{
if (!kept_picture_due)
return false;
kept_picture_due = false;
return renderer != NULL && renderer->PresentKeptPicture();
}
void rend_resize(int width, int height)
{
if (renderer != NULL)
renderer->Resize(width, height);
}
void rend_term_renderer()
{
/* A new renderer has no picture, and the first render to each
* framebuffer starts from an empty one - unless the last picture was
* kept for it: then the game goes on where it was. */
int w, h;
if (last_picture(&w, &h) == NULL)
fb_addr_history[0] = fb_addr_history[1] = 1;
if (renderer != NULL)
{
renderer->Term();
delete renderer;
renderer = NULL;
}
if (fallback_renderer != NULL)
{
delete fallback_renderer;
fallback_renderer = NULL;
}
texcache_scratch_free();
}
void rend_prepare_renderer()
{
if (renderer_changed || (renderer == NULL && !renderer_failed))
{
renderer_changed = false;
renderer_failed = false;
if (renderer != NULL)
{
rend_term_renderer();
kept_picture_due = true;
}
rend_create_renderer();
rend_init_renderer();
}
}
bool rend_frame(TA_context* ctx, bool draw_osd)
{
rend_prepare_renderer();
if (renderer == NULL)
{
/* (none could be set up: nothing drawn, and the emulation let go on
* as a frame that draws nothing lets it) */
#if !defined(TARGET_NO_THREADS)
if (settings.rend.ThreadedRendering)
emu_baton.AckWork();
#endif
return false;
}
#if !defined(TARGET_NO_THREADS)
/* The renderer reads the registers as they were when the game started
* this render. */
if (settings.rend.ThreadedRendering)
rend_pvr_regs = ctx->regs;
#endif
/* The palette this frame is drawn with, converted here, on the thread
* that draws, from the registers the frame is drawn with. */
palette_update();
bool proc = renderer->Process(ctx);
#if !defined(TARGET_NO_THREADS)
/* Process() has read everything the frame takes from the game's memory:
* the display lists, the textures, the framebuffer. The emulation thread
* has been asleep since it handed the render over, so what was read is
* what non-threaded rendering would have read; from here it can run
* while the frame is drawn. A render to a texture ends up back in the
* game's memory, so that one keeps the emulation asleep until it is
* finished. */
const bool hold = proc && ctx->rend.isRTT;
if (settings.rend.ThreadedRendering && !hold)
emu_baton.AckWork();
#endif
bool do_swp = proc && renderer->Render();
/* a picture kept across a context is for the first render to the screen
* after it, and that has now been made */
if (!ctx->rend.isRTT)
last_picture_drop();
#if !defined(TARGET_NO_THREADS)
if (settings.rend.ThreadedRendering && hold)
emu_baton.AckWork();
#endif
return do_swp;
}
/* Draw the render that is queued. Non-threaded rendering calls this where
* the game starts the render; threaded rendering calls it on the libretro
* thread for the render the emulation thread has handed over. Either way
* it is the same code doing the same thing to the same data. */
bool rend_single_frame(void)
{
while (true)
{
if (_pvrrc == NULL)
{
_pvrrc = DequeueRender();
if (_pvrrc == NULL)
return false;
}
bool do_swp = rend_frame(_pvrrc, true);
//clear up & free data ..
FinishRender(_pvrrc);
_pvrrc=0;
rend_pvr_regs = pvr_regs;
if (do_swp)
return true;
}
}
void rend_skip_screen(bool skip)
{
skip_screen = skip;
}
void rend_start_render(void)
{
#if !defined(TARGET_NO_THREADS)
/* One render at a time: the libretro thread may still be drawing the
* last one. Nothing is ever dropped to make room. */
if (settings.rend.ThreadedRendering)
emu_baton.WaitWorkDone();
#endif
render_called = true;
pend_rend = false;
TA_context* ctx = tactx_Get(CORE_CURRENT_CTX);
/* No end of render interrupt when rendering the framebuffer. (A frame
* drawn and kept is drawn again, but the machine's timing is as for a
* frame that is not there - as it was before frames were kept; a
* state has it, and loading one changes nothing here.) */
if (!ctx || !ctx->rend.isRenderFramebuffer)
SetREP(ctx != NULL && !ctx->drawn ? (int)(ctx->tad.thd_data - ctx->tad.thd_root) : -1);
if (ctx)
{
bool is_rtt=(FB_W_SOF1& 0x1000000)!=0 && !ctx->rend.isRenderFramebuffer;
/* A render to the screen may be where this run ends (hw/pvr/spg.cpp):
* the picture is shown once it is drawn, below. Whether or not it is
* drawn - the frontend may not show this frame - or the frame is
* there to draw: where a run ends is the machine's to say alone. */
const bool screen = !is_rtt && !ctx->rend.isRenderFramebuffer;
/* The render passes this render draws. A frame the game may start
* another render of, for others, stays in the list (rend.keep): the
* one after can find it there, as can a state saved in between.
* (What the last render of it overran is not this one's.) */
if (ctx->rend.keep)
ctx->rend.Overrun = false;
ta_select_passes(ctx);
if (!ctx->rend.keep)
tactx_Remove(ctx);
else
{
// (no longer the frame being written, as one taken out is not)
if (ta_ctx == ctx)
SetCurrentTARC(TACTX_NONE);
ctx->drawn = true;
}
if (!ctx->rend.Overrun)
{
//printf("REP: %.2f ms\n",render_end_pending_cycles/200000.0);
if (!ctx->rend.isRenderFramebuffer)
FillBGP(ctx);
ctx->rend.isRTT = is_rtt;
ctx->rend.clearFramebuffer = false;
if (!is_rtt && !ctx->rend.isRenderFramebuffer
&& FB_W_SOF1 != fb_addr_history[0] && FB_W_SOF1 != fb_addr_history[1])
{
ctx->rend.clearFramebuffer = true;
fb_addr_history[0] = fb_addr_history[1];
fb_addr_history[1] = FB_W_SOF1;
}
if (skip_screen && !is_rtt && !ctx->rend.isRenderFramebuffer)
{
/* Not shown, so not drawn. The game did render: the run
* reports no new picture, and the vblank does not take the
* framebuffer for one either. */
tactx_Drawn(ctx);
spg_screen_render();
return;
}
ctx->rend.fb_X_CLIP = FB_X_CLIP;
ctx->rend.fb_Y_CLIP = FB_Y_CLIP;
ctx->rend.fog_clamp_min = FOG_CLAMP_MIN;
ctx->rend.fog_clamp_max = FOG_CLAMP_MAX;
max_idx = std::max(max_idx, ctx->rend.idx.used());
max_vtx = std::max(max_vtx, ctx->rend.verts.used());
max_op = std::max(max_op, ctx->rend.global_param_op.used());
max_pt = std::max(max_pt, ctx->rend.global_param_pt.used());
max_tr = std::max(max_tr, ctx->rend.global_param_tr.used());
max_mvo = std::max(max_mvo, ctx->rend.global_param_mvo.used());
max_modt = std::max(max_modt, ctx->rend.modtrig.used());
if (QueueRender(ctx))
{
#if !defined(TARGET_NO_THREADS)
if (settings.rend.ThreadedRendering)
{
/* Hand the render to the libretro thread, with the registers
* as they are now, and sleep until it has read what it needs
* of the game's memory. */
memcpy(ctx->regs, pvr_regs, pvr_RendRegSize);
emu_baton.HandOver(ctx);
}
else
#endif
{
/* This is the thread that runs the SH4, in the middle of
* running it: the frame is drawn with the thread's own
* floating point settings and not the game's, as it is
* when a thread of its own draws it, and the game's are
* put back after. */
HostFloatModeBack();
rend_single_frame();
RestoreHostRoundingMode();
}
pend_rend = true;
if (!is_rtt)
frame_produced = true;
}
}
else
{
ovrn++;
INFO_LOG(PVR, "WARNING: Rendering context is overrun (%d), aborting frame", ovrn);
tactx_Drawn(ctx);
}
if (screen)
spg_screen_render();
}
}
void rend_end_render(void)
{
/* Non-threaded only: in threaded rendering this is the emulation thread,
* which leaves the renderer alone. */
if (pend_rend && !settings.rend.ThreadedRendering && renderer != NULL)
{
// (the SH4's thread, as in rend_start_render())
HostFloatModeBack();
renderer->Present();
RestoreHostRoundingMode();
}
}
void rend_term(void)
{
}
void rend_vblank()
{
/* A framebuffer the game wrote itself, with no render since the last
* vblank, is shown: it is made a render here. */
if (!render_called && fb_dirty && FB_R_CTRL.fb_enable)
{
DEBUG_LOG(PVR, "Direct framebuffer write detected");
u32 saved_ctx_addr = PARAM_BASE;
/* The frame the game is sending lists to, which this borrows the
* place of: it is the one to go back to. (What was gone back to
* was the frame PARAM_BASE names - the one the game last had
* drawn, another one. A game in the middle of sending a frame's
* lists had the rest of them go there, and that frame drawn
* without them.) */
const u32 current_ctx = ta_ctx != NULL ? ta_ctx->Address : TACTX_NONE;
PARAM_BASE = 0xF00000;
SetCurrentTARC(CORE_CURRENT_CTX);
// (NULL: there was not the memory for it, and nothing is shown)
if (ta_ctx != NULL)
{
ta_ctx->Reset();
ta_ctx->rend.isRenderFramebuffer = true;
ta_ctx->rend.isRTT = false;
rend_start_render();
}
PARAM_BASE = saved_ctx_addr;
if (current_ctx != TACTX_NONE)
SetCurrentTARC(current_ctx);
fb_dirty = false;
}
render_called = false;
check_framebuffer_write();
cheatManager.Apply();
}
/* Whether this run made a picture to show: a render to the screen (or of
* the framebuffer the game wrote), drawn. If not, the picture shown is the
* one before, and the frontend is told the frame is a duplicate - otherwise
* stale content (a 30fps title's repeated frame, a static loading screen)
* is delivered as a fresh frame and pacing breaks. (A render to a texture
* alone is no new picture.) Used by os_DoEvents to set is_dupe for the
* frontend. One-shot: cleared on read. */
bool rend_frame_produced(void)
{
bool r = frame_produced;
frame_produced = false;
return r;
}
/* The save state, from V22: whether the game has written to the
* framebuffer itself since that was last shown, and where the framebuffer
* was taken to be. A machine loaded from a state had these as the machine
* before the load left them, and showed the framebuffer at the next
* vblank, or did not, by that.
*
* Bit 1 of the first byte: a render has been started since the last
* vblank (render_called), which the vblank goes by as well. A frame used
* to end at the vblank, where that is never so; now it may end after the
* game's render, before the next vblank (hw/pvr/spg.cpp), and a machine
* loaded there showed the framebuffer the game wrote over the render, or
* not, as the machine before the load had it - run ahead, the frame after
* was not the one run straight through. A state from before has the bit
* clear, as it was where it was saved. */
void pvr_serialize_v22(void **data, unsigned int *total_size)
{
u8 dirty = (fb_dirty ? 1 : 0) | (render_called ? 2 : 0);
LIBRETRO_S(dirty);
LIBRETRO_S(fb_watch_addr_start);
LIBRETRO_S(fb_watch_addr_end);
}
void pvr_unserialize_v22(void **data, unsigned int *total_size)
{
u8 dirty = 0;
LIBRETRO_US(dirty);
LIBRETRO_US(fb_watch_addr_start);
LIBRETRO_US(fb_watch_addr_end);
fb_dirty = (dirty & 1) != 0;
render_called = (dirty & 2) != 0;
}
/* A state of before V22 does not say: nothing written, and the framebuffer
* where the registers have it. */
void pvr_state_before_v22(void)
{
fb_dirty = false;
render_called = false;
check_framebuffer_write();
}
void check_framebuffer_write()
{
u32 fb_size = (FB_R_SIZE.fb_y_size + 1) * (FB_R_SIZE.fb_x_size + FB_R_SIZE.fb_modulus) * 4;
fb_watch_addr_start = (SPG_CONTROL.interlace ? FB_R_SOF2 : FB_R_SOF1) & VRAM_MASK;
fb_watch_addr_end = fb_watch_addr_start + fb_size;
}