mirror of
https://github.com/libretro/parallel-dc.git
synced 2026-11-10 17:26:21 -10:00
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.
549 lines
14 KiB
C++
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
|
|
}
|