Skip to content

Commit

Permalink
rg_display: New partial update system
Browse files Browse the repository at this point in the history
The previous partial update system worked as follows:

1. Compare the previous and current framebuffers line by line
2. For each line find the first different pixel and the last one
3. Do a second pass and merge line differences into rectangles
4. Adjust the rectangles until they're no longer on boundaries that would break scaling or filtering
5. Prepare/render the region within a rectangle (do pixel conversion, scaling, filtering, etc)
5. Send the block to the SPI display

This worked reasonably well but:

- Step 4 was always a problem and we often ended up with artifacts despite many attempts at fixing it
- Mid-frame palette changes were completely broken
- The need for two framebuffers is an issue in some emulators
- It was fairly slow. Better than a full redraw, but still slow

The new system works as follows:

1. Prepare/render a few lines to be sent to the display, as if we were doing a full update
2. Do a checksum of the block and compare it to the checksum of the same block from the previous frame
3. If it matches, drop the block and advance the window. Otherwise store the new checksum and send the SPI buffer

This has many advantages:

- We don't have to care about boundaries since they're always the same
- In many cases it's faster than doing a diff, even though we have to render the full lines
- It can work with a single framebuffer
- It uses less memory
- We can invalidate lines that rg_display_write writes to, avoiding accidental leftovers

The main drawback is that it's currently less granular. If a single pixel changes in a block, we have to
send it fully to the display. Whereas previously we narrowed down the diff to a smaller region.

But I intend to experiment with block sizes and shapes, there's no reason we couldn't split the frame
into smaller blocks. It was just more convenient to start with the current block size (4 full lines).
  • Loading branch information
ducalex committed Feb 15, 2024
1 parent 748db71 commit 4396bb7
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 321 deletions.
360 changes: 116 additions & 244 deletions components/retro-go/rg_display.c

Large diffs are not rendered by default.

26 changes: 6 additions & 20 deletions components/retro-go/rg_display.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@
#include <stdbool.h>
#include <stdint.h>

typedef enum
{
RG_UPDATE_EMPTY = 0,
RG_UPDATE_FULL,
RG_UPDATE_PARTIAL,
RG_UPDATE_ERROR,
} rg_update_t;

typedef enum
{
RG_DISPLAY_UPDATE_PARTIAL = 0,
Expand Down Expand Up @@ -88,8 +80,10 @@ typedef struct
typedef struct
{
int32_t totalFrames;
int32_t partFrames;
int32_t fullFrames;
int64_t busyTime; // This is only time spent blocking the main task
int64_t busyTime;
bool lastFullFrame;
} rg_display_counters_t;

typedef struct
Expand Down Expand Up @@ -126,23 +120,15 @@ typedef struct
int crop_h;
int crop_v;
int format;
bool ready;
} source;
bool changed;
} rg_display_t;

typedef struct
{
short left; // int32_t left:10;
short width; // int32_t width:10;
short repeat; // int32_t repeat:10;
} rg_line_diff_t;

typedef struct
{
rg_update_t type;
void *buffer; // Should be at least height*stride bytes. expects uint8_t * | uint16_t *
uint16_t palette[256]; // Used in RG_PIXEL_PAL is set
rg_line_diff_t diff[256];
} rg_video_update_t;

void rg_display_init(void);
Expand All @@ -154,9 +140,9 @@ bool rg_display_sync(bool block);
void rg_display_force_redraw(void);
bool rg_display_save_frame(const char *filename, const rg_video_update_t *frame, int width, int height);
void rg_display_set_source_format(int width, int height, int crop_h, int crop_v, int stride, rg_pixel_flags_t format);
rg_update_t rg_display_submit(/*const*/ rg_video_update_t *update, const rg_video_update_t *previousUpdate);
void rg_display_submit(const rg_video_update_t *update, uint32_t flags);

rg_display_counters_t rg_display_get_counters(void);
const rg_display_counters_t *rg_display_get_counters(void);
const rg_display_config_t *rg_display_get_config(void);
const rg_display_t *rg_display_get_info(void);

