Files

433 lines
12 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/GBAScreens.h"
#include <cstdlib>
#include <memory>
#include <retro_atomic.h>
#include <rthreads/retro_eventcount.h>
#include "Common/MathUtil.h"
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/AbstractStagingTexture.h"
#include "VideoCommon/AbstractTexture.h"
#include "VideoCommon/PostProcessing.h"
#include "VideoCommon/TextureConfig.h"
enum
{
SCREEN_BUFFERS = 3,
/* the exchange word: the buffer in the middle, whether the GPU thread
* has not taken it yet, and the size of its frame */
EXCHANGE_INDEX = 3,
EXCHANGE_FRESH = 4,
EXCHANGE_WIDTH_SHIFT = 3,
EXCHANGE_HEIGHT_SHIFT = 12,
EXCHANGE_SIZE_MASK = 0x1FF,
SCREEN_STOPPED = 0,
SCREEN_RUNNING = 1,
/* the buffers are being taken back: the GBA waits to stop */
SCREEN_REVOKING = 2,
INIT_NONE = 0,
INIT_BUSY = 1,
INIT_DONE = 2
};
/* Between a GBA's thread (the writer) and the GPU thread (the reader).
* The buffers, their stride and writer_index are set by the GPU thread
* before it sets lent; the writer reads them, and from then on owns
* writer_index and attached, until it was told to let go. */
struct GBAScreen
{
u32* buffers[SCREEN_BUFFERS];
size_t stride;
void (*detach)(void* context);
void* detach_context;
retro_eventcount_t event;
retro_atomic_int_t lent;
retro_atomic_int_t exchange;
retro_atomic_int_t state;
retro_atomic_int_t detached;
int writer_index;
int attached;
};
/* The GPU thread's side of a screen. */
struct GBAScreenView
{
AbstractTexture* texture;
AbstractStagingTexture* staging[SCREEN_BUFFERS];
/* without upload memory that stays mapped: the three buffers */
u32* memory;
/* the buffer the texture was last filled from, given back to the GBA
* once that is done */
int copying;
/* width << 16 | height of what the texture holds; 0 for nothing */
u32 shown_size;
/* the GPU had nothing to lend: not asked again until the backend is */
bool lend_failed;
};
static GBAScreen s_screens[GBA_SCREENS];
static GBAScreenView s_views[GBA_SCREENS];
static retro_atomic_int_t s_shown;
static retro_atomic_int_t s_init;
static void ScreensInit()
{
unsigned i;
if (retro_atomic_load_acquire_int(&s_init) == INIT_DONE)
return;
if (!retro_atomic_cas_int(&s_init, INIT_NONE, INIT_BUSY))
{
while (retro_atomic_load_acquire_int(&s_init) != INIT_DONE)
{
}
return;
}
for (i = 0; i < GBA_SCREENS; i++)
{
retro_eventcount_init(&s_screens[i].event);
retro_atomic_int_init(&s_screens[i].lent, 0);
retro_atomic_int_init(&s_screens[i].exchange, 0);
retro_atomic_int_init(&s_screens[i].state, SCREEN_STOPPED);
retro_atomic_int_init(&s_screens[i].detached, 0);
}
retro_atomic_store_release_int(&s_init, INIT_DONE);
}
/* Waits on the screen's event until test(screen) holds. */
static void ScreenWait(GBAScreen* screen, bool (*test)(GBAScreen* screen))
{
while (!test(screen))
{
const int key = retro_eventcount_prepare_wait(&screen->event);
if (test(screen))
{
retro_eventcount_cancel_wait(&screen->event);
break;
}
retro_eventcount_commit_wait(&screen->event, key);
}
}
static bool ScreenNotRevoking(GBAScreen* screen)
{
return retro_atomic_load_acquire_int(&screen->state) != SCREEN_REVOKING;
}
static bool ScreenDetached(GBAScreen* screen)
{
return retro_atomic_load_acquire_int(&screen->detached) != 0;
}
void GBAScreensSetShown(bool shown)
{
retro_atomic_store_relaxed_int(&s_shown, shown ? 1 : 0);
}
void GBAScreenStart(unsigned gba, void (*detach)(void* context), void* context)
{
GBAScreen* const screen = &s_screens[gba];
ScreensInit();
screen->detach = detach;
screen->detach_context = context;
screen->attached = 0;
retro_atomic_store_release_int(&screen->state, SCREEN_RUNNING);
}
void GBAScreenStop(unsigned gba)
{
GBAScreen* const screen = &s_screens[gba];
ScreensInit();
for (;;)
{
if (retro_atomic_cas_int(&screen->state, SCREEN_RUNNING, SCREEN_STOPPED))
break;
if (retro_atomic_load_acquire_int(&screen->state) == SCREEN_STOPPED)
break;
/* the buffers are being taken back: the GBA's thread, which would
* have let go, is gone, so nothing renders into them any more */
GBAScreenDetached(gba);
ScreenWait(screen, ScreenNotRevoking);
}
}
u32* GBAScreenFrameEnd(unsigned gba, u32 width, u32 height, size_t* stride)
{
GBAScreen* const screen = &s_screens[gba];
if (!retro_atomic_load_acquire_int(&screen->lent))
{
screen->attached = 0;
return nullptr;
}
if (!screen->attached)
{
/* the frame that ended went into the GBA's own buffer */
screen->attached = 1;
}
else
{
if (width > GBA_SCREEN_MAX_WIDTH)
width = GBA_SCREEN_MAX_WIDTH;
if (height > GBA_SCREEN_MAX_HEIGHT)
height = GBA_SCREEN_MAX_HEIGHT;
screen->writer_index =
retro_atomic_exchange_int(&screen->exchange,
screen->writer_index | EXCHANGE_FRESH |
(int)(width << EXCHANGE_WIDTH_SHIFT) |
(int)(height << EXCHANGE_HEIGHT_SHIFT)) &
EXCHANGE_INDEX;
}
*stride = screen->stride;
return screen->buffers[screen->writer_index];
}
u32* GBAScreenBuffer(unsigned gba, size_t* stride)
{
GBAScreen* const screen = &s_screens[gba];
if (!screen->attached || !retro_atomic_load_acquire_int(&screen->lent))
return nullptr;
*stride = screen->stride;
return screen->buffers[screen->writer_index];
}
void GBAScreenDetached(unsigned gba)
{
GBAScreen* const screen = &s_screens[gba];
screen->attached = 0;
retro_atomic_store_release_int(&screen->detached, 1);
retro_eventcount_notify(&screen->event);
}
static void ViewFree(GBAScreenView* view)
{
int i;
for (i = 0; i < SCREEN_BUFFERS; i++)
{
AbstractStagingTextureDelete(view->staging[i]);
view->staging[i] = nullptr;
}
AbstractTextureDelete(view->texture);
view->texture = nullptr;
std::free(view->memory);
view->memory = nullptr;
view->shown_size = 0;
view->lend_failed = false;
}
/* Lends the GBA its buffers: upload memory that stays mapped, or plain
* memory uploaded from. */
static bool ScreenLend(unsigned gba)
{
GBAScreen* const screen = &s_screens[gba];
GBAScreenView* const view = &s_views[gba];
const TextureConfig config(GBA_SCREEN_MAX_WIDTH, GBA_SCREEN_MAX_HEIGHT, 1, 1, 1,
AbstractTextureFormat::RGBA8, 0, AbstractTextureType::Texture_2DArray);
int i;
if (view->lend_failed)
return false;
view->texture = g_gfx->CreateTexture(config, "GBA screen");
if (!view->texture)
{
view->lend_failed = true;
return false;
}
for (i = 0; i < SCREEN_BUFFERS; i++)
{
view->staging[i] = g_gfx->CreateStagingTexture(StagingTextureType::Upload, config);
if (!view->staging[i] || !view->staging[i]->m_persistent_map || !view->staging[i]->Map() ||
(view->staging[i]->GetMappedStride() & 3) != 0)
{
break;
}
}
if (i == SCREEN_BUFFERS)
{
for (i = 0; i < SCREEN_BUFFERS; i++)
screen->buffers[i] = reinterpret_cast<u32*>(view->staging[i]->GetMappedPointer());
screen->stride = view->staging[0]->GetMappedStride() / sizeof(u32);
}
else
{
const size_t plane = (size_t)GBA_SCREEN_MAX_WIDTH * GBA_SCREEN_MAX_HEIGHT;
for (i = 0; i < SCREEN_BUFFERS; i++)
{
AbstractStagingTextureDelete(view->staging[i]);
view->staging[i] = nullptr;
}
view->memory = static_cast<u32*>(std::calloc(plane * SCREEN_BUFFERS, sizeof(u32)));
if (!view->memory)
{
ViewFree(view);
view->lend_failed = true;
return false;
}
for (i = 0; i < SCREEN_BUFFERS; i++)
screen->buffers[i] = view->memory + plane * i;
screen->stride = GBA_SCREEN_MAX_WIDTH;
}
/* the GBA writes 0 and the GPU copies from 2; 1 is in the middle */
screen->writer_index = 0;
retro_atomic_store_relaxed_int(&screen->exchange, 1);
view->copying = 2;
view->shown_size = 0;
retro_atomic_store_release_int(&screen->lent, 1);
return true;
}
/* Whether the GPU is done with the buffer the texture was filled from. */
static bool ViewCopyDone(GBAScreenView* view)
{
return !view->staging[view->copying] || StagingTransferDone(view->staging[view->copying]);
}
static void ViewUpload(GBAScreenView* view, int index, u32 width, u32 height)
{
if (view->staging[index])
{
const MathUtil::Rectangle<int> rect(0, 0, (int)width, (int)height);
view->staging[index]->CopyToTexture(rect, view->texture, rect, 0, 0);
}
else
{
const size_t plane = (size_t)GBA_SCREEN_MAX_WIDTH * GBA_SCREEN_MAX_HEIGHT;
view->texture->Load(0, GBA_SCREEN_MAX_WIDTH, GBA_SCREEN_MAX_HEIGHT, GBA_SCREEN_MAX_WIDTH,
reinterpret_cast<const u8*>(view->memory + plane * index),
plane * sizeof(u32));
}
}
bool GBAScreensUpdate()
{
bool any = false;
unsigned i;
if (!retro_atomic_load_relaxed_int(&s_shown) ||
retro_atomic_load_acquire_int(&s_init) != INIT_DONE)
{
return false;
}
for (i = 0; i < GBA_SCREENS; i++)
{
GBAScreen* const screen = &s_screens[i];
GBAScreenView* const view = &s_views[i];
int word;
if (retro_atomic_load_relaxed_int(&screen->state) == SCREEN_STOPPED)
{
view->shown_size = 0;
continue;
}
if (!retro_atomic_load_relaxed_int(&screen->lent) && !ScreenLend(i))
continue;
word = retro_atomic_load_acquire_int(&screen->exchange);
if ((word & EXCHANGE_FRESH) && ViewCopyDone(view))
{
/* the GBA only ever leaves it fresh: what is swapped out is */
word = retro_atomic_exchange_int(&screen->exchange, view->copying);
{
const u32 width = ((u32)word >> EXCHANGE_WIDTH_SHIFT) & EXCHANGE_SIZE_MASK;
const u32 height = ((u32)word >> EXCHANGE_HEIGHT_SHIFT) & EXCHANGE_SIZE_MASK;
view->copying = word & EXCHANGE_INDEX;
ViewUpload(view, view->copying, width, height);
view->shown_size = width << 16 | height;
}
}
if (view->shown_size)
any = true;
}
return any;
}
void GBAScreensDraw(VideoCommon::PostProcessing* post_processor, int width, int height)
{
u32 sizes[GBA_SCREENS];
const AbstractTexture* textures[GBA_SCREENS];
u32 total_width = 0;
u32 count = 0;
u32 scale;
u32 i;
int right = width;
for (i = 0; i < GBA_SCREENS; i++)
{
if (!s_views[i].shown_size)
continue;
sizes[count] = s_views[i].shown_size;
textures[count] = s_views[i].texture;
total_width += sizes[count] >> 16;
count++;
}
if (!count || width <= 0 || height <= 0)
return;
/* whole pixels, about a third of the picture's height */
scale = (u32)height / 480;
if (scale < 1)
scale = 1;
for (i = count; i-- > 0;)
{
const int src_width = (int)(sizes[i] >> 16);
const int src_height = (int)(sizes[i] & 0xFFFF);
int dst_width = src_width * (int)scale;
int dst_height = src_height * (int)scale;
/* side by side across the picture when wider than it */
if (total_width * scale > (u32)width)
{
dst_width = (int)((s64)src_width * width / total_width);
dst_height = (int)((s64)src_height * width / total_width);
}
post_processor->BlitScreen(
MathUtil::Rectangle<int>(right - dst_width, height - dst_height, right, height),
MathUtil::Rectangle<int>(0, 0, src_width, src_height), textures[i]);
right -= dst_width;
}
}
void GBAScreensRevoke()
{
unsigned i;
if (retro_atomic_load_acquire_int(&s_init) != INIT_DONE)
return;
for (i = 0; i < GBA_SCREENS; i++)
{
GBAScreen* const screen = &s_screens[i];
if (retro_atomic_load_relaxed_int(&screen->lent))
{
retro_atomic_store_release_int(&screen->lent, 0);
/* a running GBA lets go on its own thread first */
if (retro_atomic_cas_int(&screen->state, SCREEN_RUNNING, SCREEN_REVOKING))
{
retro_atomic_store_release_int(&screen->detached, 0);
screen->detach(screen->detach_context);
ScreenWait(screen, ScreenDetached);
retro_atomic_store_release_int(&screen->state, SCREEN_RUNNING);
retro_eventcount_notify(&screen->event);
}
}
ViewFree(&s_views[i]);
}
}