Files
libretroadmin 53bc61f4d8 Renders to textures: attachments and read-back room made before play
The texture drawers' attachments are made with them, as large as a
render to a texture can be (TextureDrawer::MaxSide()), as SSAA's already
were: the per-triangle drawer's depth, and the per-pixel drawer's colour
and depth with their framebuffers (OITDrawer::MakeAttachments(), split
from MakeBuffers()), each with a framebuffer's place for every image. The
room a render read back into video memory is put in is reserved before a
renderer's first frame for one of the screen's size (rtt_watch_reserve()).
A game's first render to a texture no longer makes them mid-game.

Four sites come off the list of allocations during play, 22 left.
2026-11-10 03:57:13 +01:00

549 lines
14 KiB
C++

/* See rtt_watch.h. */
#include "rtt_watch.h"
#include "hw/mem/_vmem.h"
#include "hw/pvr/pvr_mem.h"
#include "hw/sh4/modules/mmu.h"
#include "rend/TexCache.h"
#include <cstdlib>
#include <cstring>
#if !defined(TARGET_NO_THREADS)
#include <rthreads/rthreads.h>
#include "libretro/emu_baton.h"
extern EmuBaton emu_baton;
#endif
#define RTT_WATCH_SLOTS 16
static RttWatch slots[RTT_WATCH_SLOTS];
static RttWatchBackend backend;
static u8 *pixels; /* kept: the fault handler does not allocate */
static size_t pixels_size;
static uintptr_t garbage[RTT_WATCH_SLOTS];
static unsigned garbage_count;
int rtt_watch_request;
static u32 request_offset, request_size;
static bool request_overwrite; /* the request is rtt_watch_overwrite()'s */
static u32 next_serial;
#if !defined(TARGET_NO_THREADS)
static uintptr_t render_thread, emu_thread_id;
#endif
static inline u32 first_page(const RttWatch& s) { return s.addr / PAGE_SIZE; }
static inline u32 last_page(const RttWatch& s) { return (s.addr + s.bytes - 1) / PAGE_SIZE; }
static bool page_wanted(u32 page, const RttWatch *but)
{
for (const RttWatch& s : slots)
if (s.used && &s != but && page >= first_page(s) && page <= last_page(s))
return true;
return false;
}
/* The slot is done with: its pages go back to what they were, except
* where another slot has them too. */
static void close_slot(RttWatch& s, bool opened)
{
const u32 last = last_page(s);
s.used = false;
/* runs of pages that get the same, in one call each: a page costs a
* system call for every mapping of it */
for (u32 page = first_page(s); page <= last; )
{
const bool wanted = page_wanted(page, &s);
u32 end = page + 1;
while (end <= last && page_wanted(end, &s) == wanted)
end++;
if (!wanted)
_vmem_unwatch_vram(page * PAGE_SIZE, (end - page) * PAGE_SIZE);
else if (opened)
_vmem_watch_vram(page * PAGE_SIZE, (end - page) * PAGE_SIZE);
page = end;
}
}
static void release_garbage(void)
{
while (garbage_count != 0)
backend.release(garbage[--garbage_count]);
}
/* What the console adds to a colour before it drops the low bits, when
* FB_W_CTRL asks for dithering: a number from this four by four pattern,
* laid over the picture from its first pixel, as large as the bits that are
* dropped (a half of it where three are, a quarter where two are). The
* colour stops at 255. Without dithering the bits are just dropped. */
static const u8 dither_pattern[4][4] = {
{ 5, 13, 7, 15 },
{ 9, 1, 11, 3 },
{ 6, 14, 4, 12 },
{ 10, 2, 8, 0 },
};
static inline u32 dithered(u32 colour, u32 add)
{
colour += add;
return colour > 255 ? 255 : colour;
}
static void pack_dithered(const RttWatch& s, const u8 *rgba, size_t pitch, u16 *dst)
{
const u32 skip = (s.stride - s.w * 2) / 2;
for (u32 l = 0; l < s.h; l++, dst += skip, rgba += pitch)
{
const u8 *p = rgba;
// (the pattern goes by the pixel's place in the frame)
const u8 *row = dither_pattern[(l + s.y0) & 3];
switch (s.packmode)
{
case 1:
for (u32 c = 0; c < s.w; c++, p += 4)
{
const u32 d = row[(c + s.x0) & 3];
*dst++ = (u16)(((dithered(p[0], d >> 1) >> 3) << 11) | ((dithered(p[1], d >> 2) >> 2) << 5)
| (dithered(p[2], d >> 1) >> 3));
}
break;
case 2:
/* (alpha is not a colour: its low bits are dropped) */
for (u32 c = 0; c < s.w; c++, p += 4)
{
const u32 d = row[(c + s.x0) & 3];
*dst++ = (u16)(((dithered(p[0], d) >> 4) << 8) | ((dithered(p[1], d) >> 4) << 4) | (dithered(p[2], d) >> 4)
| ((p[3] >> 4) << 12));
}
break;
default:
for (u32 c = 0; c < s.w; c++, p += 4)
{
const u32 d = row[(c + s.x0) & 3] >> 1;
*dst++ = (u16)(((dithered(p[0], d) >> 3) << 10) | ((dithered(p[1], d) >> 3) << 5) | (dithered(p[2], d) >> 3)
| (s.packmode == 0 ? s.kval_bit : p[3] >= s.alpha_threshold ? 0x8000 : 0));
}
break;
}
}
}
/* As the console packs a pixel into the framebuffer it renders to. */
static void pack(const RttWatch& s, const u8 *rgba, size_t pitch, u16 *dst)
{
const u32 skip = (s.stride - s.w * 2) / 2;
if (s.dither)
{
pack_dithered(s, rgba, pitch, dst);
return;
}
for (u32 l = 0; l < s.h; l++, dst += skip, rgba += pitch)
{
const u8 *p = rgba;
switch (s.packmode)
{
case 0:
for (u32 c = 0; c < s.w; c++, p += 4)
*dst++ = (u16)(((p[0] >> 3) << 10) | ((p[1] >> 3) << 5) | (p[2] >> 3) | s.kval_bit);
break;
case 1:
for (u32 c = 0; c < s.w; c++, p += 4)
*dst++ = (u16)(((p[0] >> 3) << 11) | ((p[1] >> 2) << 5) | (p[2] >> 3));
break;
case 2:
for (u32 c = 0; c < s.w; c++, p += 4)
*dst++ = (u16)(((p[0] >> 4) << 8) | ((p[1] >> 4) << 4) | (p[2] >> 4) | ((p[3] >> 4) << 12));
break;
default:
for (u32 c = 0; c < s.w; c++, p += 4)
*dst++ = (u16)(((p[0] >> 3) << 10) | ((p[1] >> 3) << 5) | (p[2] >> 3)
| (p[3] >= s.alpha_threshold ? 0x8000 : 0));
break;
}
}
}
static bool take_opens; /* the watch's pages are opened first (fill()) */
/* The picture as the renderer hands it over, read once - where the
* renderer has it, its readback memory, as often as not - packed into
* video memory. (None: what the console would have found, nothing.) */
static void take(const RttWatch *watch, const u8 *rgba, size_t pitch)
{
const RttWatch& s = *watch;
if (rgba == NULL)
{
memset(pixels, 0, (size_t)s.w * s.h * 4);
rgba = pixels;
pitch = (size_t)s.w * 4;
}
if (take_opens)
{
_vmem_open_watched_vram(first_page(s) * PAGE_SIZE, (last_page(s) - first_page(s) + 1) * PAGE_SIZE);
take_opens = false;
}
pack(s, rgba, pitch, (u16 *)&vram[s.addr]);
}
/* Render thread, and the machine is not running: the picture goes into
* video memory. */
static void fill(RttWatch& s)
{
take_opens = true;
backend.read(&s, pixels, take);
// (a read that handed nothing over leaves what a failed one does)
if (take_opens)
take(&s, NULL, 0);
close_slot(s, true);
if (s.owned)
backend.release(s.tex);
}
static void drop(RttWatch& s)
{
close_slot(s, false);
if (s.owned)
garbage[garbage_count++] = s.tex;
}
/* Several at once: their copies off the card begun together first, so
* that the card is waited for once, not once each. */
static void fill_many(RttWatch **list, unsigned count)
{
if (count > 1 && backend.prefetch != NULL)
{
const RttWatch *watches[RTT_WATCH_SLOTS];
for (unsigned i = 0; i < count; i++)
watches[i] = list[i];
backend.prefetch(watches, count);
}
for (unsigned i = 0; i < count; i++)
if (list[i]->used)
fill(*list[i]);
}
/* never leave a page that faults with nothing behind it */
static void unwatch_unwanted(u32 first, u32 last)
{
for (u32 page = first; page <= last; page++)
if (_vmem_vram_page_watched(page) && !page_wanted(page, NULL))
_vmem_unwatch_vram(page * PAGE_SIZE, PAGE_SIZE);
}
static void fill_range(u32 offset, u32 size)
{
const u32 first = offset / PAGE_SIZE, last = (offset + size - 1) / PAGE_SIZE;
RttWatch *list[RTT_WATCH_SLOTS];
unsigned count = 0;
for (RttWatch& s : slots)
if (s.used && first_page(s) <= last && last_page(s) >= first)
list[count++] = &s;
fill_many(list, count);
unwatch_unwanted(first, last);
}
/* A write of every byte from @offset to @offset + @size: what it covers
* whole is dropped, not fetched, and what it covers in part (or shares a
* page with) is fetched. */
static void overwrite_range(u32 offset, u32 size)
{
const u32 first = offset / PAGE_SIZE, last = (offset + size - 1) / PAGE_SIZE;
RttWatch *list[RTT_WATCH_SLOTS];
unsigned count = 0;
for (RttWatch& s : slots)
{
if (!s.used || first_page(s) > last || last_page(s) < first)
continue;
if (s.addr >= offset && s.addr + s.bytes <= offset + size)
drop(s);
else
list[count++] = &s;
}
fill_many(list, count);
unwatch_unwanted(first, last);
}
static void drop_range(u32 offset, u32 size)
{
const u32 first = offset / PAGE_SIZE, last = (offset + size - 1) / PAGE_SIZE;
for (RttWatch& s : slots)
if (s.used && first_page(s) <= last && last_page(s) >= first)
drop(s);
unwatch_unwanted(first, last);
}
void rtt_watch_supersede(u32 addr, u32 bytes)
{
for (RttWatch& s : slots)
{
if (!s.used || s.addr >= addr + bytes || s.addr + s.bytes <= addr)
continue;
if (s.addr >= addr && s.addr + s.bytes <= addr + bytes)
{
/* Its pages stay as they are: the render that covers it is
* about to have them watched, and a game that renders to the
* same texture every frame pays for no system call here. (Were
* that not to follow, the first touch finds nothing waiting
* and opens them.) */
s.used = false;
if (s.owned)
backend.release(s.tex);
}
else
fill(s);
}
}
void rtt_watch_add(const RttWatch *watch, const RttWatchBackend *b)
{
RttWatch *slot = NULL;
const size_t need = (size_t)watch->w * watch->h * 4;
backend = *b;
#if !defined(TARGET_NO_THREADS)
render_thread = sthread_get_current_thread_id();
#endif
if (watch->bytes != 0 && watch->addr + watch->bytes <= VRAM_SIZE)
VramRendered(watch->addr, watch->bytes, watch->target);
release_garbage();
if (need > pixels_size)
{
free(pixels);
pixels = (u8 *)malloc(need);
pixels_size = need;
}
#if defined(TARGET_NO_EXCEPTIONS)
const bool can_watch = false;
#else
/* With the MMU on, the game's own mappings of video memory come and go
* outside the ones closed here. */
const bool can_watch = !mmu_enabled();
#endif
if (!can_watch || watch->bytes == 0 || watch->addr + watch->bytes > VRAM_SIZE)
{
/* into video memory now, as the console has it */
if (watch->bytes != 0 && watch->addr + watch->bytes <= VRAM_SIZE)
{
drop_range(watch->addr, watch->bytes);
backend.read(watch, pixels, take);
}
if (watch->owned)
backend.release(watch->tex);
return;
}
for (RttWatch& s : slots)
if (!s.used)
{
slot = &s;
break;
}
if (slot == NULL)
{
/* the one that has waited longest: the likeliest not to be wanted */
slot = &slots[0];
for (RttWatch& s : slots)
if (s.age > slot->age)
slot = &s;
fill(*slot);
}
*slot = *watch;
slot->used = true;
slot->age = 0;
if (++next_serial == 0)
next_serial = 1;
slot->serial = next_serial;
_vmem_watch_vram(first_page(*slot) * PAGE_SIZE, (last_page(*slot) - first_page(*slot) + 1) * PAGE_SIZE);
}
bool rtt_watch_take(uintptr_t tex)
{
for (RttWatch& s : slots)
if (s.used && s.tex == tex && !s.owned)
{
s.owned = true;
return true;
}
return false;
}
/* rtt_watch_touch() and rtt_watch_overwrite(): fill_range() or
* overwrite_range() on the thread that renders */
static void watched_access(u32 offset, u32 size, bool overwrite)
{
bool any = false;
if (size == 0 || offset >= VRAM_SIZE)
return;
if (size > VRAM_SIZE - offset)
size = VRAM_SIZE - offset;
for (u32 page = offset / PAGE_SIZE; page <= (offset + size - 1) / PAGE_SIZE && !any; page++)
any = _vmem_vram_page_watched(page);
if (!any)
return;
#if !defined(TARGET_NO_THREADS)
const uintptr_t self = sthread_get_current_thread_id();
if (self != render_thread)
{
if (self == emu_thread_id && emu_baton.FrameRunning())
{
/* The thread that renders is in retro_run, taking what this one
* hands over: it fetches the picture while this one sleeps. */
request_offset = offset;
request_size = size;
request_overwrite = overwrite;
emu_baton.WaitWorkDone();
emu_baton.HandOver(&rtt_watch_request);
return;
}
/* Some other thread, with the machine standing still and nobody
* rendering (the frontend saving a state from a thread of its own):
* the graphics card cannot be asked from here. The picture stays
* where it is drawn from; video memory does not get it. */
drop_range(offset, size);
return;
}
#endif
if (overwrite)
overwrite_range(offset, size);
else
fill_range(offset, size);
}
void rtt_watch_touch(u32 offset, u32 size)
{
watched_access(offset, size, false);
}
void rtt_watch_overwrite(u32 offset, u32 size)
{
watched_access(offset, size, true);
}
bool rtt_watch_fault(void *address)
{
const u32 offset = _vmem_get_vram_offset(address);
if (offset == (u32)-1 || offset >= VRAM_SIZE || !_vmem_vram_page_watched(offset / PAGE_SIZE))
return false;
rtt_watch_touch(offset & ~(u32)PAGE_MASK, PAGE_SIZE);
return true;
}
void rtt_watch_serve(void)
{
release_garbage();
if (request_overwrite)
overwrite_range(request_offset, request_size);
else
fill_range(request_offset, request_size);
request_overwrite = false;
}
bool rtt_watch_pending(u32 offset, u32 size, RttWatch *watch)
{
const RttWatch *found = NULL;
for (const RttWatch& s : slots)
if (s.used && s.addr < offset + size && s.addr + s.bytes > offset)
{
if (found != NULL)
return false;
found = &s;
}
if (found == NULL)
return false;
*watch = *found;
return true;
}
void rtt_watch_flush(void)
{
RttWatch *list[RTT_WATCH_SLOTS];
unsigned count = 0;
if (backend.read == NULL)
return;
for (RttWatch& s : slots)
if (s.used)
list[count++] = &s;
fill_many(list, count);
}
#define RTT_WATCH_SETTLED 3
void rtt_watch_frame(void)
{
const RttWatch *early[RTT_WATCH_SLOTS];
RttWatch *settled[RTT_WATCH_SLOTS];
unsigned n_early = 0, n_settled = 0;
if (backend.read == NULL)
return;
for (RttWatch& s : slots)
{
if (!s.used)
continue;
if (++s.age >= RTT_WATCH_SETTLED)
settled[n_settled++] = &s;
else if (s.age == RTT_WATCH_SETTLED - 1)
early[n_early++] = &s;
}
/* the copies of the ones that settle next frame, begun now: by then the
* card is done with them, and their fill() waits for nothing. (Begun
* twice, for one already begun: the backend sees it is.) */
if (n_early != 0 && backend.prefetch != NULL)
backend.prefetch(early, n_early);
fill_many(settled, n_settled);
}
void rtt_watch_forget(void)
{
for (RttWatch& s : slots)
if (s.used)
{
/* A texture that shows this picture shows what video memory
* never had and now never will: it is made again, from what
* is there. (Before textures were kept through the loading of
* a state, they all were.) */
VramLockedWriteRange(s.addr, s.bytes);
drop(s);
}
}
void rtt_watch_reserve(void)
{
const size_t need = (size_t)640 * 480 * 4;
if (need > pixels_size)
{
free(pixels);
pixels = (u8 *)malloc(need);
pixels_size = pixels != NULL ? need : 0;
}
}
void rtt_watch_term(void)
{
for (RttWatch& s : slots)
if (s.used)
drop(s);
if (backend.release != NULL)
release_garbage();
garbage_count = 0;
}
void rtt_watch_emu_thread(void)
{
#if !defined(TARGET_NO_THREADS)
emu_thread_id = sthread_get_current_thread_id();
#endif
}