Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: build

on:
push:
branches: [ master ]
pull_request:
workflow_dispatch:

jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Standard GitHub-hosted runners are free and unlimited on public
# repositories, arm64 included, so every target here is native --
# no cross-compiling, no QEMU.
- { os: ubuntu-latest, name: linux-x86_64, artifact: libSugarLibRetro.so }
- { os: ubuntu-24.04-arm, name: linux-arm64, artifact: libSugarLibRetro.so }
- { os: windows-latest, name: windows-x86_64, artifact: SugarLibRetro.dll }
- { os: windows-11-arm, name: windows-arm64, artifact: SugarLibRetro.dll }
# CMake gives a SHARED library the .dylib suffix on Apple. Nobody
# has built this core on macOS before, so treat a failure here as
# news about the code rather than about the workflow.
- { os: macos-latest, name: macos-arm64, artifact: libSugarLibRetro.dylib }
- { os: macos-15-intel, name: macos-x86_64, artifact: libSugarLibRetro.dylib }

steps:
- uses: actions/checkout@v4
with:
submodules: recursive

# The build blanks Romantic Robot's bundled Multiface II firmware before
# compiling (see the root CMakeLists). Without Python that step only
# WARNS and the ROM ends up inside a published binary, so make sure CI
# always has an interpreter rather than silently shipping it.
- uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DSUGARLIBRETRO_BUILD_TESTS=ON

- name: Build
run: cmake --build build --config Release --parallel

- name: Test
working-directory: build
run: ctest -C Release --output-on-failure

- name: Locate core
shell: bash
run: |
found=$(find build -name '${{ matrix.artifact }}' | head -1)
if [ -z "$found" ]; then echo "core not produced"; find build -name '*SugarLibRetro*'; exit 1; fi
echo "core=$found" >> "$GITHUB_ENV"
echo "built $found"

- uses: actions/upload-artifact@v4
with:
name: SugarLibRetro-${{ matrix.name }}
path: ${{ env.core }}
if-no-files-found: error

# Architectures with no GitHub-hosted runner. Cross-compiled on an x86_64
# runner with the distro cross toolchains, then the tests are RUN under
# qemu-user via CMAKE_CROSSCOMPILING_EMULATOR -- so this is not just a
# "does it compile" job.
#
# One baseline build per architecture, deliberately, not a build per CPU
# variant: per-core tuning belongs in the REG-Linux per-CPU prebuilds.
cross:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: linux-armv7
packages: crossbuild-essential-armhf
toolchain: cmake/toolchain-armv7.cmake
- name: linux-riscv64
packages: crossbuild-essential-riscv64
toolchain: cmake/toolchain-riscv64.cmake

steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install cross toolchain and qemu-user
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends ${{ matrix.packages }} qemu-user-static

- name: Configure
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DSUGARLIBRETRO_BUILD_TESTS=ON \
-DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/${{ matrix.toolchain }}

- name: Build
run: cmake --build build --parallel

- name: Test (under qemu-user)
working-directory: build
run: ctest --output-on-failure

- name: Check the binary really is for the target architecture
run: |
so=$(find build -name 'libSugarLibRetro.so' | head -1)
[ -n "$so" ] || { echo "core not produced"; exit 1; }
file "$so"
file "$so" | grep -qiE 'ARM|RISC-V' || { echo "not a cross-built binary"; exit 1; }

- uses: actions/upload-artifact@v4
with:
name: SugarLibRetro-${{ matrix.name }}
path: build/SugarLibRetro/libSugarLibRetro.so
if-no-files-found: error
29 changes: 28 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.0)
cmake_minimum_required(VERSION 3.16)

# Nom du projet
project(SugarLibRetro)
Expand Down Expand Up @@ -39,9 +39,36 @@ endif()
# more for anything else.
set(CPCCORE_NO_HOST_KEYBOARD ON CACHE BOOL "" FORCE)