Expand Down
7 changes: 2 additions & 5 deletions components/retro-go/rg_system.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ static void update_statistics(void)
static counters_t counters = {0};
const counters_t previous = counters;

rg_display_counters_t display = rg_display_get_counters();
// rg_audio_counters_t audio = rg_audio_get_counters();
rg_display_counters_t display = *rg_display_get_counters();
// rg_audio_counters_t audio = *rg_audio_get_counters();

counters.totalFrames = display.totalFrames;
counters.fullFrames = display.fullFrames;
Expand Down Expand Up @@ -685,8 +685,6 @@ bool rg_emu_load_state(uint8_t slot)
WDT_RELOAD(WDT_TIMEOUT);
free(filename);

rg_display_force_redraw();

return success;
}

Expand Down Expand Up @@ -750,7 +748,6 @@ bool rg_emu_save_state(uint8_t slot)

rg_storage_commit();
rg_system_set_led(0);
rg_display_force_redraw();

return success;
}
Expand Down
59 changes: 58 additions & 1 deletion components/retro-go/rg_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const char *rg_relpath(const char *path)
return path;
}

uint32_t rg_crc32(uint32_t crc, const uint8_t *buf, uint32_t len)
uint32_t rg_crc32(uint32_t crc, const uint8_t *buf, size_t len)
{
#ifdef ESP_PLATFORM
// This is part of the ROM but finding the correct header is annoying as it differs per SOC...
Expand All @@ -102,6 +102,63 @@ uint32_t rg_crc32(uint32_t crc, const uint8_t *buf, uint32_t len)
#endif
}

/**
* This function is the SuperFastHash from:
* http://www.azillionmonkeys.com/qed/hash.html
*/
uint32_t rg_hash(const char *data, size_t len)
{
#define get16bits(d) (*((const uint16_t *)(d)))

if (len <= 0 || data == NULL)
return 0;

uint32_t hash = len, tmp;
int rem = len & 3;
len >>= 2;

/* Main loop */
for (; len > 0; len--)
{
hash += get16bits(data);
tmp = (get16bits(data + 2) << 11) ^ hash;
hash = (hash << 16) ^ tmp;
data += 2 * sizeof(uint16_t);
hash += hash >> 11;
}

/* Handle end cases */
switch (rem)
{
case 3:
hash += get16bits(data);
hash ^= hash << 16;
hash ^= ((signed char)data[sizeof(uint16_t)]) << 18;
hash += hash >> 11;
break;
case 2:
hash += get16bits(data);
hash ^= hash << 11;
hash += hash >> 17;
break;
case 1:
hash += (signed char)*data;
hash ^= hash << 10;
hash += hash >> 1;
}

/* Force "avalanching" of final 127 bits */
hash ^= hash << 3;
hash += hash >> 5;
hash ^= hash << 4;
hash += hash >> 17;
hash ^= hash << 25;
hash += hash >> 6;

#undef get16bits
return hash;
}

