Compare commits

...

2 commits

Author SHA1 Message Date
0060116835 Start of 3D support. 2026-09-05 18:38:46 -05:00
3f4b7aa405 Mac build is now universal. For real this time 2026-09-05 01:26:37 -05:00
26 changed files with 17194 additions and 92 deletions

View file

@ -18,6 +18,18 @@ API Changes
database), and lfs.mkdir and lfs.rmdir act on its data overlay, so
frameworks that list or create their own directories work packed.
- 3D scenes. A game can draw a 3D scene between the disc video and the
overlay: primitive meshes and script-built geometry, materials with
colour, textures from sprites, the disc or a loaded video, metallic and
roughness, up to eight lights, any node as the camera, and glTF 2.0
models (.glb, self-contained) with node animation and skinning, placed
any number of times. Everything is a node in one tree. New calls:
scene*, node*, mesh*, material*, light*, camera*, model*, animation*,
and the LIGHT_* constants; see the 3D Scenes chapter of the manual.
Needs a GPU with Vulkan, Direct3D 12 or Metal (Raspberry Pi 4 or
later); 2D games run as before without one. The renderer now runs on
SDL's GPU device where there is one.
- A game can be one file: singe --pack DIRECTORY GAME.game writes the game's
scripts, art, sounds, fonts, and video into an SQLite database, and
singe GAME.game (or the menu, which lists every .game file beside the game
@ -236,7 +248,7 @@ Fixes
builds every library and Singe itself. The results are a Linux binary
that runs on glibc 2.28 or newer, a Windows 10 or newer binary, a
64-bit Raspberry Pi OS (bullseye or newer) binary and a macOS 13 or
newer Apple silicon binary. The toolchains repository is no longer
newer universal (Apple silicon and Intel) binary. The toolchains repository is no longer
needed. See INSTALL.

View file

@ -273,6 +273,12 @@ set(SINGE_SOURCE
src/main.h
src/pack.c
src/pack.h
src/model.c
src/model.h
src/math3d.c
src/math3d.h
src/scene.c
src/scene.h
src/singe.c
src/singe.h
src/stddclmr.h

View file

@ -76,6 +76,18 @@
"SINGE_CROSS_OS": "darwin",
"SINGE_SUFFIX": ""
}
},
{
"name": "macos-x86_64",
"displayName": "macOS Intel (zig)",
"inherits": "base",
"binaryDir": "${sourceDir}/.builddir/macos/x86_64/superbuild",
"toolchainFile": "${sourceDir}/cmake/zig/x86_64-macos.cmake",
"cacheVariables": {
"SINGE_TRIPLE": "x86_64-apple-darwin22.4",
"SINGE_CROSS_OS": "darwin",
"SINGE_SUFFIX": ""
}
}
],
"buildPresets": [
@ -98,6 +110,10 @@
{
"name": "macos-aarch64",
"configurePreset": "macos-aarch64"
},
{
"name": "macos-x86_64",
"configurePreset": "macos-x86_64"
}
]
}

View file

@ -90,7 +90,11 @@ put a tarball holding a single MacOSX*.sdk directory at
that), or point SINGE_MACOS_SDK at an unpacked SDK. SINGE_MACOS_SDK_URL
and SINGE_MACOS_SDK_SHA256 pick a different mirror. Then:
./build-all.sh macos aarch64
./build-all.sh macos universal
builds the Apple silicon and Intel presets (macos-aarch64, macos-x86_64)
and joins them with llvm-lipo into Singe-v3.00-Macos-universal. Either
architecture alone is ./build-all.sh macos aarch64 or x86_64.
The Raspberry Pi build (64-bit Raspberry Pi OS, glibc 2.31 or newer)
also uses zig. The platform headers and libraries it links against are

View file

@ -5,6 +5,7 @@ these tools, Singe would not exist.
arg_parser BSD-2-Clause http://savannah.nongnu.org/projects/arg-parser
binaryheap.lua MIT http://tieske.github.io/binaryheap.lua
cgltf MIT https://github.com/jkuhlmann/cgltf
copas MIT https://lunarmodules.github.io/copas
ffmpeg LGPL-2.1 https://ffmpeg.org
freetype FTL https://freetype.org

View file

