mirror of
https://codeberg.org/virtualxt/virtualxt.git
synced 2026-10-21 14:24:47 +00:00
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
441 lines
12 KiB
C++
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!");
|
|
}
|