const char *const_string(const char *str)
{
static const char **strings = NULL;
Expand Down
3 changes: 2 additions & 1 deletion components/retro-go/rg_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ const char *rg_basename(const char *path);
const char *rg_extension(const char *path);
const char *rg_relpath(const char *path);
const char *const_string(const char *str);
uint32_t rg_crc32(uint32_t crc, const uint8_t *buf, uint32_t len);
uint32_t rg_crc32(uint32_t crc, const uint8_t *buf, size_t len);
uint32_t rg_hash(const char *buf, size_t len);
void *rg_alloc(size_t size, uint32_t caps);
void rg_usleep(uint32_t us);

Expand Down
6 changes: 2 additions & 4 deletions gwenesis/main/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ static void event_handler(int event, void *arg)
{
if (event == RG_EVENT_REDRAW)
{
rg_display_submit(currentUpdate, NULL);
rg_display_submit(currentUpdate, 0);
}
}

Expand Down Expand Up @@ -431,9 +431,7 @@ void app_main(void)
{
for (int i = 0; i < 256; ++i)
currentUpdate->palette[i] = (CRAM565[i] << 8) | (CRAM565[i] >> 8);
// rg_video_update_t *previousUpdate = &updates[currentUpdate == &updates[0]];
rg_display_submit(currentUpdate, NULL);
// currentUpdate = previousUpdate;
rg_display_submit(currentUpdate, 0);
}

int elapsed = rg_system_timer() - startTime;
Expand Down
6 changes: 3 additions & 3 deletions prboom-go/main/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ static rg_gui_event_t gamma_update_cb(rg_gui_option_t *option, rg_gui_event_t ev
{
usegamma = gamma;
I_SetPalette(current_palette);
rg_display_submit(&update, NULL);
rg_display_submit(&update, 0);
rg_settings_set_number(NS_APP, SETTING_GAMMA, gamma);
rg_task_delay(50);
}
Expand All @@ -140,7 +140,7 @@ void I_UpdateNoBlit(void)

void I_FinishUpdate(void)
{
rg_display_submit(&update, NULL);
rg_display_submit(&update, 0);
rg_display_sync(true); // Wait for update->buffer to be released
}

Expand Down Expand Up @@ -506,7 +506,7 @@ static void event_handler(int event, void *arg)
}
else if (event == RG_EVENT_REDRAW)
{
rg_display_submit(&update, NULL);
rg_display_submit(&update, 0);
}
}

Expand Down
1 change: 0 additions & 1 deletion retro-core/main/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ rg_audio_sample_t audioBuffer[AUDIO_BUFFER_LENGTH];

rg_video_update_t updates[2];
rg_video_update_t *currentUpdate = &updates[0];
rg_video_update_t *previousUpdate = NULL;

rg_app_t *app;

Expand Down
9 changes: 3 additions & 6 deletions retro-core/main/main_gbc.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#include <sys/time.h>
#include <gnuboy.h>

static bool fullFrame = false;
static int skipFrames = 20; // The 20 is to hide startup flicker in some games

static int video_time;
Expand Down Expand Up @@ -33,7 +32,7 @@ static void event_handler(int event, void *arg)
{
if (event == RG_EVENT_REDRAW)
{
rg_display_submit(currentUpdate, NULL);
rg_display_submit(currentUpdate, 0);
}
}

Expand Down Expand Up @@ -74,7 +73,6 @@ static bool reset_handler(bool hard)
gnuboy_reset(hard);
update_rtc_time();

fullFrame = false;
skipFrames = 20;
autoSaveSRAM_Timer = 0;

