// Copyright (c) 2019-2026 Andreas T Jonsson // // 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 #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 #include #include // ── 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!"); }