Files
Andreas T Jonsson 23af71339a feat(pi): CRT offload to Core 2, 4:3 letterboxing, and peripheral status logging
Pi 3 bare-metal improvements:
- Offload CRT filter to Core 2 via CRT_FILTER_EXTERNAL compile flag.
  Core 1 queues frames; Core 2 worker (RenderThread) processes
  asynchronously with 1-frame latency. Trampoline pattern in crt.c/h.
- Add 4:3 letterboxed scaling on Core 2 render thread.
  HW framebuffer created once at physical display resolution
  (CBcmFrameBuffer auto-detect); largest centered 4:3 rect computed,
  black bars cleared once, nearest-neighbor scale into target rect.
- Raise KERNEL_MAX_SIZE to 0x1400000 (20 MB) across Circle Config.mk,
  kernel Makefile, and package-rpi.sh to accommodate ~19 MB .bss.
- Add vxt_peripheral_print_status() in src/vxt/peripheral.c that logs
  CPU type (8088/V20), frequency, installed peripherals, IO map ranges,
  and memory map ranges. Called from retro_load_game() in core.c.

Refs: docs/STATUS.md
2026-05-27 14:25:50 +02:00

441 lines
12 KiB
C++

// Copyright (c) 2019-2026 Andreas T Jonsson <mail@andreasjonsson.se>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
#include "emuloop.h"
#include "kernel.h"
#include <circle/util.h>
#ifndef ARM_ALLOW_MULTI_CORE
#error You need to enable multicore support!
#endif
extern struct retro_frame_time_callback frame_time;
extern "C" {
void retro_run(void);
void retro_set_video_refresh(retro_video_refresh_t cb);
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb);
}
CSpinLock CEmuLoop::s_SpinLock(TASK_LEVEL);
CBcmFrameBuffer *CEmuLoop::s_pFrameBuffer(0);
unsigned screen_width = 0;
unsigned screen_height = 0;
#ifdef CRT_FILTER_EXTERNAL
#include <circle/atomic.h>
#include <circle/synchronize.h>
#include <frontend/crt.h>
// ── CRT filter offload + 4:3 letterbox scaling (Core 2 worker) ────────────
//
// When CRT_FILTER_EXTERNAL is defined, all framebuffer output is produced
// on Core 2. Core 1 only queues raw (or CRT-filtered intermediate) frames;
// Core 2 scales + letterboxes into the physical-resolution HW framebuffer.
#define CRT_MAX_WIDTH 720u
#define CRT_MAX_HEIGHT 700u
#define CRT_MAX_PIXELS (CRT_MAX_WIDTH * CRT_MAX_HEIGHT)
static CBcmFrameBuffer *g_pHwFb = nullptr;
static unsigned g_hw_w = 0, g_hw_h = 0;
static unsigned g_hw_pitch = 0;
static unsigned g_target_x = 0, g_target_y = 0;
static unsigned g_target_w = 0, g_target_h = 0;
static int g_bars_drawn = 0;
static uint32_t core2_input[CRT_MAX_PIXELS];
static uint32_t core2_intermediate[CRT_MAX_PIXELS];
static unsigned core2_w = 0;
static unsigned core2_h = 0;
static volatile int core2_job_ready = 0;
static volatile int core2_apply_crt = 0;
static volatile int core2_reset_pending = 0;
static void ensure_hw_framebuffer(void) {
if (g_pHwFb)
return;
g_pHwFb = new CBcmFrameBuffer(0, 0, 32);
if (!g_pHwFb->Initialize()) {
LOG(RETRO_LOG_WARN, "Could not auto-detect display size.");
delete g_pHwFb;
g_pHwFb = new CBcmFrameBuffer(1920, 1080, 32);
if (!g_pHwFb->Initialize()) {
LOG(RETRO_LOG_WARN, "Could not set 1920x1080 fallback.");
delete g_pHwFb;
g_pHwFb = new CBcmFrameBuffer(640, 480, 32);
if (!g_pHwFb->Initialize()) {
LOG(RETRO_LOG_ERROR, "Could not initialize any framebuffer!");
delete g_pHwFb;
g_pHwFb = nullptr;
return;
}
}
}
g_hw_w = g_pHwFb->GetWidth();
g_hw_h = g_pHwFb->GetHeight();
g_hw_pitch = g_pHwFb->GetPitch();
// Compute largest 4:3 rect that fits inside the physical display.
g_target_h = g_hw_h;
g_target_w = g_hw_h * 4 / 3;
if (g_target_w > g_hw_w) {
g_target_w = g_hw_w;
g_target_h = g_hw_w * 3 / 4;
}
g_target_x = (g_hw_w - g_target_w) / 2;
g_target_y = (g_hw_h - g_target_h) / 2;
g_bars_drawn = 0;
LOG(RETRO_LOG_INFO, "HW framebuffer: %ux%u, 4:3 target: %ux%u @ (%u,%u)",
g_hw_w, g_hw_h, g_target_w, g_target_h, g_target_x, g_target_y);
}
static void core2_blit_letterbox(const uint32_t *src, unsigned sw, unsigned sh) {
if (!g_pHwFb)
return;
u8 *fb_base = (u8*)(u64)g_pHwFb->GetBuffer();
if (!g_bars_drawn) {
memset(fb_base, 0, g_hw_h * g_hw_pitch);
g_bars_drawn = 1;
}
for (unsigned dy = 0; dy < g_target_h; dy++) {
unsigned sy = (dy * sh) / g_target_h;
const uint32_t *src_row = src + sy * sw;
#if RASPPI == 5
#define RGB555(red, green, blue) (((red) & 0x1F) << 11 | ((green) & 0x1F) << 6 | ((blue) & 0x1F))
u16 *dst_row = (u16*)(fb_base + (g_target_y + dy) * g_hw_pitch) + g_target_x;
for (unsigned dx = 0; dx < g_target_w; dx++) {
unsigned sx = (dx * sw) / g_target_w;
uint32_t p = src_row[sx];
dst_row[dx] = RGB555((p >> 19) & 0x1F, (p >> 11) & 0x1F, (p >> 3) & 0x1F);
}
#else
uint32_t *dst_row = (uint32_t*)(fb_base + (g_target_y + dy) * g_hw_pitch) + g_target_x;
for (unsigned dx = 0; dx < g_target_w; dx++) {
unsigned sx = (dx * sw) / g_target_w;
dst_row[dx] = src_row[sx];
}
#endif
}
}
// Trampolines — provided here when CRT_FILTER_EXTERNAL is defined.
extern "C" void crt_filter_apply(const uint32_t *src,
unsigned width,
unsigned height,
uint32_t *dst) {
(void)dst;
if (width == 0 || height == 0)
return;
unsigned pixels = width * height;
if (pixels > CRT_MAX_PIXELS)
pixels = CRT_MAX_PIXELS;
memcpy(core2_input, src, pixels * sizeof(uint32_t));
core2_w = width;
core2_h = height;
DataSyncBarrier();
AtomicSet(&core2_apply_crt, 1);
AtomicSet(&core2_job_ready, 1);
SendEvent();
}
extern "C" void crt_filter_reset(void) {
AtomicSet(&core2_reset_pending, 1);
}
extern "C" unsigned crt_filter_output_height(unsigned height) {
return crt_filter_output_height_impl(height);
}
#endif // CRT_FILTER_EXTERNAL
// ── Audio ring buffer ─────────────────────────────────────────────────────
#define AUDIO_RING_BUFFER_SIZE (SAMPLE_RATE * 2) // 2 seconds of stereo samples
static int16_t audio_ring[AUDIO_RING_BUFFER_SIZE];
static volatile unsigned audio_ring_write = 0;
static volatile unsigned audio_ring_read = 0;
static unsigned audio_ring_count(void) {
unsigned w = audio_ring_write;
unsigned r = audio_ring_read;
return (w >= r) ? (w - r) : (AUDIO_RING_BUFFER_SIZE - r + w);
}
static unsigned audio_ring_free(void) {
return AUDIO_RING_BUFFER_SIZE - audio_ring_count() - 1;
}
static void audio_ring_write_samples(const int16_t *data, size_t frames) {
unsigned free_space = audio_ring_free();
unsigned to_write = (frames * 2 < free_space) ? (frames * 2) : free_space;
for (unsigned i = 0; i < to_write; i++) {
audio_ring[audio_ring_write] = data[i];
audio_ring_write = (audio_ring_write + 1) % AUDIO_RING_BUFFER_SIZE;
}
}
static unsigned audio_ring_read_samples(int16_t *data, unsigned samples) {
unsigned avail = audio_ring_count();
unsigned to_read = (samples < avail) ? samples : avail;
for (unsigned i = 0; i < to_read; i++) {
data[i] = audio_ring[audio_ring_read];
audio_ring_read = (audio_ring_read + 1) % AUDIO_RING_BUFFER_SIZE;
}
return to_read;
}
// ── Libretro callbacks ────────────────────────────────────────────────────
extern "C" void video_refresh(const void *data, unsigned width, unsigned height, size_t pitch) {
(void)pitch;
#ifdef CRT_FILTER_EXTERNAL
(void)data;
(void)width;
(void)height;
if (!g_pHwFb)
ensure_hw_framebuffer();
// If the trampoline already queued a CRT-filtered frame, nothing to do.
// Otherwise queue the raw frame now.
if (!AtomicGet(&core2_job_ready)) {
unsigned pixels = width * height;
if (pixels > CRT_MAX_PIXELS)
pixels = CRT_MAX_PIXELS;
memcpy(core2_input, data, pixels * sizeof(uint32_t));
core2_w = width;
core2_h = height;
DataSyncBarrier();
AtomicSet(&core2_apply_crt, 0);
AtomicSet(&core2_job_ready, 1);
SendEvent();
}
#else
if ((width != screen_width) || (height != screen_height) || !CEmuLoop::s_pFrameBuffer) {
screen_width = width;
screen_height = height;
if (CEmuLoop::s_pFrameBuffer)
delete CEmuLoop::s_pFrameBuffer;
CEmuLoop::s_pFrameBuffer = new CBcmFrameBuffer(width, height, 32);
if (!CEmuLoop::s_pFrameBuffer->Initialize()) {
LOG(RETRO_LOG_WARN, "Could not set correct resolution. Fallback to virtual resolution.");
delete CEmuLoop::s_pFrameBuffer;
CEmuLoop::s_pFrameBuffer = new CBcmFrameBuffer(640, 480, 32, width, height);
if (!CEmuLoop::s_pFrameBuffer->Initialize()) {
LOG(RETRO_LOG_ERROR, "Could not initialize framebuffer!");
delete CEmuLoop::s_pFrameBuffer;
return;
}
}
}
const u8 *rgba = (const u8*)data;
u8 *buffer = (u8*)(u64)CEmuLoop::s_pFrameBuffer->GetBuffer();
for (unsigned i = 0; i < height; i++) {
#if RASPPI == 5
#define RGB555(red, green, blue) (((red) & 0x1F) << 11 | ((green) & 0x1F) << 6 | ((blue) & 0x1F))
for (unsigned j = 0; j < width; j++) {
((u16*)buffer)[j] = RGB555(rgba[2] >> 3, rgba[1] >> 3, rgba[0] >> 3);
rgba += 4;
}
#else
memcpy(buffer, rgba, 4 * width);
rgba += 4 * width;
#endif
buffer += CEmuLoop::s_pFrameBuffer->GetPitch();
}
#endif
}
extern "C" size_t audio_batch(const int16_t *data, size_t frames) {
audio_ring_write_samples(data, frames);
return frames;
}
// ── CEmuLoop implementation ───────────────────────────────────────────────
CEmuLoop::CEmuLoop(
CMemorySystem *mem, CKernelOptions *opt, CBcmFrameBuffer *fb,
CSoundBaseDevice *snd, unsigned al)
:
CMultiCoreSupport(mem),
m_pOptions(opt),
m_pSound(snd),
m_AudioLatency(al)
{
assert(CLOCKHZ == 1000000);
s_pFrameBuffer = fb;
retro_set_video_refresh(&video_refresh);
retro_set_audio_sample_batch(&audio_batch);
}
CEmuLoop::~CEmuLoop(void) {
SendIPI(1, IPI_HALT_CORE); // EmuThread
SendIPI(2, IPI_HALT_CORE); // RenderThread
SendIPI(3, IPI_HALT_CORE); // AudioThread
}
void CEmuLoop::Lock(void) {
s_SpinLock.Acquire();
}
void CEmuLoop::Unlock(void) {
s_SpinLock.Release();
}
void CEmuLoop::Run(unsigned nCore) {
switch (nCore) {
case 1: EmuThread(); break;
case 2: RenderThread(); break;
case 3: AudioThread(); break;
}
}
void CEmuLoop::EmuThread(void) {
LOGI("Emulation thread started!");
for (u64 ticks = CTimer::GetClockTicks64(); CKernel::Get()->m_ShutdownMode == ShutdownNone;) {
u64 now = CTimer::GetClockTicks64();
u64 dt = now - ticks;
if (dt >= (CLOCKHZ / 60)) {
Lock();
if (frame_time.callback)
frame_time.callback(dt);
retro_run();
Unlock();
ticks = now;
}
}
LOGI("Emulation thread ended!");
}
void CEmuLoop::RenderThread(void) {
LOGI("Render thread started!");
#ifdef CRT_FILTER_EXTERNAL
for (; CKernel::Get()->m_ShutdownMode == ShutdownNone;) {
if (!AtomicGet(&core2_job_ready)) {
WaitForEvent();
continue;
}
if (AtomicGet(&core2_reset_pending)) {
crt_filter_reset_impl();
AtomicSet(&core2_reset_pending, 0);
}
const uint32_t *src = core2_input;
unsigned sw = core2_w;
unsigned sh = core2_h;
if (AtomicGet(&core2_apply_crt)) {
crt_filter_apply_impl(core2_input, sw, sh, core2_intermediate);
src = core2_intermediate;
sh = crt_filter_output_height_impl(sh);
AtomicSet(&core2_apply_crt, 0);
}
core2_blit_letterbox(src, sw, sh);
DataSyncBarrier();
AtomicSet(&core2_job_ready, 0);
}
#else
for (u64 ticks = CTimer::GetClockTicks64(); CKernel::Get()->m_ShutdownMode == ShutdownNone;) {
u64 now = CTimer::GetClockTicks64();
if ((now - ticks) >= (CLOCKHZ / 60)) {
ticks = now;
// Rendering is done synchronously in video_refresh callback.
// This thread can be used for future post-processing or VSync.
}
}
#endif
LOGI("Render thread ended!");
}
void CEmuLoop::AudioThread(void) {
if (!m_pSound)
return;
LOGI("Audio thread started!");
const unsigned chunk_samples = CHUNK_SIZE * 2; // stereo
DMA_BUFFER(int16_t, audio_chunk, chunk_samples);
u64 audio_ticks = CTimer::GetClockTicks64();
for (u64 ticks = audio_ticks; CKernel::Get()->m_ShutdownMode == ShutdownNone; ticks = CTimer::GetClockTicks64()) {
if ((ticks - audio_ticks) >= (CLOCKHZ / 1000)) { // 1ms granularity
audio_ticks = ticks;
if (m_pSound && m_pSound->IsActive()) {
unsigned samples_needed = m_pSound->GetQueueSizeFrames() - m_pSound->GetQueueFramesAvail();
samples_needed *= 2; // stereo
while (samples_needed > 0) {
unsigned to_read = (samples_needed < chunk_samples) ? samples_needed : chunk_samples;
unsigned read = audio_ring_read_samples(audio_chunk, to_read);
if (read == 0)
break;
m_pSound->Write((u8*)audio_chunk, read * sizeof(int16_t));
samples_needed -= read;
}
}
}
}
LOGI("Audio thread ended!");
}