Expand Down Expand Up @@ -197,7 +195,7 @@ static rg_gui_event_t rtc_update_cb(rg_gui_option_t *option, rg_gui_event_t even
static void video_callback(void *buffer)
{
int64_t startTime = rg_system_timer();
fullFrame = rg_display_submit(currentUpdate, previousUpdate) == RG_UPDATE_FULL;
rg_display_submit(currentUpdate, 0);
video_time += rg_system_timer() - startTime;
}

Expand Down Expand Up @@ -316,7 +314,6 @@ void gbc_main(void)

if (drawFrame)
{
previousUpdate = currentUpdate;
currentUpdate = &updates[currentUpdate == &updates[0]];
gnuboy_set_framebuffer(currentUpdate->buffer);
}
Expand Down Expand Up @@ -346,7 +343,7 @@ void gbc_main(void)
int frameTime = 1000000 / (60 * app->speed);
if (elapsed > frameTime - 2000) // It takes about 2ms to copy the audio buffer
skipFrames = (elapsed + frameTime / 2) / frameTime;
else if (drawFrame && fullFrame) // This could be avoided when scaling != full
else if (drawFrame && rg_display_get_counters()->lastFullFrame) // This could be avoided when scaling != full
skipFrames = 1;
if (app->speed > 1.f) // This is a hack until we account for audio speed...
skipFrames += (int)app->speed;
Expand Down
4 changes: 2 additions & 2 deletions retro-core/main/main_gw.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ static void event_handler(int event, void *arg)
{
if (event == RG_EVENT_REDRAW)
{
rg_display_submit(currentUpdate, NULL);
rg_display_submit(currentUpdate, 0);
}
}

Expand Down Expand Up @@ -264,7 +264,7 @@ void gw_main(void)
if (rg_display_sync(0) && drawFrame)
{
gw_system_blit(currentUpdate->buffer);
rg_display_submit(currentUpdate, NULL);
rg_display_submit(currentUpdate, 0);
}
/****************************************************************************/

Expand Down
8 changes: 3 additions & 5 deletions retro-core/main/main_lynx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ static void event_handler(int event, void *arg)
{
if (event == RG_EVENT_REDRAW)
{
rg_display_submit(currentUpdate, NULL);
rg_display_submit(currentUpdate, 0);
}
}

Expand Down Expand Up @@ -201,7 +201,6 @@ extern "C" void lynx_main(void)

float sampleTime = 1000000.f / app->sampleRate;
long skipFrames = 0;
bool fullFrame = 0;

// Start emulation
while (1)
Expand Down Expand Up @@ -237,8 +236,7 @@ extern "C" void lynx_main(void)

if (drawFrame)
{
fullFrame = rg_display_submit(currentUpdate, previousUpdate) == RG_UPDATE_FULL;
previousUpdate = currentUpdate;
rg_display_submit(currentUpdate, 0);
currentUpdate = &updates[currentUpdate == &updates[0]];
gPrimaryFrameBuffer = (UBYTE*)currentUpdate->buffer;
}
Expand All @@ -253,7 +251,7 @@ extern "C" void lynx_main(void)
// The Lynx uses a variable framerate so we use the count of generated audio samples as reference instead
else if (elapsed > ((gAudioBufferPointer / 2) * sampleTime))
skipFrames += 1;
else if (drawFrame && fullFrame) // This could be avoided when scaling != full
else if (drawFrame && rg_display_get_counters()->lastFullFrame) // This could be avoided when scaling != full
skipFrames += 1;
}
else if (skipFrames > 0)
Expand Down
9 changes: 3 additions & 6 deletions retro-core/main/main_nes.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#include <nofrendo.h>
#include <nes/nes.h>

static int fullFrame = 0;
static int overscan = true;
static int autocrop = 0;
static int palette = 0;
Expand All @@ -21,7 +20,7 @@ static void event_handler(int event, void *arg)
{
if (event == RG_EVENT_REDRAW)
{
rg_display_submit(currentUpdate, NULL);
rg_display_submit(currentUpdate, 0);
}
}

Expand Down Expand Up @@ -74,7 +73,6 @@ static void build_palette(int n)
updates[1].palette[i] = color;
}
free(pal);
previousUpdate = NULL;
}

static rg_gui_event_t sprite_limit_cb(rg_gui_option_t *option, rg_gui_event_t event)
Expand Down Expand Up @@ -163,7 +161,7 @@ static void blit_screen(uint8 *bmp)
// A rolling average should be used for autocrop == 1, it causes jitter in some games...
// int crop_h = (autocrop == 2) || (autocrop == 1 && nes->ppu->left_bg_counter > 210) ? 8 : 0;
currentUpdate->buffer = NES_SCREEN_GETPTR(bmp, crop_h, crop_v);
fullFrame = rg_display_submit(currentUpdate, previousUpdate) == RG_UPDATE_FULL;
rg_display_submit(currentUpdate, 0);
}

static void nsf_draw_overlay(void)
Expand Down Expand Up @@ -273,7 +271,6 @@ void nes_main(void)

if (drawFrame)
{
previousUpdate = currentUpdate;
currentUpdate = &updates[currentUpdate == &updates[0]];
}

Expand All @@ -287,7 +284,7 @@ void nes_main(void)
int frameTime = 1000000 / (nes->refresh_rate * app->speed);
if (elapsed > frameTime - 2000) // It takes about 2ms to copy the audio buffer
skipFrames = (elapsed + frameTime / 2) / frameTime;
else if (drawFrame && fullFrame) // This could be avoided when scaling != full
else if (drawFrame && rg_display_get_counters()->lastFullFrame)
skipFrames = 1;
else if (nsfPlayer)
skipFrames = 10, nsf_draw_overlay();
Expand Down
Loading

0 comments on commit 4396bb7

Please sign in to comment.