added support for honoring VO_CONTROL->blank_video

added support for rendering the raw framebuffer in case it is
directly written to
This commit is contained in:
Anthony Pesch 2017-10-10 17:10:54 -04:00
parent 9f67222a03
commit 046411f8f8
10 changed files with 677 additions and 311 deletions

View File

@ -45,6 +45,15 @@ enum {
EMU_SHUTDOWN,
EMU_WAITING,
EMU_RUNFRAME,
EMU_DRAWFRAME,
EMU_ENDFRAME,
};
enum {
EMU_DRAW_INVALID,
EMU_DRAW_PIXELS,
EMU_DRAW_CONTEXT,
EMU_DRAW_DISABLED,
};
struct emu_texture {
@ -77,15 +86,22 @@ struct emu {
volatile int state;
volatile unsigned frame;
thread_t run_thread;
mutex_t run_mutex;
cond_t run_cond;
mutex_t frame_mutex;
cond_t frame_cond;
mutex_t req_mutex;
cond_t req_cond;
mutex_t res_mutex;
cond_t res_cond;
/* latest context from the dreamcast, ready to be rendered */
/* latest video state pushed by the dreamcast */
volatile int latest;
struct {
uint8_t pixels[PVR_FRAMEBUFFER_SIZE];
int width;
int height;
} latest_fb;
struct tr_context latest_rc;
/* latest context submitted to emu_start_render */
struct ta_context *pending_ctx;
struct tr_context pending_rc;
unsigned pending_id;
/* texture cache. the dreamcast interface calls into us when new contexts are
available to be rendered. parsing the contexts, uploading their textures to
@ -236,8 +252,8 @@ static void emu_register_texture_source(struct emu *emu, union tsp tsp,
}
/* mark texture source valid for the current pending frame */
int first_registration_this_frame = entry->frame != emu->pending_id;
entry->frame = emu->pending_id;
int first_registration_this_frame = entry->frame != emu->frame;
entry->frame = emu->frame;
/* set texture address */
if (!entry->texture || !entry->palette) {
@ -336,12 +352,29 @@ static void emu_start_tracing(struct emu *emu) {
/*
* dreamcast guest interface
*/
static void emu_vblank_in(void *userdata) {}
static void emu_vblank_in(void *userdata, int video_disabled) {
struct emu *emu = userdata;
if (emu->multi_threaded) {
mutex_lock(emu->res_mutex);
}
if (video_disabled) {
emu->latest = EMU_DRAW_DISABLED;
}
emu->state = EMU_DRAWFRAME;
if (emu->multi_threaded) {
cond_signal(emu->res_cond);
mutex_unlock(emu->res_mutex);
}
}
static void emu_vblank_out(void *userdata) {
struct emu *emu = userdata;
emu->frame++;
emu->state = EMU_ENDFRAME;
}
static void emu_finish_render(void *userdata) {
@ -352,13 +385,13 @@ static void emu_finish_render(void *userdata) {
textures, etc. during the estimated render time. however, if it hasn't
finished, the emulation thread must be paused to avoid altering
the yet-to-be-uploaded texture memory */
mutex_lock(emu->frame_mutex);
mutex_lock(emu->res_mutex);
/* if pending_ctx is non-NULL here, a frame is being skipped */
emu->pending_ctx = NULL;
cond_signal(emu->frame_cond);
cond_signal(emu->res_cond);
mutex_unlock(emu->frame_mutex);
mutex_unlock(emu->res_mutex);
}
}
@ -368,7 +401,7 @@ static void emu_start_render(void *userdata, struct ta_context *ctx) {
/* incement internal frame number. this frame number is assigned to the each
texture source registered to assert synchronization between the emulator
and video thread is working as expected */
emu->pending_id++;
emu->frame++;
/* now that the video thread is sure to not be accessing the texture data,
mark any textures dirty that were invalidated by a memory watch */
@ -386,18 +419,27 @@ static void emu_start_render(void *userdata, struct ta_context *ctx) {
if (emu->multi_threaded) {
/* save off context and notify video thread that it's available */
mutex_lock(emu->frame_mutex);
mutex_lock(emu->res_mutex);
emu->pending_ctx = ctx;
cond_signal(emu->frame_cond);
cond_signal(emu->res_cond);
mutex_unlock(emu->frame_mutex);
mutex_unlock(emu->res_mutex);
} else {
/* convert the context immediately */
tr_convert_context(emu->r, emu, &emu_find_texture, ctx, &emu->pending_rc);
emu->pending_ctx = ctx;
}
}
static void emu_push_pixels(void *userdata, const uint8_t *data, int w, int h) {
struct emu *emu = userdata;
emu->latest = EMU_DRAW_PIXELS;
memcpy(emu->latest_fb.pixels, data, w * h * 4);
emu->latest_fb.width = w;
emu->latest_fb.height = h;
}
static void emu_push_audio(void *userdata, const int16_t *data, int frames) {
struct emu *emu = userdata;
audio_push(emu->host, data, frames);
@ -520,52 +562,40 @@ static void emu_debug_menu(struct emu *emu) {
/*
* frame running logic
*/
static void emu_run_frame(struct emu *emu);
static void emu_run_until_vblank(struct emu *emu);
static void *emu_run_thread(void *data) {
struct emu *emu = data;
while (1) {
/* wait for video thread to request a frame to be ran */
{
mutex_lock(emu->run_mutex);
mutex_lock(emu->req_mutex);
while (emu->state == EMU_WAITING) {
cond_wait(emu->run_cond, emu->run_mutex);
}
if (emu->state == EMU_SHUTDOWN) {
mutex_unlock(emu->run_mutex);
break;
}
emu_run_frame(emu);
emu->state = EMU_WAITING;
mutex_unlock(emu->run_mutex);
while (emu->state == EMU_WAITING) {
cond_wait(emu->req_cond, emu->req_mutex);
}
/* in case no context was submitted before the frame end, signal the video
thread to continue, reusing the previous context */
{
mutex_lock(emu->frame_mutex);
cond_signal(emu->frame_cond);
mutex_unlock(emu->frame_mutex);
if (emu->state == EMU_SHUTDOWN) {
mutex_unlock(emu->req_mutex);
break;
}
emu_run_until_vblank(emu);
emu->state = EMU_WAITING;
mutex_unlock(emu->req_mutex);
}
return NULL;
}
static void emu_run_frame(struct emu *emu) {
static void emu_run_until_vblank(struct emu *emu) {
const int64_t MACHINE_STEP = HZ_TO_NANO(1000);
unsigned start_frame = emu->frame;
/* run up to the next vblank */
while (emu->frame == start_frame) {
emu->state = EMU_RUNFRAME;
while (emu->state == EMU_RUNFRAME || emu->state == EMU_DRAWFRAME) {
dc_tick(emu->dc, MACHINE_STEP);
}
}
@ -613,51 +643,92 @@ void emu_render_frame(struct emu *emu) {
r_viewport(emu->r, frame_x, frame_y, frame_width, frame_height);
/* an overview of each frames lifecycle looks like:
main thread | emulation thread
---------------------------------------------------------------------------
set EMU_RUNFRAME |
---------------------------------------------------------------------------
| see EMU_RUNFRAME, start running frame
---------------------------------------------------------------------------
wait for EMU_DRAWFRAME / or for |
pending_ctx to be set |
---------------------------------------------------------------------------
| emu_start_render sets pending_ctx or
| emu_push_pixels copies off framebuffer
---------------------------------------------------------------------------
convert pending_ctx if set |
---------------------------------------------------------------------------
| emu_vblank_in sets EMU_DRAWFRAME
---------------------------------------------------------------------------
see EMU_DRAWFRAME, start drawing |
---------------------------------------------------------------------------
| emu_vblank_out sets EMU_ENDFRAME */
/* ensure the emulation thread isn't still executing a previous frame */
if (emu->multi_threaded) {
/* tell the emulation thread to run the next frame */
{
mutex_lock(emu->run_mutex);
/* build the debug menus before running the frame, while the two threads
are synchronized */
emu_debug_menu(emu);
emu->state = EMU_RUNFRAME;
cond_signal(emu->run_cond);
mutex_unlock(emu->run_mutex);
}
/* wait for the emulation thread to submit a context */
{
mutex_lock(emu->frame_mutex);
while (emu->state == EMU_RUNFRAME && !emu->pending_ctx) {
cond_wait(emu->frame_cond, emu->frame_mutex);
}
/* if a context was submitted before the vblank, convert it and upload
its textures to the render backend */
if (emu->pending_ctx) {
tr_convert_context(emu->r, emu, &emu_find_texture, emu->pending_ctx,
&emu->pending_rc);
emu->pending_ctx = NULL;
}
/* unblock the emulation thread */
mutex_unlock(emu->frame_mutex);
}
/* note, the emulation thread may still be running at this point, but the
latest context has been converted and is available in pending_rc */
} else {
emu_debug_menu(emu);
emu_run_frame(emu);
mutex_lock(emu->req_mutex);
mutex_unlock(emu->req_mutex);
}
/* render the latest context */
tr_render_context(emu->r, &emu->pending_rc);
/* build the debug menus before running the frame, while the two threads
are synchronized */
emu_debug_menu(emu);
/* request a frame to be ran */
if (emu->multi_threaded) {
mutex_lock(emu->req_mutex);
emu->state = EMU_RUNFRAME;
cond_signal(emu->req_cond);
mutex_unlock(emu->req_mutex);
} else {
emu_run_until_vblank(emu);
}
/* process any context submitted during the frame */
if (emu->multi_threaded) {
mutex_lock(emu->res_mutex);
while (emu->state == EMU_RUNFRAME && !emu->pending_ctx) {
cond_wait(emu->res_cond, emu->res_mutex);
}
}
if (emu->pending_ctx) {
emu->latest = EMU_DRAW_CONTEXT;
tr_convert_context(emu->r, emu, &emu_find_texture, emu->pending_ctx,
&emu->latest_rc);
emu->pending_ctx = NULL;
}
if (emu->multi_threaded) {
mutex_unlock(emu->res_mutex);
}
/* wait for vblank_in */
if (emu->multi_threaded) {
mutex_lock(emu->res_mutex);
while (emu->state == EMU_RUNFRAME) {
cond_wait(emu->res_cond, emu->res_mutex);
}
mutex_unlock(emu->res_mutex);
}
/* render the latest framebuffer or context */
if (emu->latest == EMU_DRAW_PIXELS) {
r_draw_pixels(emu->r, emu->latest_fb.pixels, 0, 0, emu->latest_fb.width,
emu->latest_fb.height);
} else if (emu->latest == EMU_DRAW_CONTEXT) {
tr_render_context(emu->r, &emu->latest_rc);
}
/* note, the emulation thread may still be running the code between vblank_in
and vblank_out at this point, but there's no need to wait for it */
}
int emu_load(struct emu *emu, const char *path) {
@ -706,18 +777,18 @@ void emu_vid_created(struct emu *emu, struct render_backend *r) {
void emu_destroy(struct emu *emu) {
/* shutdown the emulation thread */
if (emu->multi_threaded) {
mutex_lock(emu->run_mutex);
mutex_lock(emu->req_mutex);
emu->state = EMU_SHUTDOWN;
cond_signal(emu->run_cond);
mutex_unlock(emu->run_mutex);
cond_signal(emu->req_cond);
mutex_unlock(emu->req_mutex);
void *result;
thread_join(emu->run_thread, &result);
mutex_destroy(emu->run_mutex);
cond_destroy(emu->run_cond);
mutex_destroy(emu->frame_mutex);
cond_destroy(emu->frame_cond);
mutex_destroy(emu->req_mutex);
cond_destroy(emu->req_cond);
mutex_destroy(emu->res_mutex);
cond_destroy(emu->res_cond);
}
emu_stop_tracing(emu);
@ -735,6 +806,7 @@ struct emu *emu_create(struct host *host) {
emu->dc = dc_create();
emu->dc->userdata = emu;
emu->dc->push_audio = &emu_push_audio;
emu->dc->push_pixels = &emu_push_pixels;
emu->dc->start_render = &emu_start_render;
emu->dc->finish_render = &emu_finish_render;
emu->dc->vblank_in = &emu_vblank_in;
@ -751,10 +823,10 @@ struct emu *emu_create(struct host *host) {
if (emu->multi_threaded) {
emu->state = EMU_WAITING;
emu->run_mutex = mutex_create();
emu->run_cond = cond_create();
emu->frame_mutex = mutex_create();
emu->frame_cond = cond_create();
emu->req_mutex = mutex_create();
emu->req_cond = cond_create();
emu->res_mutex = mutex_create();
emu->res_cond = cond_create();
emu->run_thread = thread_create(&emu_run_thread, NULL, emu);
CHECK_NOTNULL(emu->run_thread);

View File

@ -23,12 +23,12 @@ void dc_vblank_out(struct dreamcast *dc) {
dc->vblank_out(dc->userdata);
}
void dc_vblank_in(struct dreamcast *dc) {
void dc_vblank_in(struct dreamcast *dc, int video_disabled) {
if (!dc->vblank_in) {
return;
}
dc->vblank_in(dc->userdata);
dc->vblank_in(dc->userdata, video_disabled);
}
void dc_finish_render(struct dreamcast *dc) {
@ -47,6 +47,14 @@ void dc_start_render(struct dreamcast *dc, struct ta_context *ctx) {
dc->start_render(dc->userdata, ctx);
}
void dc_push_pixels(struct dreamcast *dc, const uint8_t *data, int w, int h) {
if (!dc->push_pixels) {
return;
}
dc->push_pixels(dc->userdata, data, w, h);
}
void dc_push_audio(struct dreamcast *dc, const int16_t *data, int frames) {
if (!dc->push_audio) {
return;
@ -81,29 +89,63 @@ int dc_running(struct dreamcast *dc) {
return dc->running;
}
int dc_load(struct dreamcast *dc, const char *path) {
if (path) {
LOG_INFO("dc_load path=%s", path);
static int dc_load_bin(struct dreamcast *dc, const char *path) {
FILE *fp = fopen(path, "rb");
if (!fp) {
return 0;
}
struct disc *disc = disc_create(path);
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
fseek(fp, 0, SEEK_SET);
if (!disc) {
LOG_WARNING("dc_load_game failed");
return 0;
}
/* load to 0x0c010000 (area 3) which is where 1ST_READ.BIN is loaded to */
uint8_t *data = memory_translate(dc->memory, "system ram", 0x00010000);
int n = (int)fread(data, sizeof(uint8_t), size, fp);
fclose(fp);
gdrom_set_disc(dc->gdrom, disc);
} else {
LOG_INFO("dc_load no path supplied, loading bios");
if (n != size) {
LOG_WARNING("failed to read %s", path);
return 0;
}
/* boot to bios bootstrap */
sh4_reset(dc->sh4, 0x0c010000);
dc_resume(dc);
return 1;
}
static int dc_load_disc(struct dreamcast *dc, const char *path) {
struct disc *disc = disc_create(path);
if (!disc) {
return 0;
}
/* boot to bios bootstrap */
gdrom_set_disc(dc->gdrom, disc);
sh4_reset(dc->sh4, 0xa0000000);
dc_resume(dc);
return 1;
}
int dc_load(struct dreamcast *dc, const char *path) {
if (!path) {
LOG_INFO("dc_load no path supplied, loading bios");
/* boot to bios bootstrap */
sh4_reset(dc->sh4, 0xa0000000);
dc_resume(dc);
return 1;
}
LOG_INFO("dc_load path=%s", path);
return dc_load_disc(dc, path) || dc_load_bin(dc, path);
}
int dc_init(struct dreamcast *dc) {
if (dc->debugger && !debugger_init(dc->debugger)) {
LOG_WARNING("dc_init failed to initialize debugger");

View File

@ -133,9 +133,11 @@ struct device {
* machine
*/
typedef void (*push_audio_cb)(void *, const int16_t *, int);
typedef void (*push_pixels_cb)(void *, const uint8_t *, int, int);
typedef void (*start_render_cb)(void *, struct ta_context *);
typedef void (*finish_render_cb)(void *);
typedef void (*vblank_cb)(void *);
typedef void (*vblank_in_cb)(void *, int);
typedef void (*vblank_out_cb)(void *);
struct dreamcast {
int running;
@ -162,10 +164,11 @@ struct dreamcast {
/* client callbacks */
void *userdata;
push_audio_cb push_audio;
push_pixels_cb push_pixels;
start_render_cb start_render;
finish_render_cb finish_render;
vblank_cb vblank_in;
vblank_cb vblank_out;
vblank_in_cb vblank_in;
vblank_out_cb vblank_out;
};
struct dreamcast *dc_create();
@ -202,9 +205,10 @@ void dc_input(struct dreamcast *dc, int port, int button, int16_t value);
/* client functionality */
void dc_push_audio(struct dreamcast *dc, const int16_t *data, int frames);
void dc_push_pixels(struct dreamcast *dc, const uint8_t *data, int w, int h);
void dc_start_render(struct dreamcast *dc, struct ta_context *ctx);
void dc_finish_render(struct dreamcast *dc);
void dc_vblank_in(struct dreamcast *dc);
void dc_vblank_in(struct dreamcast *dc, int video_disabled);
void dc_vblank_out(struct dreamcast *dc);
#endif

View File

@ -22,7 +22,9 @@ static void holly_ch2_dma_stop(struct holly *hl) {
}
static void holly_ch2_dma(struct holly *hl) {
/* FIXME what are SB_LMMODE0 / SB_LMMODE1 */
/* the ta mmio handlers assume the 64-bit access path will be used */
CHECK(*hl->SB_LMMODE0 == 0 && *hl->SB_LMMODE1 == 0);
struct sh4_dtr dtr = {0};
dtr.channel = 2;
dtr.dir = SH4_DMA_TO_ADDR;

View File

@ -7,7 +7,194 @@
#include "guest/sh4/sh4.h"
#include "stats.h"
struct reg_cb pvr_cb[PVR_NUM_REGS];
static struct reg_cb pvr_cb[PVR_NUM_REGS];
/* the dreamcast has 8MB of vram, split into two 4MB banks, with two ways of
accessing it:
* 64-bit access path - each 4MB bank is interleaved every 32-bits, enabling
a 64-bit data bus to be populated from both banks in parallel
* 32-bit access path - each 4MB bank is accessed sequentially one after the
other
by default (when SB_LMMODE0/1=0) the ta will use the 64-bit access path for
poly and texture transfers. due to this being the default for the ta, our
internal vram layout matches the 64-bit access paths view, meaning 32-bit
accesses will have to be converted to an interleaved address */
static uint32_t VRAM64(uint32_t addr32) {
const uint32_t bank_size = 0x00400000;
uint32_t bank = addr32 & bank_size;
uint32_t offset = addr32 & (bank_size - 1);
return ((offset & ~0x3) << 1) | (bank >> 20) | (offset & 0x3);
}
/* on the real hardware, the CORE copies it's final accumulation buffer to a
framebuffer in texture memory, where the DVE then reads it from to produce
the actual video output
when emulating, this process is skipped, and the output is instead rendered
directly to the host's default framebuffer. this avoids several unnecessary
copies between the gpu and cpu, and is significantly faster
the downside to this approach being that it doesn't work for programs such
as the IP.BIN license screen code, which write directly to the framebuffer,
as that memory is never read from to produce video output
to support these direct writes to the framebuffer, the PVR code marks each
framebuffer during a STARTRENDER request by writing a cookie to its memory,
and then checks for this cookie during the vblank. if the cookie doesn't
exist, it's assumed that the framebuffer memory is dirty and the texture
memory is copied and passed to the client to render */
#define PVR_FB_COOKIE 0xdeadbeef
static int pvr_test_framebuffer(struct pvr *pvr, uint32_t addr) {
uint32_t data = *(uint32_t *)&pvr->video_ram[VRAM64(addr)];
return data != PVR_FB_COOKIE;
}
static void pvr_mark_framebuffer(struct pvr *pvr, uint32_t addr) {
/* don't mark framebuffers which are being used as textures */
if (addr & 0x01000000) {
return;
}
*(uint32_t *)&pvr->video_ram[VRAM64(addr)] = PVR_FB_COOKIE;
/* it's not enough to just mark the starting address of this framebuffer. next
frame, this framebuffer could be used as field 2, in which case FB_R_SOF2
would be set to addr + line_size + line_mod */
const uint32_t line_width[] = {320, 640};
const uint32_t line_bpp[] = {2, 3, 4};
const uint32_t line_scale[] = {1, 2};
for (int i = 0; i < ARRAY_SIZE(line_width); i++) {
for (int j = 0; j < ARRAY_SIZE(line_bpp); j++) {
for (int k = 0; k < ARRAY_SIZE(line_scale); k++) {
uint32_t next_line = addr + line_width[i] * line_bpp[j] * line_scale[k];
*(uint32_t *)&pvr->video_ram[VRAM64(next_line)] = PVR_FB_COOKIE;
}
}
}
}
static int pvr_update_framebuffer(struct pvr *pvr) {
uint32_t fields[2] = {*pvr->FB_R_SOF1, *pvr->FB_R_SOF2};
int num_fields = pvr->SPG_CONTROL->interlace ? 2 : 1;
int field = pvr->SPG_STATUS->fieldnum;
if (!pvr->FB_R_CTRL->fb_enable) {
return 0;
}
/* don't do anything if the framebuffer hasn't been written to */
if (!pvr_test_framebuffer(pvr, fields[field])) {
return 0;
}
/* values in FB_R_SIZE are in 32-bit units */
int line_mod = (pvr->FB_R_SIZE->mod << 2) - 4;
int x_size = (pvr->FB_R_SIZE->x + 1) << 2;
int y_size = (pvr->FB_R_SIZE->y + 1);
pvr->framebuffer_w = pvr->FB_R_SIZE->x + 1;
pvr->framebuffer_h = pvr->FB_R_SIZE->y + 1;
/* final fb will be 2x height when interlacing */
if (pvr->SPG_CONTROL->interlace) {
pvr->framebuffer_h *= 2;
}
/* convert framebuffer into a 24-bit RGB pixel buffer */
uint8_t *dst = pvr->framebuffer;
uint8_t *src = pvr->video_ram;
switch (pvr->FB_R_CTRL->fb_depth) {
case 0:
case 1: {
/* FB_R_SIZE specifies x in 32-bit units, if the framebuffer is using a
16-bit format this needs to be doubled */
pvr->framebuffer_w *= 2;
for (int y = 0; y < y_size; y++) {
for (int n = 0; n < num_fields; n++) {
for (int x = 0; x < x_size; x += 2) {
uint16_t rgb = *(uint16_t *)&src[VRAM64(fields[n])];
dst[0] = (rgb & 0b1111100000000000) >> 8;
dst[1] = (rgb & 0b0000011111100000) >> 3;
dst[2] = (rgb & 0b0000000000011111) << 3;
fields[n] += 2;
dst += 3;
}
fields[n] += line_mod;
}
}
} break;
case 2: {
for (int y = 0; y < y_size; y++) {
for (int n = 0; n < num_fields; n++) {
for (int x = 0; x < x_size; x += 3) {
uint8_t *rgb = &src[VRAM64(fields[n])];
dst[0] = rgb[2];
dst[1] = rgb[1];
dst[2] = rgb[0];
fields[n] += 3;
dst += 3;
}
fields[n] += line_mod;
}
}
} break;
case 3: {
for (int y = 0; y < y_size; y++) {
for (int n = 0; n < num_fields; n++) {
for (int x = 0; x < x_size; x += 4) {
uint8_t *krgb = &src[VRAM64(fields[n])];
dst[0] = krgb[2];
dst[1] = krgb[1];
dst[2] = krgb[0];
fields[n] += 4;
dst += 3;
}
fields[n] += line_mod;
}
}
} break;
default:
LOG_FATAL("pvr_push_framebuffer unexpected fb_depth %d",
pvr->FB_R_CTRL->fb_depth);
break;
}
dc_push_pixels(pvr->dc, pvr->framebuffer, pvr->framebuffer_w,
pvr->framebuffer_h);
return 1;
}
static void pvr_vblank_out(struct pvr *pvr) {
dc_vblank_out(pvr->dc);
}
static void pvr_vblank_in(struct pvr *pvr) {
prof_counter_add(COUNTER_pvr_vblanks, 1);
/* if STARTRENDER wasn't written to this frame, check to see if the
framebuffer was written to directly */
if (!pvr->got_startrender) {
pvr_update_framebuffer(pvr);
} else {
pvr->got_startrender = 0;
}
/* flip field */
if (pvr->SPG_CONTROL->interlace) {
pvr->SPG_STATUS->fieldnum = !pvr->SPG_STATUS->fieldnum;
} else {
pvr->SPG_STATUS->fieldnum = 0;
}
dc_vblank_in(pvr->dc, pvr->VO_CONTROL->blank_video);
}
static void pvr_next_scanline(void *data) {
struct pvr *pvr = data;
@ -50,12 +237,10 @@ static void pvr_next_scanline(void *data) {
}
pvr->SPG_STATUS->scanline = pvr->current_line;
/* FIXME toggle SPG_STATUS.fieldnum on vblank? */
if (!was_vsync && pvr->SPG_STATUS->vsync) {
prof_counter_add(COUNTER_pvr_vblanks, 1);
dc_vblank_in(pvr->dc);
pvr_vblank_in(pvr);
} else if (was_vsync && !pvr->SPG_STATUS->vsync) {
dc_vblank_out(pvr->dc);
pvr_vblank_out(pvr);
}
/* reschedule */
@ -140,83 +325,62 @@ static void pvr_palette_write(struct pvr *pvr, uint32_t addr, uint32_t data,
WRITE_DATA(&pvr->palette_ram[addr]);
}
static uint32_t MAP64(uint32_t addr) {
/* the dreamcast has 8MB of vram, split into two 4MB banks, with two ways of
accessing it:
0x04000000 -> 0x047fffff, 32-bit sequential access
0x05000000 -> 0x057fffff, 64-bit interleaved access
in 64-bit interleaved mode, the addresses map like so:
0x05000000 = 0x0400000
0x05400000 = 0x0400004
0x05400002 = 0x0400006
0x05000004 = 0x0400008
0x05000006 = 0x040000a
0x05400004 = 0x040000c
0x05000008 = 0x0400010
0x05400008 = 0x0400014
0x0500000c = 0x0400018
0x0540000c = 0x040001c */
return (((addr & 0x003ffffc) << 1) + ((addr & 0x00400000) >> 20) +
(addr & 0x3));
}
static uint32_t pvr_vram_read(struct pvr *pvr, uint32_t addr,
uint32_t data_mask) {
static uint32_t pvr_vram64_read(struct pvr *pvr, uint32_t addr,
uint32_t data_mask) {
/* note, the video ram can't be directly accessed through fastmem, or texture
cache invalidations will break. this is because textures cache entries
only watch the physical video ram address, not all of its mirrors */
return READ_DATA(&pvr->video_ram[addr]);
}
static void pvr_vram_write(struct pvr *pvr, uint32_t addr, uint32_t data,
uint32_t data_mask) {
static void pvr_vram64_write(struct pvr *pvr, uint32_t addr, uint32_t data,
uint32_t data_mask) {
WRITE_DATA(&pvr->video_ram[addr]);
}
static void pvr_vram_read_string(struct pvr *pvr, void *ptr, uint32_t src,
int size) {
static void pvr_vram64_read_string(struct pvr *pvr, void *ptr, uint32_t src,
int size) {
memcpy(ptr, &pvr->video_ram[src], size);
}
static void pvr_vram_write_string(struct pvr *pvr, uint32_t dst, void *ptr,
int size) {
static void pvr_vram64_write_string(struct pvr *pvr, uint32_t dst, void *ptr,
int size) {
memcpy(&pvr->video_ram[dst], ptr, size);
}
static uint32_t pvr_vram_interleaved_read(struct pvr *pvr, uint32_t addr,
uint32_t data_mask) {
addr = MAP64(addr);
static uint32_t pvr_vram32_read(struct pvr *pvr, uint32_t addr,
uint32_t data_mask) {
addr = VRAM64(addr);
return READ_DATA(&pvr->video_ram[addr]);
}
static void pvr_vram_interleaved_write(struct pvr *pvr, uint32_t addr,
uint32_t data, uint32_t data_mask) {
addr = MAP64(addr);
static void pvr_vram32_write(struct pvr *pvr, uint32_t addr, uint32_t data,
uint32_t data_mask) {
addr = VRAM64(addr);
WRITE_DATA(&pvr->video_ram[addr]);
}
static void pvr_vram_interleaved_read_string(struct pvr *pvr, void *ptr,
uint32_t src, int size) {
static void pvr_vram32_read_string(struct pvr *pvr, void *ptr, uint32_t src,
int size) {
CHECK(size % 4 == 0);
uint8_t *dst = ptr;
uint8_t *end = dst + size;
while (dst < end) {
*(uint32_t *)dst = *(uint32_t *)&pvr->video_ram[MAP64(src)];
*(uint32_t *)dst = *(uint32_t *)&pvr->video_ram[VRAM64(src)];
dst += 4;
src += 4;
}
}
static void pvr_vram_interleaved_write_string(struct pvr *pvr, uint32_t dst,
void *ptr, int size) {
static void pvr_vram32_write_string(struct pvr *pvr, uint32_t dst, void *ptr,
int size) {
CHECK(size % 4 == 0);
uint8_t *src = ptr;
uint8_t *end = src + size;
while (src < end) {
*(uint32_t *)&pvr->video_ram[MAP64(dst)] = *(uint32_t *)src;
*(uint32_t *)&pvr->video_ram[VRAM64(dst)] = *(uint32_t *)src;
dst += 4;
src += 4;
}
@ -242,6 +406,40 @@ static int pvr_init(struct device *dev) {
return 1;
}
void pvr_video_size(struct pvr *pvr, int *video_width, int *video_height) {
int vga_mode = !pvr->SPG_CONTROL->NTSC && !pvr->SPG_CONTROL->PAL &&
!pvr->SPG_CONTROL->interlace;
if (vga_mode) {
*video_width = 640;
*video_height = 480;
} else {
*video_width = 640;
*video_height = 240;
}
if (pvr->VO_CONTROL->pixel_double) {
*video_width /= 2;
}
if (pvr->SPG_CONTROL->interlace) {
*video_height *= 2;
}
/* scale_x signals to scale the framebuffer down by half. do so by scaling
up the width used by the projection matrix */
if (pvr->SCALER_CTL->scale_x) {
*video_width *= 2;
}
/* scale_y is a fixed-point scaler, with 6-bits in the integer and 10-bits
in the decimal. this scale value is ignored when used for interlacing
which is not emulated */
if (!pvr->SCALER_CTL->interlace) {
*video_height = (*video_height * pvr->SCALER_CTL->scale_y) >> 10;
}
}
void pvr_destroy(struct pvr *pvr) {
dc_destroy_device((struct device *)pvr);
}
@ -253,6 +451,49 @@ struct pvr *pvr_create(struct dreamcast *dc) {
return pvr;
}
REG_W32(pvr_cb, SOFTRESET) {
struct pvr *pvr = dc->pvr;
if (!(value & 0x1)) {
return;
}
ta_soft_reset(pvr->ta);
}
REG_W32(pvr_cb, STARTRENDER) {
struct pvr *pvr = dc->pvr;
if (!value) {
return;
}
ta_start_render(pvr->ta);
pvr_mark_framebuffer(pvr, *pvr->FB_W_SOF1);
pvr_mark_framebuffer(pvr, *pvr->FB_W_SOF2);
pvr->got_startrender = 1;
}
REG_W32(pvr_cb, TA_LIST_INIT) {
struct pvr *pvr = dc->pvr;
if (!(value & 0x80000000)) {
return;
}
ta_list_init(pvr->ta);
}
REG_W32(pvr_cb, TA_LIST_CONT) {
struct pvr *pvr = dc->pvr;
if (!(value & 0x80000000)) {
return;
}
ta_list_cont(pvr->ta);
}
REG_W32(pvr_cb, TA_YUV_TEX_BASE) {
struct pvr *pvr = dc->pvr;
pvr->TA_YUV_TEX_BASE->full = value;
ta_yuv_init(pvr->ta);
}
REG_W32(pvr_cb, SPG_LOAD) {
struct pvr *pvr = dc->pvr;
pvr->SPG_LOAD->full = value;
@ -279,15 +520,15 @@ AM_END();
AM_BEGIN(struct pvr, pvr_vram_map);
AM_RANGE(0x00000000, 0x007fffff) AM_MOUNT("video ram")
AM_RANGE(0x00000000, 0x007fffff) AM_HANDLE("video ram sequential",
(mmio_read_cb)&pvr_vram_read,
(mmio_write_cb)&pvr_vram_write,
(mmio_read_string_cb)&pvr_vram_read_string,
(mmio_write_string_cb)&pvr_vram_write_string)
AM_RANGE(0x01000000, 0x017fffff) AM_HANDLE("video ram interleaved",
(mmio_read_cb)&pvr_vram_interleaved_read,
(mmio_write_cb)&pvr_vram_interleaved_write,
(mmio_read_string_cb)&pvr_vram_interleaved_read_string,
(mmio_write_string_cb)&pvr_vram_interleaved_write_string)
AM_RANGE(0x00000000, 0x007fffff) AM_HANDLE("video ram 64",
(mmio_read_cb)&pvr_vram64_read,
(mmio_write_cb)&pvr_vram64_write,
(mmio_read_string_cb)&pvr_vram64_read_string,
(mmio_write_string_cb)&pvr_vram64_write_string)
AM_RANGE(0x01000000, 0x017fffff) AM_HANDLE("video ram 32",
(mmio_read_cb)&pvr_vram32_read,
(mmio_write_cb)&pvr_vram32_write,
(mmio_read_string_cb)&pvr_vram32_read_string,
(mmio_write_string_cb)&pvr_vram32_write_string)
AM_END();
/* clang-format on */

View File

@ -9,6 +9,8 @@ struct dreamcast;
struct holly;
struct timer;
#define PVR_FRAMEBUFFER_SIZE 640 * 640 * 4
struct pvr {
struct device;
uint8_t *palette_ram;
@ -20,6 +22,14 @@ struct pvr {
int line_clock;
uint32_t current_line;
/* copy of deinterlaced framebuffer from texture memory */
uint8_t framebuffer[PVR_FRAMEBUFFER_SIZE];
int framebuffer_w;
int framebuffer_h;
/* tracks if a STARTRENDER was received for the current frame */
int got_startrender;
#define PVR_REG(offset, name, default, type) type *name;
#include "guest/pvr/pvr_regs.inc"
#undef PVR_REG
@ -28,9 +38,9 @@ struct pvr {
AM_DECLARE(pvr_reg_map);
AM_DECLARE(pvr_vram_map);
extern struct reg_cb pvr_cb[PVR_NUM_REGS];
struct pvr *pvr_create(struct dreamcast *dc);
void pvr_destroy(struct pvr *pvr);
void pvr_video_size(struct pvr *pvr, int *video_width, int *video_height);
#endif

View File

@ -312,10 +312,6 @@ static int ta_param_size_raw(union pcw pcw, int vert_type) {
}
}
static void ta_soft_reset(struct ta *ta) {
/* FIXME what are we supposed to do here? */
}
/*
* ta parameter handling
*
@ -498,37 +494,7 @@ static void ta_save_state(struct ta *ta, struct ta_context *ctx) {
ctx->palette_fmt = pvr->PAL_RAM_CTRL->pixel_fmt;
/* save video resolution in order to unproject the screen space coordinates */
int vga_mode = !pvr->SPG_CONTROL->NTSC && !pvr->SPG_CONTROL->PAL &&
!pvr->SPG_CONTROL->interlace;
if (vga_mode) {
ctx->video_width = 640;
ctx->video_height = 480;
} else {
ctx->video_width = 320;
ctx->video_height = 240;
}
if (pvr->SPG_CONTROL->interlace) {
ctx->video_height *= 2;
}
if (!pvr->VO_CONTROL->pixel_double) {
ctx->video_width *= 2;
}
/* scale_x signals to scale the framebuffer down by half. do so by scaling
up the width used by the projection matrix */
if (pvr->SCALER_CTL->scale_x) {
ctx->video_width *= 2;
}
/* scale_y is a fixed-point scaler, with 6-bits in the integer and 10-bits
in the decimal. this scale value is ignored when used for interlacing
which is not emulated */
if (!pvr->SCALER_CTL->interlace) {
ctx->video_height = (ctx->video_height * pvr->SCALER_CTL->scale_y) >> 10;
}
pvr_video_size(pvr, &ctx->video_width, &ctx->video_height);
/* according to the hardware docs, this is the correct calculation of the
background ISP address. however, in practice, the second TA buffer's ISP
@ -578,7 +544,7 @@ static void ta_save_state(struct ta *ta, struct ta_context *ctx) {
}
}
static void ta_finish_render(void *data) {
static void ta_render_context_end(void *data) {
struct ta_context *ctx = data;
struct ta *ta = ctx->userdata;
@ -594,7 +560,7 @@ static void ta_finish_render(void *data) {
holly_raise_interrupt(ta->holly, HOLLY_INT_PCEOTINT);
}
static void ta_start_render(struct ta *ta, struct ta_context *ctx) {
static void ta_render_context(struct ta *ta, struct ta_context *ctx) {
prof_counter_add(COUNTER_ta_renders, 1);
/* remove context from pool */
@ -611,7 +577,7 @@ static void ta_start_render(struct ta *ta, struct ta_context *ctx) {
TODO figure out a heuristic involving the number of polygons rendered */
int64_t end = INT64_C(10000000);
ctx->userdata = ta;
scheduler_start_timer(ta->scheduler, &ta_finish_render, ctx, end);
scheduler_start_timer(ta->scheduler, &ta_render_context_end, ctx, end);
}
/*
@ -622,7 +588,7 @@ static void ta_start_render(struct ta *ta, struct ta_context *ctx) {
#define TA_MAX_MACROBLOCK_SIZE \
MAX(TA_YUV420_MACROBLOCK_SIZE, TA_YUV422_MACROBLOCK_SIZE)
static void ta_yuv_init(struct ta *ta) {
static void ta_yuv_reset(struct ta *ta) {
struct pvr *pvr = ta->pvr;
/* FIXME only YUV420 -> YUV422 supported for now */
@ -711,7 +677,7 @@ static void ta_yuv_process_macroblock(struct ta *ta, void *data) {
pvr->TA_YUV_TEX_CNT->num++;
if ((int)pvr->TA_YUV_TEX_CNT->num >= ta->yuv_macroblock_count) {
ta_yuv_init(ta);
ta_yuv_reset(ta);
/* raise DMA end interrupt */
holly_raise_interrupt(ta->holly, HOLLY_INT_TAYUVINT);
@ -824,6 +790,36 @@ void ta_texture_info(struct ta *ta, union tsp tsp, union tcw tcw,
*palette = *palette_size ? &ta->pvr->palette_ram[palette_addr] : NULL;
}
void ta_yuv_init(struct ta *ta) {
ta_yuv_reset(ta);
}
void ta_list_cont(struct ta *ta) {
struct ta_context *ctx =
ta_get_context(ta, ta->pvr->TA_ISP_BASE->base_address);
CHECK_NOTNULL(ctx);
ta_cont_context(ta, ctx);
ta->curr_context = ctx;
}
void ta_list_init(struct ta *ta) {
struct ta_context *ctx =
ta_demand_context(ta, ta->pvr->TA_ISP_BASE->base_address);
ta_init_context(ta, ctx);
ta->curr_context = ctx;
}
void ta_start_render(struct ta *ta) {
struct ta_context *ctx =
ta_get_context(ta, ta->pvr->PARAM_BASE->base_address);
CHECK_NOTNULL(ctx);
ta_render_context(ta, ctx);
}
void ta_soft_reset(struct ta *ta) {
/* FIXME what are we supposed to do here? */
}
void ta_destroy(struct ta *ta) {
dc_destroy_device((struct device *)ta);
}
@ -836,68 +832,6 @@ struct ta *ta_create(struct dreamcast *dc) {
return ta;
}
/*
* ta mmio registers
*/
REG_W32(pvr_cb, SOFTRESET) {
struct ta *ta = dc->ta;
if (!(value & 0x1)) {
return;
}
ta_soft_reset(ta);
}
REG_W32(pvr_cb, STARTRENDER) {
struct ta *ta = dc->ta;
if (!value) {
return;
}
struct ta_context *ctx =
ta_get_context(ta, ta->pvr->PARAM_BASE->base_address);
CHECK_NOTNULL(ctx);
ta_start_render(ta, ctx);
}
REG_W32(pvr_cb, TA_LIST_INIT) {
struct ta *ta = dc->ta;
if (!(value & 0x80000000)) {
return;
}
struct ta_context *ctx =
ta_demand_context(ta, ta->pvr->TA_ISP_BASE->base_address);
ta_init_context(ta, ctx);
ta->curr_context = ctx;
}
REG_W32(pvr_cb, TA_LIST_CONT) {
struct ta *ta = dc->ta;
if (!(value & 0x80000000)) {
return;
}
struct ta_context *ctx =
ta_get_context(ta, ta->pvr->TA_ISP_BASE->base_address);
CHECK_NOTNULL(ctx);
ta_cont_context(ta, ctx);
ta->curr_context = ctx;
}
REG_W32(pvr_cb, TA_YUV_TEX_BASE) {
struct ta *ta = dc->ta;
struct pvr *pvr = dc->pvr;
pvr->TA_YUV_TEX_BASE->full = value;
ta_yuv_init(ta);
}
/* clang-format off */
AM_BEGIN(struct ta, ta_data_map);
AM_RANGE(0x00000000, 0x007fffff) AM_HANDLE("ta poly fifo",

View File

@ -13,6 +13,11 @@ AM_DECLARE(ta_data_map);
struct ta *ta_create(struct dreamcast *dc);
void ta_destroy(struct ta *ta);
void ta_soft_reset(struct ta *ta);
void ta_start_render(struct ta *ta);
void ta_list_init(struct ta *ta);
void ta_list_cont(struct ta *ta);
void ta_yuv_init(struct ta *ta);
void ta_texture_info(struct ta *ta, union tsp tsp, union tcw tcw,
const uint8_t **texture, int *texture_size,
const uint8_t **palette, int *palette_size);

View File

@ -52,17 +52,26 @@ struct texture {
GLuint texture;
};
struct viewport {
int x, y, w, h;
};
struct render_backend {
struct host *host;
int width, height;
int viewport_width;
int viewport_height;
/* current viewport */
struct viewport viewport;
/* default assets created during intitialization */
GLuint white_texture;
struct shader_program ta_programs[ATTR_COUNT];
struct shader_program ui_program;
/* offscreen framebuffer for blitting raw pixels */
GLuint pixel_fbo;
GLuint pixel_texture;
/* texture cache */
struct texture textures[MAX_TEXTURES];
@ -132,6 +141,24 @@ static GLenum prim_types[] = {
GL_LINES, /* PRIM_LINES */
};
static GLuint internal_formats[] = {
GL_NONE, /* PXL_INVALID */
GL_RGB, /* PXL_RGB */
GL_RGBA, /* PXL_RGBA */
GL_RGBA, /* PXL_RGBA5551 */
GL_RGB, /* PXL_RGB565 */
GL_RGBA, /* PXL_RGBA4444 */
};
static GLuint pixel_formats[] = {
GL_NONE, /* PXL_INVALID */
GL_UNSIGNED_BYTE, /* PXL_RGB */
GL_UNSIGNED_BYTE, /* PXL_RGBA */
GL_UNSIGNED_SHORT_5_5_5_1, /* PXL_RGBA5551 */
GL_UNSIGNED_SHORT_5_6_5, /* PXL_RGB565 */
GL_UNSIGNED_SHORT_4_4_4_4, /* PXL_RGBA4444 */
};
static inline void r_bind_texture(struct render_backend *r,
enum texture_map map, GLuint tex) {
glActiveTexture(GL_TEXTURE0 + map);
@ -270,6 +297,9 @@ static void r_create_shaders(struct render_backend *r) {
static void r_destroy_textures(struct render_backend *r) {
glDeleteTextures(1, &r->white_texture);
glDeleteFramebuffers(1, &r->pixel_fbo);
glDeleteTextures(1, &r->pixel_texture);
for (int i = 0; i < MAX_TEXTURES; i++) {
struct texture *tex = &r->textures[i];
@ -293,6 +323,31 @@ static void r_create_textures(struct render_backend *r) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE,
pixels);
glBindTexture(GL_TEXTURE_2D, 0);
/* create fbo for blitting raw framebuffers to */
glGenFramebuffers(1, &r->pixel_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, r->pixel_fbo);
glGenTextures(1, &r->pixel_texture);
glBindTexture(GL_TEXTURE_2D, r->pixel_texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 1, 1, 0, GL_RGB,
GL_UNSIGNED_SHORT_5_6_5, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
GLenum buffers[] = {GL_COLOR_ATTACHMENT0};
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, r->pixel_texture,
0);
glDrawBuffers(ARRAY_SIZE(buffers), buffers);
GLenum res = glCheckFramebufferStatus(GL_FRAMEBUFFER);
CHECK_EQ(res, GL_FRAMEBUFFER_COMPLETE);
glBindTexture(GL_TEXTURE_2D, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
static void r_destroy_vertex_arrays(struct render_backend *r) {
@ -495,13 +550,13 @@ void r_begin_ui_surfaces(struct render_backend *r,
const uint16_t *indices, int num_indices) {
/* setup projection matrix */
float ortho[16];
ortho[0] = 2.0f / (float)r->viewport_width;
ortho[0] = 2.0f / (float)r->viewport.w;
ortho[4] = 0.0f;
ortho[8] = 0.0f;
ortho[12] = -1.0f;
ortho[1] = 0.0f;
ortho[5] = -2.0f / (float)r->viewport_height;
ortho[5] = -2.0f / (float)r->viewport.h;
ortho[9] = 0.0f;
ortho[13] = 1.0f;
@ -611,11 +666,29 @@ void r_begin_ta_surfaces(struct render_backend *r, int video_width,
GL_DYNAMIC_DRAW);
}
void r_viewport(struct render_backend *r, int x, int y, int width, int height) {
r->viewport_width = width;
r->viewport_height = height;
void r_draw_pixels(struct render_backend *r, const uint8_t *pixels, int x,
int y, int width, int height) {
glBindTexture(GL_TEXTURE_2D, r->pixel_texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, pixels);
glBindTexture(GL_TEXTURE_2D, 0);
glViewport(x, y, r->viewport_width, r->viewport_height);
glBindFramebuffer(GL_READ_FRAMEBUFFER, r->pixel_fbo);
glReadBuffer(GL_COLOR_ATTACHMENT0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(x, y + height, x + width, y, r->viewport.x, r->viewport.y,
r->viewport.x + r->viewport.w,
r->viewport.y + r->viewport.h, GL_COLOR_BUFFER_BIT,
GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void r_viewport(struct render_backend *r, int x, int y, int width, int height) {
r->viewport.x = x;
r->viewport.y = y;
r->viewport.w = width;
r->viewport.h = height;
glViewport(r->viewport.x, r->viewport.y, r->viewport.w, r->viewport.h);
}
void r_clear(struct render_backend *r) {
@ -650,29 +723,8 @@ texture_handle_t r_create_texture(struct render_backend *r,
}
CHECK_LT(handle, MAX_TEXTURES);
GLuint internal_fmt;
GLuint pixel_fmt;
switch (format) {
case PXL_RGBA:
internal_fmt = GL_RGBA;
pixel_fmt = GL_UNSIGNED_BYTE;
break;
case PXL_RGBA5551:
internal_fmt = GL_RGBA;
pixel_fmt = GL_UNSIGNED_SHORT_5_5_5_1;
break;
case PXL_RGB565:
internal_fmt = GL_RGB;
pixel_fmt = GL_UNSIGNED_SHORT_5_6_5;
break;
case PXL_RGBA4444:
internal_fmt = GL_RGBA;
pixel_fmt = GL_UNSIGNED_SHORT_4_4_4_4;
break;
default:
LOG_FATAL("unexpected pixel format %d", format);
break;
}
GLuint internal_fmt = internal_formats[format];
GLuint pixel_fmt = pixel_formats[format];
struct texture *tex = &r->textures[handle];
glGenTextures(1, &tex->texture);

View File

@ -9,6 +9,7 @@ typedef unsigned texture_handle_t;
enum pxl_format {
PXL_INVALID,
PXL_RGB,
PXL_RGBA,
PXL_RGBA5551,
PXL_RGB565,
@ -139,6 +140,9 @@ void r_destroy_texture(struct render_backend *r, texture_handle_t handle);
void r_clear(struct render_backend *r);
void r_viewport(struct render_backend *r, int x, int y, int width, int height);
void r_draw_pixels(struct render_backend *r, const uint8_t *pixels, int x,
int y, int width, int height);
void r_begin_ta_surfaces(struct render_backend *r, int video_width,
int video_height, const struct ta_vertex *verts,
int num_verts, const uint16_t *indices,