# zlib registers its own CTest suite, which turns up in our results the
# moment enable_testing() is called at the top level. We do not test zlib.
set(ZLIB_BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(ZLIB_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)

add_subdirectory(${CPCCORE})
add_subdirectory(${ZLIB})

add_subdirectory(SugarLibRetro)



# Tests are opt-in: the REG-Linux package build and the libretro buildbot only
# want the core, and configuring googletest for them would be pure cost.
# cmake -DSUGARLIBRETRO_BUILD_TESTS=ON && ctest
option(SUGARLIBRETRO_BUILD_TESTS "Build the unit tests" OFF)

if (SUGARLIBRETRO_BUILD_TESTS)
# enable_testing() has to be called at the TOP level or ctest finds no
# tests to run from the build root, however many gtest_discover_tests()
# calls the subdirectories make.
enable_testing()

# This project rewrites /MD to /MT (see SugarLibRetro/CMakeLists.txt), so
# googletest must be built against the static runtime too or MSVC fails to
# link with LNK2038. Must be set BEFORE the subdirectory is added.
set(gtest_force_shared_crt OFF CACHE BOOL "" FORCE)
set(BUILD_GMOCK OFF CACHE BOOL "" FORCE)
set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
add_subdirectory(${PROJECT_SOURCE_DIR}/CPCCore/googletest googletest)

add_subdirectory(SugarLibRetro/tests)
endif()
2 changes: 1 addition & 1 deletion CPCCore
2 changes: 1 addition & 1 deletion SugarLibRetro/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.0)
cmake_minimum_required(VERSION 3.16)
project(SugarLibRetro)

file( GLOB SRCS
Expand Down
58 changes: 58 additions & 0 deletions SugarLibRetro/display_geometry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

#include <stdint.h>

// Display geometry and the coordinate transforms that depend on it.
//
// Header-only and free of libretro state on purpose: everything here is pure
// arithmetic, so it can be unit tested without standing up a core. libretro.cpp
// is one large translation unit of static functions and frontend callbacks,
// which is not testable; this is the part that has actual invariants.

// Visible window cut out of the emulator's internal raster buffer, which is
// 1024 ints wide with rows written at 2y (see RetroDisplay::GetVideoBuffer),
// so roughly 1008 x 576 of it is real picture.
//
// "normal" is the long-standing crop: picture plus a thin border, which is
// what most software expects. "full" widens it to show the CPC's overscan
// region, which demos and a fair amount of French software draw into. The
// frontend is told the full size as its maximum so the geometry can change at
// runtime without a reinit; cap32 exposes the same idea as cap32_scr_crop.
#define WIDTH 640
#define HEIGHT 480
#define OFFSET_X 207
#define OFFSET_Y 84

#define FULL_WIDTH 800
#define FULL_HEIGHT 560
#define FULL_OFFSET_X 112
#define FULL_OFFSET_Y 8

// The emulator writes one CPC scanline per two buffer rows, so a crop that is
// crop_h buffer rows tall is only half that many real lines. That halved
// figure is what the frontend is told and what is handed to video_cb.
static inline int SugarboxOutputHeight(int crop_h)
{
return crop_h / 2;
}

// Lightgun. RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X/Y arrive as a signed 16-bit
// sweep of the visible picture; the CPC's CRTC wants a position in the raw
// buffer coordinate space, offset by the crop origin.
static inline int SugarboxGunBufferX(int16_t gun_x, int crop_w, int crop_x)
{
return ((int)gun_x + 0x8000) * crop_w / 0x10000 + crop_x;
}

// Y is the subtle one. CPCCore tests
// ((gate_array_)->monitor_)->y_ * 2 == gun_y_ (CRTC.cpp)
// which is an exact equality against an always-even number. So the engine
// wants a doubled-row Y, and one that is EVEN, or the comparison can never
// hold. Scaling against the displayed line count and doubling gives that.
// Scaling against the full crop height -- as this did before -- produced an
// odd value half the time, and no shot at those positions could ever register.
static inline int SugarboxGunBufferY(int16_t gun_y, int crop_h, int crop_y)
{
const int displayed = ((int)gun_y + 0x8000) * SugarboxOutputHeight(crop_h) / 0x10000;
return displayed * 2 + crop_y;
}
Loading
Loading