@ -41,6 +41,14 @@ function buildAll() {
# Every platform builds with the zig the build fetches itself; the Pi preset also fetches its
# platform packages and macOS needs the Apple SDK tarball (see INSTALL).
# "macos universal" builds both macOS presets and joins them with llvm-lipo.
if [[ "${OS}" == "macos" && "${ARCH}" == "universal" ]]; then
buildAll macos aarch64 "$@"
buildAll macos x86_64 "$@"
cmake -P cmake/zig/macosUniversal.cmake
return
fi
cmake --preset ${OS}-${ARCH}
cmake --build --preset ${OS}-${ARCH} "$@"
}
@ -83,7 +91,7 @@ if [[ $# -ge 2 ]]; then
buildAll "$@" 2>&1 | tee ${G_BUILDDIR}/$1-$2.log
else
buildAll linux x86_64 2>&1 | tee ${G_BUILDDIR}/linux-x86_64.log
buildAll macos aarch64 2>&1 | tee ${G_BUILDDIR}/macos-aarch64.log
buildAll macos universal 2>&1 | tee ${G_BUILDDIR}/macos-universal.log
buildAll pi aarch64 2>&1 | tee ${G_BUILDDIR}/pi-aarch64.log
buildAll windows x86_64 2>&1 | tee ${G_BUILDDIR}/windows-x86_64.log
fi

View file

@ -257,14 +257,14 @@ if(SINGE_ZIG_TARGET)
# FFmpeg only uses nm to learn the symbol prefix. The host's binutils nm reads x86 ELF and PE
# objects but not aarch64 or Mach-O ones; the prefix is empty on Linux either way, and on macOS
# (where it is an underscore) llvm-nm from Debian's llvm package reads Mach-O.
if(KANGAROO_ARCH STREQUAL "x86_64")
list(APPEND ffmpegTools --nm=nm)
elseif(KANGAROO_OS STREQUAL "macos")
if(KANGAROO_OS STREQUAL "macos")
find_program(SINGE_LLVM_NM NAMES llvm-nm llvm-nm-21 llvm-nm-20 llvm-nm-19 llvm-nm-18 llvm-nm-17 llvm-nm-16 llvm-nm-15)
if(NOT SINGE_LLVM_NM)
message(FATAL_ERROR "llvm-nm is needed for the macOS FFmpeg build (apt-get install llvm).")
endif()
list(APPEND ffmpegTools --nm=${SINGE_LLVM_NM})
elseif(KANGAROO_ARCH STREQUAL "x86_64")
list(APPEND ffmpegTools --nm=nm)
endif()
else()
set(ffmpegTools --cross-prefix=${SINGE_TRIPLE}- --cc=${SB_CC} --cxx=${SB_CXX} --ranlib=${SB_RANLIB})

View file

@ -17,83 +17,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
# macOS on Apple silicon with zig as the compiler. zig carries the macOS libc headers and the
# libSystem stub itself; everything else (Cocoa, CoreAudio, VideoToolbox, Metal and the rest of
# the frameworks, libc++, libiconv) comes from an Apple SDK, which Apple does not let a build
# script fetch. The SDK is expected, in this order:
# 1. an unpacked SDK at SINGE_MACOS_SDK (default .builddir/toolchains/MacOSX.sdk);
# 2. a tarball at .builddir/toolchains/MacOSX.sdk.tar.xz, unpacked on first use;
# 3. SINGE_MACOS_SDK_URL (with SINGE_MACOS_SDK_SHA256), downloaded and unpacked on first use;
# the default points at the community mirror osxcross users share (github.com/joseluisq/
# macosx-sdks), pinned to one SDK release and its published SHA-256.
# See INSTALL for how to make the tarball from Xcode instead.
# macOS on Apple silicon; everything is in macos.cmake.
include(${CMAKE_CURRENT_LIST_DIR}/zig.cmake)
set(CMAKE_SYSTEM_NAME Darwin)
set(SINGE_MACOS_ARCH aarch64)
set(CMAKE_SYSTEM_PROCESSOR arm64)
# Oldest macOS the binary runs on (Ventura: Intel Macs from 2017 and every Apple silicon Mac).
set(SINGE_MACOS_VERSION 13.0)
get_filename_component(sdkToolchains ${CMAKE_CURRENT_LIST_DIR}/../../.builddir/toolchains ABSOLUTE)
set(SINGE_MACOS_SDK ${sdkToolchains}/MacOSX.sdk CACHE PATH "Unpacked Apple SDK for the macOS build")
set(SINGE_MACOS_SDK_URL "https://github.com/joseluisq/macosx-sdks/releases/download/15.5/MacOSX15.5.sdk.tar.xz" CACHE STRING "Where to fetch the Apple SDK tarball from; empty to never download")
set(SINGE_MACOS_SDK_SHA256 "c15cf0f3f17d714d1aa5a642da8e118db53d79429eb015771ba816aa7c6c1cbd" CACHE STRING "SHA-256 of that tarball; empty to skip the check")
set(sdkTarball ${sdkToolchains}/MacOSX.sdk.tar.xz)
if(NOT EXISTS ${SINGE_MACOS_SDK}/System/Library/Frameworks/Cocoa.framework)
if((NOT EXISTS ${sdkTarball}) AND SINGE_MACOS_SDK_URL)
message(STATUS "Downloading the Apple SDK from ${SINGE_MACOS_SDK_URL}")
file(MAKE_DIRECTORY ${sdkToolchains})
if(SINGE_MACOS_SDK_SHA256)
file(DOWNLOAD ${SINGE_MACOS_SDK_URL} ${sdkTarball} EXPECTED_HASH SHA256=${SINGE_MACOS_SDK_SHA256} STATUS status)
else()
file(DOWNLOAD ${SINGE_MACOS_SDK_URL} ${sdkTarball} STATUS status)
endif()
list(GET status 0 code)
if(NOT code EQUAL 0)
message(FATAL_ERROR "Apple SDK download failed: ${status}")
endif()
endif()
if(EXISTS ${sdkTarball})
message(STATUS "Unpacking ${sdkTarball}")
set(sdkUnpack ${sdkToolchains}/MacOSX.sdk.unpack)
file(REMOVE_RECURSE ${sdkUnpack})
file(ARCHIVE_EXTRACT INPUT ${sdkTarball} DESTINATION ${sdkUnpack})
file(GLOB sdkFound LIST_DIRECTORIES true ${sdkUnpack}/*.sdk ${sdkUnpack}/*/*.sdk)
list(LENGTH sdkFound sdkCount)
if(NOT sdkCount EQUAL 1)
message(FATAL_ERROR "${sdkTarball} should hold exactly one MacOSX*.sdk directory; found ${sdkCount}.")
endif()
file(RENAME ${sdkFound} ${SINGE_MACOS_SDK})
file(REMOVE_RECURSE ${sdkUnpack})
endif()
endif()
if(NOT EXISTS ${SINGE_MACOS_SDK}/System/Library/Frameworks/Cocoa.framework)
message(FATAL_ERROR "No Apple SDK at ${SINGE_MACOS_SDK}. Put an SDK tarball at ${sdkTarball} (see INSTALL), or set SINGE_MACOS_SDK to an unpacked SDK, or SINGE_MACOS_SDK_URL to fetch one.")
endif()
# No --sysroot: zig would then prefix every -L path with it, the superbuild's own lib directory
# included. The SDK is named outright instead: its usr/include (libDER and other non-libc pieces)
# after zig's own libc headers, its frameworks, and its stub libraries. The SDK's CMTag.h uses two
# macOS 14 types without an availability guard, and FFmpeg's configure makes that an error with
# -Werror=partial-availability; the trailing -Wno-unguarded-availability-new wins over it.
singeZigToolchain(aarch64-macos.${SINGE_MACOS_VERSION} "-idirafter ${SINGE_MACOS_SDK}/usr/include -F${SINGE_MACOS_SDK}/System/Library/Frameworks -L${SINGE_MACOS_SDK}/usr/lib -Wno-unguarded-availability-new")
# SDL3 compiles Objective-C, which the same zig wrappers handle, and CMake's Darwin support insists
# on an install_name_tool even though nothing here links dynamically; llvm's stands in.
set(CMAKE_OBJC_COMPILER ${CMAKE_C_COMPILER} CACHE FILEPATH "" FORCE)
set(CMAKE_OBJCXX_COMPILER ${CMAKE_CXX_COMPILER} CACHE FILEPATH "" FORCE)
find_program(SINGE_INSTALL_NAME_TOOL NAMES llvm-install-name-tool llvm-install-name-tool-21 llvm-install-name-tool-20 llvm-install-name-tool-19 llvm-install-name-tool-18 llvm-install-name-tool-17 llvm-install-name-tool-16 llvm-install-name-tool-15)
if(NOT SINGE_INSTALL_NAME_TOOL)
message(FATAL_ERROR "llvm-install-name-tool is needed for the macOS build (apt-get install llvm).")
endif()
set(CMAKE_INSTALL_NAME_TOOL ${SINGE_INSTALL_NAME_TOOL} CACHE FILEPATH "" FORCE)
set(CMAKE_OSX_SYSROOT "" CACHE PATH "Left empty; the wrappers name the SDK themselves" FORCE)
set(CMAKE_OSX_ARCHITECTURES "" CACHE STRING "Left empty; the zig target names the architecture" FORCE)
set(CMAKE_FIND_ROOT_PATH ${SINGE_MACOS_SDK})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(KANGAROO_OS macos)
set(KANGAROO_ARCH aarch64)
include(${CMAKE_CURRENT_LIST_DIR}/macos.cmake)

99
cmake/zig/macos.cmake Normal file
View file

@ -0,0 +1,99 @@
#
# Singe 3
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public Licens
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
# macOS with zig as the compiler, shared by the aarch64 and x86_64 toolchain files (which set
# SINGE_MACOS_ARCH and CMAKE_SYSTEM_PROCESSOR before including this). zig carries the macOS libc
# headers and the libSystem stub itself; everything else (Cocoa, CoreAudio, VideoToolbox, Metal and the rest of
# the frameworks, libc++, libiconv) comes from an Apple SDK, which Apple does not let a build
# script fetch. The SDK is expected, in this order:
# 1. an unpacked SDK at SINGE_MACOS_SDK (default .builddir/toolchains/MacOSX.sdk);
# 2. a tarball at .builddir/toolchains/MacOSX.sdk.tar.xz, unpacked on first use;
# 3. SINGE_MACOS_SDK_URL (with SINGE_MACOS_SDK_SHA256), downloaded and unpacked on first use;
# the default points at the community mirror osxcross users share (github.com/joseluisq/
# macosx-sdks), pinned to one SDK release and its published SHA-256.
# See INSTALL for how to make the tarball from Xcode instead.
include(${CMAKE_CURRENT_LIST_DIR}/zig.cmake)
set(CMAKE_SYSTEM_NAME Darwin)
# Oldest macOS the binary runs on (Ventura: Intel Macs from 2017 and every Apple silicon Mac).
set(SINGE_MACOS_VERSION 13.0)
get_filename_component(sdkToolchains ${CMAKE_CURRENT_LIST_DIR}/../../.builddir/toolchains ABSOLUTE)
set(SINGE_MACOS_SDK ${sdkToolchains}/MacOSX.sdk CACHE PATH "Unpacked Apple SDK for the macOS build")
set(SINGE_MACOS_SDK_URL "https://github.com/joseluisq/macosx-sdks/releases/download/15.5/MacOSX15.5.sdk.tar.xz" CACHE STRING "Where to fetch the Apple SDK tarball from; empty to never download")
set(SINGE_MACOS_SDK_SHA256 "c15cf0f3f17d714d1aa5a642da8e118db53d79429eb015771ba816aa7c6c1cbd" CACHE STRING "SHA-256 of that tarball; empty to skip the check")
set(sdkTarball ${sdkToolchains}/MacOSX.sdk.tar.xz)
if(NOT EXISTS ${SINGE_MACOS_SDK}/System/Library/Frameworks/Cocoa.framework)
if((NOT EXISTS ${sdkTarball}) AND SINGE_MACOS_SDK_URL)
message(STATUS "Downloading the Apple SDK from ${SINGE_MACOS_SDK_URL}")
file(MAKE_DIRECTORY ${sdkToolchains})
if(SINGE_MACOS_SDK_SHA256)
file(DOWNLOAD ${SINGE_MACOS_SDK_URL} ${sdkTarball} EXPECTED_HASH SHA256=${SINGE_MACOS_SDK_SHA256} STATUS status)
else()
file(DOWNLOAD ${SINGE_MACOS_SDK_URL} ${sdkTarball} STATUS status)
endif()
list(GET status 0 code)
if(NOT code EQUAL 0)
message(FATAL_ERROR "Apple SDK download failed: ${status}")
endif()
endif()
if(EXISTS ${sdkTarball})
message(STATUS "Unpacking ${sdkTarball}")
set(sdkUnpack ${sdkToolchains}/MacOSX.sdk.unpack)
file(REMOVE_RECURSE ${sdkUnpack})
file(ARCHIVE_EXTRACT INPUT ${sdkTarball} DESTINATION ${sdkUnpack})
file(GLOB sdkFound LIST_DIRECTORIES true ${sdkUnpack}/*.sdk ${sdkUnpack}/*/*.sdk)
list(LENGTH sdkFound sdkCount)
if(NOT sdkCount EQUAL 1)
message(FATAL_ERROR "${sdkTarball} should hold exactly one MacOSX*.sdk directory; found ${sdkCount}.")
endif()
file(RENAME ${sdkFound} ${SINGE_MACOS_SDK})
file(REMOVE_RECURSE ${sdkUnpack})
endif()
endif()
if(NOT EXISTS ${SINGE_MACOS_SDK}/System/Library/Frameworks/Cocoa.framework)
message(FATAL_ERROR "No Apple SDK at ${SINGE_MACOS_SDK}. Put an SDK tarball at ${sdkTarball} (see INSTALL), or set SINGE_MACOS_SDK to an unpacked SDK, or SINGE_MACOS_SDK_URL to fetch one.")
endif()
# No --sysroot: zig would then prefix every -L path with it, the superbuild's own lib directory
# included. The SDK is named outright instead: its usr/include (libDER and other non-libc pieces)
# after zig's own libc headers, its frameworks, and its stub libraries. The SDK's CMTag.h uses two
# macOS 14 types without an availability guard, and FFmpeg's configure makes that an error with
# -Werror=partial-availability; the trailing -Wno-unguarded-availability-new wins over it.
singeZigToolchain(${SINGE_MACOS_ARCH}-macos.${SINGE_MACOS_VERSION} "-idirafter ${SINGE_MACOS_SDK}/usr/include -F${SINGE_MACOS_SDK}/System/Library/Frameworks -L${SINGE_MACOS_SDK}/usr/lib -Wno-unguarded-availability-new")
# SDL3 compiles Objective-C, which the same zig wrappers handle, and CMake's Darwin support insists
# on an install_name_tool even though nothing here links dynamically; llvm's stands in.
set(CMAKE_OBJC_COMPILER ${CMAKE_C_COMPILER} CACHE FILEPATH "" FORCE)
set(CMAKE_OBJCXX_COMPILER ${CMAKE_CXX_COMPILER} CACHE FILEPATH "" FORCE)
find_program(SINGE_INSTALL_NAME_TOOL NAMES llvm-install-name-tool llvm-install-name-tool-21 llvm-install-name-tool-20 llvm-install-name-tool-19 llvm-install-name-tool-18 llvm-install-name-tool-17 llvm-install-name-tool-16 llvm-install-name-tool-15)
if(NOT SINGE_INSTALL_NAME_TOOL)
message(FATAL_ERROR "llvm-install-name-tool is needed for the macOS build (apt-get install llvm).")
endif()
set(CMAKE_INSTALL_NAME_TOOL ${SINGE_INSTALL_NAME_TOOL} CACHE FILEPATH "" FORCE)
set(CMAKE_OSX_SYSROOT "" CACHE PATH "Left empty; the wrappers name the SDK themselves" FORCE)
set(CMAKE_OSX_ARCHITECTURES "" CACHE STRING "Left empty; the zig target names the architecture" FORCE)
set(CMAKE_FIND_ROOT_PATH ${SINGE_MACOS_SDK})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(KANGAROO_OS macos)
set(KANGAROO_ARCH ${SINGE_MACOS_ARCH})

View file

@ -0,0 +1,38 @@
#
# Singe 3
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public Licens
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
# Joins the Apple silicon and Intel Singe binaries into one universal binary with llvm-lipo.
# Run as: cmake -P cmake/zig/macosUniversal.cmake (after both macOS presets have built)
get_filename_component(buildDir ${CMAKE_CURRENT_LIST_DIR}/../../.builddir ABSOLUTE)
file(GLOB armBinary ${buildDir}/Singe-v*-Macos-aarch64)
file(GLOB intelBinary ${buildDir}/Singe-v*-Macos-x86_64)
if(NOT armBinary OR NOT intelBinary)
message(FATAL_ERROR "Build the macos-aarch64 and macos-x86_64 presets first.")
endif()
string(REPLACE "-aarch64" "-universal" universalBinary ${armBinary})
find_program(SINGE_LIPO NAMES llvm-lipo llvm-lipo-21 llvm-lipo-20 llvm-lipo-19 llvm-lipo-18 llvm-lipo-17 llvm-lipo-16 llvm-lipo-15)
if(NOT SINGE_LIPO)
message(FATAL_ERROR "llvm-lipo is needed to make the universal binary (apt-get install llvm).")
endif()
execute_process(COMMAND ${SINGE_LIPO} -create ${armBinary} ${intelBinary} -output ${universalBinary} RESULT_VARIABLE code)
if(NOT code EQUAL 0)
message(FATAL_ERROR "llvm-lipo failed.")
endif()
message(STATUS "Wrote ${universalBinary}")

View file

@ -0,0 +1,24 @@
#
# Singe 3
# Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public Licens
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
# macOS on Intel; everything is in macos.cmake.
set(SINGE_MACOS_ARCH x86_64)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
include(${CMAKE_CURRENT_LIST_DIR}/macos.cmake)

File diff suppressed because it is too large Load diff

View file

@ -298,6 +298,7 @@ static void _launcher(const char *exeName, ConfigT *conf) {
float bestRatio = HUGE_VALF;
SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
SDL_GPUDevice *device = NULL;
SDL_Surface *icon = NULL;
MIX_Mixer *mixer = NULL;
const SDL_DisplayMode *mode = NULL;
@ -402,12 +403,27 @@ static void _launcher(const char *exeName, ConfigT *conf) {
SDL_SyncWindow(window);
}
// Create a renderer. SDL prefers accelerated drivers but can fall back to software.
// Create a renderer. On a GPU device when the platform has one, so the 3D scene can share it;
// otherwise SDL's ordinary renderer, which prefers accelerated drivers but can fall back to software.
_mainTrace(conf, "Creating renderer");
renderer = SDL_CreateRenderer(window, NULL);
if (renderer == NULL) {
utilDie("%s", SDL_GetError());
device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL, false, NULL);
if (device == NULL) {
_mainTrace(conf, "No GPU device (%s); 3D is unavailable", SDL_GetError());
} else {
renderer = SDL_CreateGPURenderer(device, window);
if (renderer == NULL) {
_mainTrace(conf, "GPU renderer failed (%s); 3D is unavailable", SDL_GetError());
SDL_DestroyGPUDevice(device);
device = NULL;
}
}
if (renderer == NULL) {
renderer = SDL_CreateRenderer(window, NULL);
if (renderer == NULL) {
utilDie("%s", SDL_GetError());
}
}
_mainTrace(conf, "Renderer: %s", SDL_GetRendererName(renderer));
// Clear screen with black
SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
@ -435,7 +451,7 @@ static void _launcher(const char *exeName, ConfigT *conf) {
// Run Singe!
_mainTrace(conf, "Starting Singe");
singe(window, renderer, conf);
singe(window, renderer, device, conf);
// Shutdown - framefiles own video handles, so they go first.
_mainTrace(conf, "Shutting down laserdisc framefile handler");
@ -446,6 +462,9 @@ static void _launcher(const char *exeName, ConfigT *conf) {
MIX_DestroyMixer(mixer);
_mainTrace(conf, "Destroying renderer");
SDL_DestroyRenderer(renderer);
if (device != NULL) {
SDL_DestroyGPUDevice(device);
}
_mainTrace(conf, "Destroying window");
SDL_DestroyWindow(window);
_mainTrace(conf, "Re-enabling screen saver");

499
src/math3d.c Normal file
View file

@ -0,0 +1,499 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
// Vector, quaternion and matrix arithmetic for the 3D scene. Small on purpose: only what a scene
// graph, a camera and glTF animation need.
#include <math.h>
#include <string.h>
#include "math3d.h"
#define PI 3.14159265358979323846f
#define DEGREES_TO_RADIANS(d) ((d) * (PI / 180.0f))
#define RADIANS_TO_DEGREES(r) ((r) * (180.0f / PI))
#define EPSILON 1e-6f
// Translation * rotation * scale, the glTF node transform.
Mat4T mat4Compose(Vec3T translation, QuatT rotation, Vec3T scale) {
Mat4T out;
float xx = rotation.x * rotation.x;
float yy = rotation.y * rotation.y;
float zz = rotation.z * rotation.z;
float xy = rotation.x * rotation.y;
float xz = rotation.x * rotation.z;
float yz = rotation.y * rotation.z;
float wx = rotation.w * rotation.x;
float wy = rotation.w * rotation.y;
float wz = rotation.w * rotation.z;
out.m[0] = (1.0f - 2.0f * (yy + zz)) * scale.x;
out.m[1] = (2.0f * (xy + wz)) * scale.x;
out.m[2] = (2.0f * (xz - wy)) * scale.x;
out.m[3] = 0.0f;
out.m[4] = (2.0f * (xy - wz)) * scale.y;
out.m[5] = (1.0f - 2.0f * (xx + zz)) * scale.y;
out.m[6] = (2.0f * (yz + wx)) * scale.y;
out.m[7] = 0.0f;
out.m[8] = (2.0f * (xz + wy)) * scale.z;
out.m[9] = (2.0f * (yz - wx)) * scale.z;
out.m[10] = (1.0f - 2.0f * (xx + yy)) * scale.z;
out.m[11] = 0.0f;
out.m[12] = translation.x;
out.m[13] = translation.y;
out.m[14] = translation.z;
out.m[15] = 1.0f;
return out;
}
// Splits a TRS matrix back into its parts (glTF nodes may carry a matrix instead of TRS).
void mat4Decompose(Mat4T a, Vec3T *translation, QuatT *rotation, Vec3T *scale) {
Vec3T x = vec3(a.m[0], a.m[1], a.m[2]);
Vec3T y = vec3(a.m[4], a.m[5], a.m[6]);
Vec3T z = vec3(a.m[8], a.m[9], a.m[10]);
Mat4T r = mat4Identity();
*translation = vec3(a.m[12], a.m[13], a.m[14]);
*scale = vec3(vec3Length(x), vec3Length(y), vec3Length(z));
// A negative determinant means one axis is mirrored; put the flip on X.
if (vec3Dot(vec3Cross(x, y), z) < 0.0f) {
scale->x = -scale->x;
}
x = vec3Scale(x, (scale->x != 0.0f) ? 1.0f / scale->x : 0.0f);
y = vec3Scale(y, (scale->y != 0.0f) ? 1.0f / scale->y : 0.0f);
z = vec3Scale(z, (scale->z != 0.0f) ? 1.0f / scale->z : 0.0f);
r.m[0] = x.x;
r.m[1] = x.y;
r.m[2] = x.z;
r.m[4] = y.x;
r.m[5] = y.y;
r.m[6] = y.z;
r.m[8] = z.x;
r.m[9] = z.y;
r.m[10] = z.z;
*rotation = quatFromMat4(r);
}
Mat4T mat4Identity(void) {
Mat4T out;
memset(&out, 0, sizeof(out));
out.m[0] = 1.0f;
out.m[5] = 1.0f;
out.m[10] = 1.0f;
out.m[15] = 1.0f;
return out;
}
// General 4x4 inverse by cofactors. Returns false for a singular matrix (out untouched).
bool mat4Invert(Mat4T a, Mat4T *out) {
float inv[16];
float det;
int32_t x;
const float *m = a.m;
inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10];
inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10];
inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9];
inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9];
inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10];
inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10];
inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9];
inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9];
inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6];
inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6];
inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5];
inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5];
inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6];
inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6];
inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5];
inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5];
det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
if (fabsf(det) < EPSILON) {
return false;
}
det = 1.0f / det;
for (x = 0; x < 16; x++) {
out->m[x] = inv[x] * det;
}
return true;
}
// A view matrix: the camera at eye looking at target.
Mat4T mat4LookAt(Vec3T eye, Vec3T target, Vec3T up) {
Mat4T out;
Vec3T f = vec3Normalize(vec3Subtract(target, eye));
Vec3T s = vec3Normalize(vec3Cross(f, up));
Vec3T u = vec3Cross(s, f);
out.m[0] = s.x;
out.m[1] = u.x;
out.m[2] = -f.x;
out.m[3] = 0.0f;
out.m[4] = s.y;
out.m[5] = u.y;
out.m[6] = -f.y;
out.m[7] = 0.0f;
out.m[8] = s.z;
out.m[9] = u.z;
out.m[10] = -f.z;
out.m[11] = 0.0f;
out.m[12] = -vec3Dot(s, eye);
out.m[13] = -vec3Dot(u, eye);
out.m[14] = vec3Dot(f, eye);
out.m[15] = 1.0f;
return out;
}
// a * b: applies b first, then a.
Mat4T mat4Multiply(Mat4T a, Mat4T b) {
Mat4T out;
int32_t column;
int32_t row;
int32_t k;
float sum;
for (column = 0; column < 4; column++) {
for (row = 0; row < 4; row++) {
sum = 0.0f;
for (k = 0; k < 4; k++) {
sum += a.m[k * 4 + row] * b.m[column * 4 + k];
}
out.m[column * 4 + row] = sum;
}
}
return out;
}
// Depth maps to 0..1 (what SDL_GPU expects on every backend).
Mat4T mat4Orthographic(float width, float height, float near, float far) {
Mat4T out;
memset(&out, 0, sizeof(out));
out.m[0] = 2.0f / width;
out.m[5] = 2.0f / height;
out.m[10] = -1.0f / (far - near);
out.m[14] = -near / (far - near);
out.m[15] = 1.0f;
return out;
}
Mat4T mat4Perspective(float fovDegrees, float aspect, float near, float far) {
Mat4T out;
float f = 1.0f / tanf(DEGREES_TO_RADIANS(fovDegrees) / 2.0f);
memset(&out, 0, sizeof(out));
out.m[0] = f / aspect;
out.m[5] = f;
out.m[10] = far / (near - far);
out.m[11] = -1.0f;
out.m[14] = (near * far) / (near - far);
return out;
}
Vec3T mat4TransformPoint(Mat4T a, Vec3T p) {
Vec3T out;
float w = a.m[3] * p.x + a.m[7] * p.y + a.m[11] * p.z + a.m[15];
out.x = a.m[0] * p.x + a.m[4] * p.y + a.m[8] * p.z + a.m[12];
out.y = a.m[1] * p.x + a.m[5] * p.y + a.m[9] * p.z + a.m[13];
out.z = a.m[2] * p.x + a.m[6] * p.y + a.m[10] * p.z + a.m[14];
if (fabsf(w) > EPSILON) {
out.x /= w;
out.y /= w;
out.z /= w;
}
return out;
}
// Directions ignore translation.
Vec3T mat4TransformVector(Mat4T a, Vec3T v) {
Vec3T out;
out.x = a.m[0] * v.x + a.m[4] * v.y + a.m[8] * v.z;
out.y = a.m[1] * v.x + a.m[5] * v.y + a.m[9] * v.z;
out.z = a.m[2] * v.x + a.m[6] * v.y + a.m[10] * v.z;
return out;
}
Mat4T mat4Transpose(Mat4T a) {
Mat4T out;
int32_t column;
int32_t row;
for (column = 0; column < 4; column++) {
for (row = 0; row < 4; row++) {
out.m[column * 4 + row] = a.m[row * 4 + column];
}
}
return out;
}
QuatT quatFromAxisAngle(Vec3T axis, float degrees) {
QuatT out;
float half = DEGREES_TO_RADIANS(degrees) / 2.0f;
float s = sinf(half);
Vec3T n = vec3Normalize(axis);
out.x = n.x * s;
out.y = n.y * s;
out.z = n.z * s;
out.w = cosf(half);
return out;
}
// Intrinsic rotations applied in the order Y (yaw), X (pitch), Z (roll), matching what a script
// means by "turn, then tilt, then bank".
QuatT quatFromEuler(float xDegrees, float yDegrees, float zDegrees) {
QuatT qx = quatFromAxisAngle(vec3(1.0f, 0.0f, 0.0f), xDegrees);
QuatT qy = quatFromAxisAngle(vec3(0.0f, 1.0f, 0.0f), yDegrees);
QuatT qz = quatFromAxisAngle(vec3(0.0f, 0.0f, 1.0f), zDegrees);
return quatMultiply(quatMultiply(qy, qx), qz);
}
// The rotation part of a matrix, assuming no scale.
QuatT quatFromMat4(Mat4T a) {
QuatT out;
float trace = a.m[0] + a.m[5] + a.m[10];
float s;
if (trace > 0.0f) {
s = sqrtf(trace + 1.0f) * 2.0f;
out.w = 0.25f * s;
out.x = (a.m[6] - a.m[9]) / s;
out.y = (a.m[8] - a.m[2]) / s;
out.z = (a.m[1] - a.m[4]) / s;
} else if ((a.m[0] > a.m[5]) && (a.m[0] > a.m[10])) {
s = sqrtf(1.0f + a.m[0] - a.m[5] - a.m[10]) * 2.0f;
out.w = (a.m[6] - a.m[9]) / s;
out.x = 0.25f * s;
out.y = (a.m[4] + a.m[1]) / s;
out.z = (a.m[8] + a.m[2]) / s;
} else if (a.m[5] > a.m[10]) {
s = sqrtf(1.0f + a.m[5] - a.m[0] - a.m[10]) * 2.0f;
out.w = (a.m[8] - a.m[2]) / s;
out.x = (a.m[4] + a.m[1]) / s;
out.y = 0.25f * s;
out.z = (a.m[9] + a.m[6]) / s;
} else {
s = sqrtf(1.0f + a.m[10] - a.m[0] - a.m[5]) * 2.0f;
out.w = (a.m[1] - a.m[4]) / s;
out.x = (a.m[8] + a.m[2]) / s;
out.y = (a.m[9] + a.m[6]) / s;
out.z = 0.25f * s;
}
return quatNormalize(out);
}
QuatT quatIdentity(void) {
QuatT out = { 0.0f, 0.0f, 0.0f, 1.0f };
return out;
}
// The rotation that points -Z along forward with +Y near up.
QuatT quatLookRotation(Vec3T forward, Vec3T up) {
Mat4T m;
Vec3T f = vec3Normalize(forward);
Vec3T s;
Vec3T u;
if (vec3Length(vec3Cross(f, up)) < EPSILON) {
// Looking straight along up: pick any perpendicular.
up = (fabsf(f.y) < 0.9f) ? vec3(0.0f, 1.0f, 0.0f) : vec3(0.0f, 0.0f, 1.0f);
}
s = vec3Normalize(vec3Cross(f, up));
u = vec3Cross(s, f);
m = mat4Identity();
m.m[0] = s.x;
m.m[1] = s.y;
m.m[2] = s.z;
m.m[4] = u.x;
m.m[5] = u.y;
m.m[6] = u.z;
m.m[8] = -f.x;
m.m[9] = -f.y;
m.m[10] = -f.z;
return quatFromMat4(m);
}
// a * b: applies b first, then a.
QuatT quatMultiply(QuatT a, QuatT b) {
QuatT out;
out.x = a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y;
out.y = a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x;
out.z = a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w;
out.w = a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z;
return out;
}
QuatT quatNormalize(QuatT q) {
float length = sqrtf(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
if (length < EPSILON) {
return quatIdentity();
}
q.x /= length;
q.y /= length;
q.z /= length;
q.w /= length;
return q;
}
Vec3T quatRotate(QuatT q, Vec3T v) {
Vec3T u = vec3(q.x, q.y, q.z);
Vec3T t = vec3Scale(vec3Cross(u, v), 2.0f);
return vec3Add(vec3Add(v, vec3Scale(t, q.w)), vec3Cross(u, t));
}
QuatT quatSlerp(QuatT a, QuatT b, float t) {
QuatT out;
float cosTheta = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
float theta;
float sinTheta;
float wa;
float wb;
// Take the short way round.
if (cosTheta < 0.0f) {
b.x = -b.x;
b.y = -b.y;
b.z = -b.z;
b.w = -b.w;
cosTheta = -cosTheta;
}
if (cosTheta > 1.0f - EPSILON) {
// Nearly parallel: lerp is accurate and avoids the division.
out.x = a.x + (b.x - a.x) * t;
out.y = a.y + (b.y - a.y) * t;
out.z = a.z + (b.z - a.z) * t;
out.w = a.w + (b.w - a.w) * t;
return quatNormalize(out);
}
theta = acosf(cosTheta);
sinTheta = sinf(theta);
wa = sinf((1.0f - t) * theta) / sinTheta;
wb = sinf(t * theta) / sinTheta;
out.x = a.x * wa + b.x * wb;
out.y = a.y * wa + b.y * wb;
out.z = a.z * wa + b.z * wb;
out.w = a.w * wa + b.w * wb;
return out;
}
// The inverse of quatFromEuler (Y, then X, then Z).
void quatToEuler(QuatT q, float *xDegrees, float *yDegrees, float *zDegrees) {
Mat4T m = mat4Compose(vec3(0.0f, 0.0f, 0.0f), q, vec3(1.0f, 1.0f, 1.0f));
float sinX = -m.m[9];
if (sinX > 1.0f) {
sinX = 1.0f;
}
if (sinX < -1.0f) {
sinX = -1.0f;
}
*xDegrees = RADIANS_TO_DEGREES(asinf(sinX));
if (fabsf(sinX) < 1.0f - EPSILON) {
*yDegrees = RADIANS_TO_DEGREES(atan2f(m.m[8], m.m[10]));
*zDegrees = RADIANS_TO_DEGREES(atan2f(m.m[1], m.m[5]));
} else {
// Gimbal lock: give all the twist to Y.
*yDegrees = RADIANS_TO_DEGREES(atan2f(-m.m[2], m.m[0]));
*zDegrees = 0.0f;
}
}
Vec3T vec3(float x, float y, float z) {
Vec3T out = { x, y, z };
return out;
}
Vec3T vec3Add(Vec3T a, Vec3T b) {
return vec3(a.x + b.x, a.y + b.y, a.z + b.z);
}
Vec3T vec3Cross(Vec3T a, Vec3T b) {
return vec3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
}
float vec3Dot(Vec3T a, Vec3T b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
float vec3Length(Vec3T a) {
return sqrtf(vec3Dot(a, a));
}
Vec3T vec3Lerp(Vec3T a, Vec3T b, float t) {
return vec3Add(a, vec3Scale(vec3Subtract(b, a), t));
}
Vec3T vec3Normalize(Vec3T a) {
float length = vec3Length(a);
if (length < EPSILON) {
return vec3(0.0f, 0.0f, 0.0f);
}
return vec3Scale(a, 1.0f / length);
}
Vec3T vec3Scale(Vec3T a, float s) {
return vec3(a.x * s, a.y * s, a.z * s);
}
Vec3T vec3Subtract(Vec3T a, Vec3T b) {
return vec3(a.x - b.x, a.y - b.y, a.z - b.z);
}

90
src/math3d.h Normal file
View file

@ -0,0 +1,90 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#ifndef MATH3D_H
#define MATH3D_H
#include <stdbool.h>
#include <stdint.h>
// Right-handed, +Y up, -Z forward (glTF's convention). Matrices are column major, as the GPU
// expects them; m[column * 4 + row].
typedef struct Vec3S {
float x;
float y;
float z;
} Vec3T;
typedef struct Vec4S {
float x;
float y;
float z;
float w;
} Vec4T;
typedef struct QuatS {
float x;
float y;
float z;
float w;
} QuatT;
typedef struct Mat4S {
float m[16];
} Mat4T;
Mat4T mat4Compose(Vec3T translation, QuatT rotation, Vec3T scale);
void mat4Decompose(Mat4T a, Vec3T *translation, QuatT *rotation, Vec3T *scale);
Mat4T mat4Identity(void);
bool mat4Invert(Mat4T a, Mat4T *out);
Mat4T mat4LookAt(Vec3T eye, Vec3T target, Vec3T up);
Mat4T mat4Multiply(Mat4T a, Mat4T b);
Mat4T mat4Orthographic(float width, float height, float near, float far);
Mat4T mat4Perspective(float fovDegrees, float aspect, float near, float far);
Vec3T mat4TransformPoint(Mat4T a, Vec3T p);
Vec3T mat4TransformVector(Mat4T a, Vec3T v);
Mat4T mat4Transpose(Mat4T a);
QuatT quatFromAxisAngle(Vec3T axis, float degrees);
QuatT quatFromEuler(float xDegrees, float yDegrees, float zDegrees);
QuatT quatFromMat4(Mat4T a);
QuatT quatIdentity(void);
QuatT quatLookRotation(Vec3T forward, Vec3T up);
QuatT quatMultiply(QuatT a, QuatT b);
QuatT quatNormalize(QuatT q);
Vec3T quatRotate(QuatT q, Vec3T v);
QuatT quatSlerp(QuatT a, QuatT b, float t);
void quatToEuler(QuatT q, float *xDegrees, float *yDegrees, float *zDegrees);
Vec3T vec3(float x, float y, float z);
Vec3T vec3Add(Vec3T a, Vec3T b);
Vec3T vec3Cross(Vec3T a, Vec3T b);
float vec3Dot(Vec3T a, Vec3T b);
float vec3Length(Vec3T a);
Vec3T vec3Lerp(Vec3T a, Vec3T b, float t);
Vec3T vec3Normalize(Vec3T a);
Vec3T vec3Scale(Vec3T a, float s);
Vec3T vec3Subtract(Vec3T a, Vec3T b);
#endif

1018
src/model.c Normal file

File diff suppressed because it is too large Load diff

50
src/model.h Normal file
View file

@ -0,0 +1,50 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#ifndef MODEL_H
#define MODEL_H
#include <stdbool.h>
#include <stdint.h>
double animationGetTime(int32_t root);
bool animationIsPlaying(int32_t root);
bool animationPause(int32_t root);
bool animationPlay(int32_t root, int32_t index, bool loop, float speed);
bool animationResume(int32_t root);
bool animationSetTime(int32_t root, double seconds);
bool animationStop(int32_t root);
int32_t modelAnimationIndex(int32_t model, const char *name);
bool modelDelete(int32_t model);
int32_t modelGetAnimationCount(int32_t model);
const char *modelGetAnimationName(int32_t model, int32_t index);
int32_t modelInstance(int32_t model, int32_t parent);
const char *modelLastError(void);
int32_t modelLoad(const char *name);
void modelQuit(void);
int32_t modelRootOf(int32_t root);
void modelUpdate(bool advance);
bool modelValid(int32_t model);
#endif

2266
src/scene.c Normal file

File diff suppressed because it is too large Load diff

130
src/scene.h Normal file
View file

@ -0,0 +1,130 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#ifndef SCENE_H
#define SCENE_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL3/SDL.h>
#include "math3d.h"
#define SCENE_ROOT_NODE 0
// A vertex as the GPU sees it; model loaders fill these directly.
typedef struct SceneVertexS {
float position[3];
float normal[3];
float uv[2];
uint8_t joints[4];
float weights[4];
} SceneVertexT;
// Hands back a video player's current texture (NULL when it has none), for materialSetVideo.
typedef SDL_Texture *(*SceneVideoSourceFn)(int32_t player);
typedef enum LightTypeE {
LIGHT_DIRECTIONAL = 0,
LIGHT_POINT = 1,
LIGHT_SPOT = 2
} LightTypeE;
bool sceneAvailable(void);
bool sceneEnable(bool enabled);
void sceneGetSize(int32_t *width, int32_t *height);
bool sceneInit(SDL_GPUDevice *device, SDL_Renderer *renderer);
bool sceneIsEnabled(void);
bool sceneProject(Vec3T world, float *x, float *y, float *depth);
void sceneQuit(void);
SDL_Texture *sceneRender(void);
bool sceneResize(int32_t width, int32_t height);
void sceneSetAntialias(bool antialias);
void sceneSetAmbient(uint8_t r, uint8_t g, uint8_t b);
void sceneComputeNormals(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
void sceneSetBackground(uint8_t r, uint8_t g, uint8_t b, uint8_t a);
Vec3T sceneUnproject(float x, float y, float distance);
void sceneUpdateVideo(SceneVideoSourceFn source);
bool cameraSet(int32_t node);
void cameraSetOrthographic(float height, float near, float far);
void cameraSetPerspective(float fovDegrees, float near, float far);
bool lightAttach(int32_t node, LightTypeE type);
int32_t lightNew(LightTypeE type, int32_t parent);
bool lightSetColor(int32_t node, uint8_t r, uint8_t g, uint8_t b);
bool lightSetCone(int32_t node, float innerDegrees, float outerDegrees);
bool lightSetIntensity(int32_t node, float intensity);
bool lightSetRange(int32_t node, float range);
bool materialDelete(int32_t material);
int32_t materialNew(void);
bool materialSetBlend(int32_t material, bool blend);
bool materialSetColor(int32_t material, uint8_t r, uint8_t g, uint8_t b, uint8_t a);
bool materialSetDoubleSided(int32_t material, bool doubleSided);
bool materialSetEmissive(int32_t material, uint8_t r, uint8_t g, uint8_t b);
bool materialSetMetallic(int32_t material, float metallic);
bool materialSetRoughness(int32_t material, float roughness);
bool materialSetTexture(int32_t material, SDL_Surface *image);
bool materialSetUnlit(int32_t material, bool unlit);
bool materialSetVideo(int32_t material, int32_t player);
bool materialValid(int32_t material);
int32_t meshBox(float width, float height, float depth);
int32_t meshCone(float radius, float height, int32_t segments);
int32_t meshCylinder(float radius, float height, int32_t segments);
bool meshDelete(int32_t mesh);
int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned);
int32_t meshPlane(float width, float depth);
int32_t meshSphere(float radius, int32_t segments);
int32_t meshTorus(float radius, float tubeRadius, int32_t segments);
bool meshValid(int32_t mesh);
bool nodeDelete(int32_t node);
int32_t nodeFind(int32_t root, const char *name);
int32_t nodeGetChild(int32_t node, int32_t index);
int32_t nodeGetChildCount(int32_t node);
uint32_t nodeGetGeneration(int32_t node);
const char *nodeGetName(int32_t node);
int32_t nodeGetParent(int32_t node);
Vec3T nodeGetPosition(int32_t node);
QuatT nodeGetRotation(int32_t node);
Vec3T nodeGetScale(int32_t node);
Vec3T nodeGetWorldPosition(int32_t node);
bool nodeLookAt(int32_t node, Vec3T target);
bool nodeMove(int32_t node, Vec3T delta);
int32_t nodeNew(int32_t parent);
bool nodeRotate(int32_t node, QuatT delta);
bool nodeSetMesh(int32_t node, int32_t mesh, int32_t material);
bool nodeSetName(int32_t node, const char *name);
bool nodeSetParent(int32_t node, int32_t parent);
bool nodeSetPosition(int32_t node, Vec3T position);
bool nodeSetRotation(int32_t node, QuatT rotation);
bool nodeSetScale(int32_t node, Vec3T scale);
bool nodeSetSkin(int32_t node, const int32_t *joints, const Mat4T *inverseBind, int32_t count);
bool nodeSetVisible(int32_t node, bool visible);
bool nodeValid(int32_t node);
#endif

61
src/shaders/build.sh Executable file
View file

@ -0,0 +1,61 @@
#!/bin/bash
#
# Compiles scene.hlsl into sceneShaders.h: SPIR-V (Vulkan), DXIL (Direct3D 12) and MSL (Metal)
# for each entry point, as C arrays. Needs SDL_shadercross's command line tool (built from
# https://github.com/libsdl-org/SDL_shadercross with SDLSHADERCROSS_DXC=ON); pass its path as the
# first argument or put it on PATH. The engine build never runs this: the output is checked in.
#
# Usage: src/shaders/build.sh [path/to/shadercross]
set -euo pipefail
cd "$(dirname "$0")"
SHADERCROSS=${1:-shadercross}
OUT=sceneShaders.h
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
ENTRIES="vertexStatic:vertex vertexSkinned:vertex fragmentMain:fragment"
emit() {
# emit NAME FILE: a C array from a binary file
local name=$1
local file=$2
echo "static const unsigned char ${name}[] = {"
od -An -v -tx1 "$file" | sed 's/ \([0-9a-f][0-9a-f]\)/0x\1,/g; s/^/\t/'
echo "};"
}
{
echo "// Generated by build.sh from scene.hlsl with SDL_shadercross; do not edit."
echo "// SPIR-V for Vulkan, DXIL for Direct3D 12, MSL for Metal, one set per entry point."
echo
echo "#ifndef SCENE_SHADERS_H"
echo "#define SCENE_SHADERS_H"
echo
echo "#include <stddef.h>"
echo
echo "typedef struct SceneShaderS {"
echo " const char *entryPoint;"
echo " const unsigned char *spirv;"
echo " size_t spirvSize;"
echo " const unsigned char *dxil;"
echo " size_t dxilSize;"
echo " const unsigned char *msl;"
echo " size_t mslSize;"
echo "} SceneShaderT;"
echo
for entry in $ENTRIES; do
name=${entry%%:*}
stage=${entry##*:}
for format in SPIRV DXIL MSL; do
ext=$(echo "$format" | tr '[:upper:]' '[:lower:]')
"$SHADERCROSS" scene.hlsl -s HLSL -d "$format" -t "$stage" -e "$name" -o "$TMP/$name.$ext" >&2
emit "_${name}${format}" "$TMP/$name.$ext"
echo
done
echo "static const SceneShaderT sceneShader$(echo "${name:0:1}" | tr '[:lower:]' '[:upper:]')${name:1} = { \"$name\", _${name}SPIRV, sizeof(_${name}SPIRV), _${name}DXIL, sizeof(_${name}DXIL), _${name}MSL, sizeof(_${name}MSL) };"
echo
done
echo "#endif"
} > "$OUT"
echo "Wrote $OUT"

166
src/shaders/scene.hlsl Normal file
View file

@ -0,0 +1,166 @@
// Singe 3 scene shaders: one vertex shader for static meshes, one for skinned meshes, one
// fragment shader for both. Compiled offline by src/shaders/build.sh (SDL_shadercross) into the
// SPIR-V, DXIL and MSL blobs in sceneShaders.h; the engine build never compiles shaders.
//
// Resource bindings follow SDL_GPU's HLSL convention: vertex uniforms in space1, fragment
// textures and samplers in space2, fragment uniforms in space3.
#define MAX_LIGHTS 8
#define MAX_JOINTS 128
#define LIGHT_DIRECTIONAL 0
#define LIGHT_POINT 1
#define LIGHT_SPOT 2
// ----- Vertex -----
cbuffer DrawUniforms : register(b0, space1) {
float4x4 modelViewProjection;
float4x4 model;
float4x4 normalMatrix; // Inverse transpose of model, for normals under non-uniform scale
};
cbuffer SkinUniforms : register(b1, space1) {
float4x4 joints[MAX_JOINTS];
};
struct VertexInput {
float3 position : TEXCOORD0;
float3 normal : TEXCOORD1;
float2 uv : TEXCOORD2;
uint4 joints : TEXCOORD3;
float4 weights : TEXCOORD4;
};
struct VertexOutput {
float4 position : SV_Position;
float3 worldPosition : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float2 uv : TEXCOORD2;
};
VertexOutput vertexStatic(VertexInput input) {
VertexOutput output;
output.position = mul(modelViewProjection, float4(input.position, 1.0));
output.worldPosition = mul(model, float4(input.position, 1.0)).xyz;
output.worldNormal = normalize(mul((float3x3)normalMatrix, input.normal));
output.uv = input.uv;
return output;
}
VertexOutput vertexSkinned(VertexInput input) {
VertexOutput output;
float4 position = float4(input.position, 1.0);
float4 skinned;
float3 skinnedNormal;
// The weighted sum of the joint transforms, applied to the position and the normal.
skinned = mul(joints[input.joints.x], position) * input.weights.x
+ mul(joints[input.joints.y], position) * input.weights.y
+ mul(joints[input.joints.z], position) * input.weights.z
+ mul(joints[input.joints.w], position) * input.weights.w;
skinnedNormal = mul((float3x3)joints[input.joints.x], input.normal) * input.weights.x
+ mul((float3x3)joints[input.joints.y], input.normal) * input.weights.y
+ mul((float3x3)joints[input.joints.z], input.normal) * input.weights.z
+ mul((float3x3)joints[input.joints.w], input.normal) * input.weights.w;
output.position = mul(modelViewProjection, skinned);
output.worldPosition = mul(model, skinned).xyz;
output.worldNormal = normalize(mul((float3x3)normalMatrix, skinnedNormal));
output.uv = input.uv;
return output;
}
// ----- Fragment -----
struct Light {
float4 positionType; // xyz position (point, spot) or unused; w = type
float4 directionRange; // xyz direction the light shines in (directional, spot); w = range (0 = infinite)
float4 color; // rgb already multiplied by intensity
float4 cone; // x = cos(inner), y = cos(outer)
};
cbuffer FragmentUniforms : register(b0, space3) {
float4 cameraPosition;
float4 ambient;
float4 baseColor;
float4 emissive;
float4 material; // x = metallic, y = roughness, z = unlit (1/0), w = textured (1/0)
float4 counts; // x = light count
Light lights[MAX_LIGHTS];
};
Texture2D<float4> baseTexture : register(t0, space2);
SamplerState baseSampler : register(s0, space2);
// Named fragmentMain because "fragment" is a keyword in Metal and the MSL entry point keeps this name.
float4 fragmentMain(VertexOutput input) : SV_Target {
float4 albedo = baseColor;
float3 normal;
float3 view;
float3 diffuseColor;
float3 specularColor;
float3 result;
float shininess;
float metallic = material.x;
float roughness = material.y;
int lightCount;
int x;
if (material.w > 0.5) {
albedo *= baseTexture.Sample(baseSampler, input.uv);
}
if (material.z > 0.5) {
return albedo;
}
// Metallic surfaces reflect their own colour and have little diffuse; dielectrics reflect a
// little white. Roughness sets how tight the highlight is. Without an environment to
// reflect, metals would go black away from highlights, so they keep some diffuse and get a
// stronger ambient term standing in for reflections.
normal = normalize(input.worldNormal);
view = normalize(cameraPosition.xyz - input.worldPosition);
diffuseColor = albedo.rgb * (1.0 - 0.6 * metallic);
specularColor = lerp(float3(0.04, 0.04, 0.04), albedo.rgb, metallic) * (1.0 + 2.0 * metallic);
shininess = lerp(256.0, 4.0, roughness * roughness);
result = ambient.rgb * albedo.rgb * (1.0 + 2.0 * metallic) + emissive.rgb;
lightCount = (int)counts.x;
for (x = 0; x < MAX_LIGHTS; x++) {
float3 toLight;
float attenuation = 1.0;
float distance;
float diffuse;
float specular;
float3 halfway;
// A break here crashes DXC; continue costs nothing worth measuring.
if (x >= lightCount) {
continue;
}
if (lights[x].positionType.w == LIGHT_DIRECTIONAL) {
toLight = normalize(-lights[x].directionRange.xyz);
} else {
toLight = lights[x].positionType.xyz - input.worldPosition;
distance = length(toLight);
toLight = toLight / max(distance, 0.0001);
attenuation = 1.0 / (1.0 + distance * distance);
if (lights[x].directionRange.w > 0.0) {
// Fade to nothing at the range so lights do not pop.
attenuation *= saturate(1.0 - pow(distance / lights[x].directionRange.w, 4.0));
}
if (lights[x].positionType.w == LIGHT_SPOT) {
float cosAngle = dot(-toLight, normalize(lights[x].directionRange.xyz));
attenuation *= smoothstep(lights[x].cone.y, lights[x].cone.x, cosAngle);
}
}
diffuse = saturate(dot(normal, toLight));
halfway = normalize(toLight + view);
specular = pow(saturate(dot(normal, halfway)), shininess) * (1.0 - roughness * 0.5);
result += (diffuseColor * diffuse + specularColor * specular * diffuse) * lights[x].color.rgb * attenuation;
}
return float4(result, albedo.a);
}

2992
src/shaders/sceneShaders.h Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -93,7 +93,7 @@ typedef struct ConfigS {
ConfigT *confFromDatabase(const ConfigT *conf);
void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf);
void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, ConfigT *conf);
#endif // SINGE_H

7
thirdparty/cgltf/LICENSE vendored Normal file
View file

@ -0,0 +1,7 @@
Copyright (c) 2018-2021 Johannes Kuhlmann
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

7175
thirdparty/cgltf/cgltf.h vendored Normal file

File diff suppressed because it is too large Load diff