singe/CHANGELOG
2026-09-12 22:36:00 -05:00

1510 lines
75 KiB
Text

SINGE 3.00
==========
Unreleased
API Changes
-----------
- --gamedir names the directory holding the games, the .game files and
the Singe support folder, so the executable can live anywhere. It
becomes the working directory; relative names count from it. The
Menu.sh the engine writes there runs the executable by its full path.
- Without --datadir the data directory is now data/ in the game
directory (2.x wrote beside the game, which may be read only). The
menu's Menu.sh no longer needs to pass -d data.
- math.randomseed accepts fractional seeds (floored), so 2.x games that
seed with os.clock() products run under Lua 5.4.
- A script launched by name (or by its directory) runs with its games.dat
entry, as the menu would run it: Singe finds the games.dat in the
script's directory or up to three above, and applies the entry whose
SCRIPT names the script, LEGACY_SPRITE_ARGS included. Options typed on
the command line keep their values. Every game in a library therefore
behaves the same whether it was started from the menu or from a shell.
- The name now expands to "SINGE Is Not a Game Emulator", in the banner,
the manual, INSTALL, and the Windows file description.
- Framework.singe applies the legacy sprite argument order once even when
a game loads it more than once.
- Hypseus Singe extensions, so games written for it run unchanged:
spriteLoadFrames, spriteDrawFrame, spriteFrameWidth, spriteFrameHeight,
spriteResetColorKey, discAudioSuffix, singeSetQuitKeyEnabled (with the
Framework alias keyboardCatchQuit), vldpGetScale, vldpFocusArea,
vldpResetFocus, vldpSetMonochrome, overlaySetMonochrome,
vldpGetYUVPixel, controllerIsValid and controllerDoRumble. Sprite calls
take the handle first like the rest of 3.00; the LEGACY_SPRITE_ARGS
shim covers spriteDrawFrame and spriteResetColorKey.
- More Hypseus Singe names, almost all of them supplied by
Framework.singe over calls Singe already had: setOverlaySize and
setOverlayResolution over overlaySetResolution, rewriteStatus as a stub,
and the six music calls over the sound family. setOverlayLinearScale is
a real engine call: it smooths the overlay as the window scales it,
which is the default and what Singe has always done, or takes the
nearest pixel for crisp pixel art.
- The picture's place on screen is now one rectangle built from four
things in order: the window, --scalefactor (range widened from 50-100
to 25-100), --shiftx and --shifty (new; they move the picture inside the
room the scale left, -100 hard against one side and 100 against the
other, which is what Hypseus's own mean -- so at full scale there is no
room and a shift does nothing), and last the Sinden border, which is
therefore still a border whatever the shift and the scale did --
--scalefactor and --sindengun may now be used together. --rotate (new;
0, 90, 180 or 270 degrees)
turns the whole presentation, video, overlay, GUIs, 3D scene and
particles together, filling a portrait window on a quarter turn. Mouse
and light gun positions follow all of it, so a click lands where it is
drawn. Scripts move the scale and the rotation while the game runs with
the Hypseus extensions vldpSetScale, vldpSetRotate and vldpGetRotate.
- Bezels. --bezel names cabinet artwork in a bezels folder (--bezeldir
names another folder, --bezelflip draws the artwork in front of the
picture instead of behind it), and a Lua sidecar named after the
artwork declares the cutout the picture is drawn in, in the artwork's
own pixels: CUTOUT = { x = 200, y = 120, width = 800, height = 500 }.
With no cutout the picture keeps the window and --scalefactor,
--shiftx and --shifty place it, as before. A Sinden border goes to the
edge of the window when artwork is loaded, because the gun's camera
sees the whole screen; --sindenedge video|window overrides that.
Scripts ask mainBezelLoaded() which artwork they got and choose with
setOverlayOnTop whether the overlay sits above it.
- The arcade score panel behind the seven scoreBezel calls: credits, and
a score and a life count for two players, in a shipped RmlUi document
(Singe/scoreBezel.rml and Singe/scoreBezel.rcss) with a seven segment
look that a theme or a game may restyle. It is off until a game asks
for it with scoreBezelEnable, exactly as in Hypseus, and it sits in the
room a bezel's cutout leaves above or below the picture, or along the
bottom of the picture when there is none. Hypseus's second argument,
which chooses among its five scoreboards, is accepted and ignored.
guiDrawScreen puts a game's own GUIs out there beside it: it is
guiDraw with the rectangle measured across the whole window.
- A settings file. settings.cfg holds the options you would otherwise
type on every command line, in Lua like controls.cfg and found the same
four ways: the directory Singe was started in, above and inside the data
directory, and beside the game's script, with a later file winning key by
key. Its keys are the long option names without the dashes, so there is
no second vocabulary, and a switch takes true or false. A settings file
is a set of defaults: a games.dat entry for the game still overrides it
and the command line always does. Anything naming the game, naming a
directory, or acting before the file could be found (--framefile,
--gamedir, --datadir, --entry, --help, --apiversion, --noconsole, the
packing tools) belongs on the command line, and a key that is not an
option, is one of those, or carries a value the option will not take is
an error naming the file and the key. Singe/settings.cfg.example lists
every key with its default.
- Options a cabinet owner has been missing, none of them with a short
letter since the alphabet ran out: --screen picks the display (counting
from 1; a number too large lists the displays that are there),
--idleexit quits after so many seconds with nothing touched,
--startsilent starts muted until the first input, --monochrome starts
the disc picture in grey, --linearscale chooses the overlay's filter,
--nogamepad ignores the pads as --nomouse ignores the mice, --haptic
caps the rumble a game may ask for (0 turns it off), --keymapfile names
one controls.cfg instead of searching four places, --altaudio (and the
AUDIO_SUFFIX games.dat key) plays <base><suffix>.ogg beside the disc's
video for another language, --xratio and --yratio carry the gun
coordinate scales a Hypseus light gun game reads with ratioGetX and
ratioGetY, --fvalue carries one number to the game through getFValue,
and --apiversion prints one machine readable line for a front end and
exits. --nosound, which had been accepted and quietly ignored, now
actually mutes.
- Every controller Singe opens is now a gamepad, whatever it started life
as. SDL calls a device a gamepad only when it has a mapping for it, and
an arcade encoder board, a spinner, a wheel or a flight stick usually has
none, so such a device used to be invisible. Singe now writes a
straight-through mapping for it -- axes in order to the two sticks and
then the triggers, buttons in order to the standard names, the first hat
to the D-pad -- and opens it as an ordinary pad, so it reaches
controls.cfg, GAMEPAD_0 to GAMEPAD_3 and every controller call like any
other. A device with more controls than the gamepad model can name loses
the extras, and one whose buttons are not wired in that order will have
them in the wrong places: --program traces the mapping in SDL's own
format so it can be pasted into a gamecontrollerdb.txt and rearranged.
Singe now reads that file, from the four places it reads controls.cfg,
and it and SDL's own database both win over the mapping Singe would
write. --mapjoysticks=false turns the writing off.
- Input options for a cabinet: --gamepad_reorder says which physical pad
fills which slot (enumeration positions from 0, as Hypseus writes them),
--trigger_threshold says how far an analogue trigger must travel before
it counts as a button (a per cent of full travel, Hypseus's unit),
--joymouse lets the first pad's left stick drive the mouse cursor at
--js_range speed so a light gun game is playable on a stick-only
cabinet, --manymouse turns the many mouse mode on whatever the game asks
for so a two gun cabinet can be fixed from outside, and
--absolutes_only keeps only the devices that report an absolute
position, which is what a real gun does. controls.cfg gains DEAD_ZONES,
a dead zone for one axis at a time, and TRIGGER_THRESHOLD; both leave
every axis on DEAD_ZONE unless set, so nothing changes for anyone who
does not ask.
- SWITCH_MOUSE_DISCONNECT reaches onInputPressed when a mouse or light gun
is unplugged, so a gun game can say so instead of appearing to have
died, and joyMouseEnable, joyMouseIsEnabled, controllerGetName and
mouseGetName let a script drive and describe all of it.
- Alt-Enter switches between full screen and a window while the game runs,
as it does in Hypseus. Singe could not change the mode at all once it
had started.
- Subtitles. srtLoad reads a SubRip .srt and turns its timestamps into
disc frames at the disc's own frame rate, so a line is found again after
any search or skip; srtEnable shows them, srtPosition puts them where a
game wants (1 to 95 per cent down the picture) and srtClear takes one
off early. They are drawn through a shipped RmlUi document,
Singe/subtitle.rml with Singe/subtitle.rcss, both of which a game or a
theme may replace, over the picture and following it wherever the scale,
a shift, a bezel cutout or a rotation put it. A malformed file answers
false rather than dying. overlayBanner puts a message of its own up the
same way for about thirty frames.
- The disc picture gains three controls beside vldpSetMonochrome:
vldpFlash puts one white picture up the way a real player flashes when
it is searched, vldpSetBlend smooths the picture down its rows to take
the edges off an interlaced transfer, and vldpSetLuma brightens or
darkens it in eighths from half to one and a half without washing the
colour out. All three work on the decoded frame, cost nothing while
they are off, and follow a frame file from one segment to the next --
which vldpSetMonochrome now does too, and did not before.
- Sprites. spriteRotateFrame turns one frame of a sheet and
spriteDrawRotatedFrame draws it centred on a point, which is what
spriteDrawFrame will not do since it ignores rotation and scale by
design; spriteDrawGrid draws any rectangle of a sprite at its own size,
which Singe had no way to do at all; spriteGetFrames counts a sheet's
frames; and spriteLoadData and soundLoadData build a sprite or a sound
from bytes held in a Lua string, for a game that unpacks its own assets.
setOverlayOpacity fades the whole overlay and controllerHowMany counts
the open gamepads.
- Music has its own volume. The music family (musicLoad, musicPlay,
musicStop with a real fade, musicPause, musicResume, musicIsPlaying,
musicSetVolume, musicUnload) is now in the engine rather than shimmed
over the sound family, and carries a gain of its own on Hypseus's 0 to
128 scale: musicSetVolume moves no sound effect and soundSetVolume
moves no music. Music starts as loud as the sound effects, at whatever
--volume_nonvldp set. Ten more Hypseus names -- hypseusGetWidth,
hypseusGetHeight, takeScreenshot, mouseHowManyReal and the six
spriteAnim ones -- are aliases in Framework.singe.
- trace.txt now opens with a header a bug report can be built on: the
version, the whole command line, the operating system, the processor,
the renderer and GPU backend that were actually chosen, how video will
be decoded, which audio decoders the build carries, the SDL versions
built and linked against, the settings that were applied and the game. It sits between two rules so it can be
pasted whole.
- lfs.dir, lfs.attributes and lfs.symlinkattributes see inside a packed
game (the union of its loose directory, its data overlay and the
database), and lfs.mkdir and lfs.rmdir act on its data overlay, so
frameworks that list or create their own directories work packed.
- Physics. Any scene node can carry a rigid body on Jolt Physics:
static, kinematic (follows its node and pushes) or dynamic (drives its
node), shaped as a box, sphere, capsule, cylinder, a convex hull of the
node's mesh or the mesh's triangles; mass, friction, bounce,
velocities, forces and impulses; triggers; hinge, ball and slider
joints with limits; a raycast; onCollision and onTrigger callbacks.
The world steps at a fixed 60 Hz between animation and rendering and
pauses with the game. physicsSet2D keeps bodies in a plane for 2D
games, which need no GPU (nodes now exist on every machine, so a
Raspberry Pi 3 gets 2D physics without 3D). New calls: body*,
joint*, physics*, and the
BODY_*, SHAPE_* and JOINT_* constants; see the Physics chapter. Needs
SSE4.1 on x86 (2008 and later); the engine's first C++ (one wrapper
file and the library), statically linked, no runtime to install.
- Soft bodies. softNew makes cloth or a pressure body from a node's
mesh (particles from its welded vertices, Jolt's stretch, shear and
bend constraints, pressure for balloons) or a rope from the node to a
point with a tube mesh the engine makes; they collide with everything
rigid and the mesh is rewritten every frame. softPin holds particles
in place or to nodes; stiffness, mass, damping and pressure are
tunable. meshPlane takes subdivisions; nodeSetMaterial changes a
material without the mesh. New calls: soft*, and the SOFT_*
constants; see Soft bodies in the Physics chapter.
- Ragdolls. ragdollNew derives capsules and swing-twist joints from a
skinned model's skeleton; ragdollActivate hands the skeleton to
physics from its current pose and the mesh follows the bones as it
falls; motors (ragdollSetStrength) pull it back toward that pose;
ragdollApplyImpulse shoves a bone; ragdollDeactivate returns it to
animation. New calls: ragdoll*; see Ragdolls in the Physics chapter.
- Water. bodySetWater fills a static body with water: dynamic bodies
inside float, sink and drift by Jolt's buoyancy against its surface,
with drag and a current (bodySetCurrent, bodySetBuoyancy per body);
players swim in it (playerIsSwimming, playerSetSwim, three-axis
playerMove); a buoyant hull with vehicleNew(node, VEHICLE_BOAT) gets
a propeller and a rudder (vehicleSetThrust, vehicleSetRudder).
- Vehicles. A dynamic body becomes a car, a motorcycle or a tank on
Jolt's vehicle constraint: wheels are nodes the engine poses (spin,
steer, suspension), the script drives with throttle, steering, brake
and hand brake, and engine, gears, suspension, steering, brakes and
anti-roll bars are tunable. Motorcycles lean and balance with a spring
scaled to the chassis; tanks pivot on the spot. New calls: vehicle*,
and the VEHICLE_* constants; see Vehicles in the Physics chapter.
- Point lights inside geometry. A bulb in a closed room, under a shade
or inside a cabinet now shadows correctly: cube faces render depth
two-sided so closed meshes cast from inside, every shadow tap picks its
own cube face so the seams along the cube edges are gone, the depth
range fits the casters round the light instead of the scene centre,
the bias grows with distance and grazing angle, and each face draws
only what it can see. nodeSetShadow excuses a mesh from casting.
- Character controller. playerNew puts Jolt's CharacterVirtual on a
node, as a capsule or any convex bodyNew shape: it walks where
playerMove says, climbs steps, slides along walls, refuses steep
slopes, falls, jumps from the ground, rides kinematic platforms and
shoves light dynamic bodies; onCollision and onTrigger report it like
a body. Works in 2D worlds as a platformer character, with tolerances
scaled to the shape. New calls: player*; see Players in the Physics
chapter.
- Linear lighting. The scene is lit in linear light on a 16-bit float
target and brought to the screen by a post pass with an exposure
control (sceneSetExposure, in stops) and a tone curve (sceneSetTonemap:
TONEMAP_NEUTRAL, TONEMAP_ACES, TONEMAP_NONE). Colours given as 0-255
are sRGB and decoded on the way in; textures are sampled as sRGB;
bright light compresses instead of clipping. Specular highlights are
GGX with Fresnel, so roughness and metallic behave as they do in glTF
viewers. Existing scenes look a little different: the same numbers
now mean what they mean everywhere else.
- GUI. Menus, HUDs and option pages can be RmlUi documents: HTML-like
RML styled by CSS-like RCSS, with layout, text wrapping, nine-slice
panels from the game's own artwork, transitions, animations, form
controls and a focus model a gamepad can drive. guiNew(width, height)
makes a GUI rendered to its own texture every frame; guiLoad puts a
document in it (names through the vfs like spriteLoad, so packed games
work), with guiShow, guiHide and guiClose; guiDraw composites it over
the overlay in overlay coordinates (through the Sinden border and the
overscan scale like the overlay itself), and materialSetGui shows it
on a material in the scene, where sceneProbeGui gives the GUI and
pixel under an overlay point. guiSetInput routes the mouse and light
gun, keys, typed text (SDL text input runs while a field has focus)
and a pad's switches (directions as arrows, ACTION_1 as Return,
ACTION_2 as Escape) to a GUI; what an element uses never reaches
onInputPressed or onKeyPressed, and the rest falls through as before.
guiSetHandler calls a Lua function for an element's click, change or
submit; guiGetValue and guiSetValue read and write a control's value
or an element's inner RML; guiLoadFont adds a face for every GUI.
RmlUi's own Lua API runs on the game's state: rmlui.contexts["gui0"]
and its documents, a document's <script> blocks and on* attributes.
Singe/gui.rcss is the shipped theme (.panel, .list, .muted and the
styled form controls) and Singe/FreeSansBold.ttf the fallback face
(family FreeSans, bold). RmlUi (MIT) is added to LICENSES. Test
scenes scene36 to scene39 cover a document over the disc, a GUI on a
material, input and handlers, and the pointer on a 3D surface. See
the GUI chapter.
- GUI effects. Every RCSS effect now draws: rounded and transformed
elements clip their children to their shape (stencil clip masks),
filter: opacity() fades a group as one, mask-image cuts by an image's
alpha, box-shadow casts hard and soft shadows, filter and
backdrop-filter take blur, drop-shadow, brightness, contrast, invert,
grayscale, sepia, hue-rotate and saturate, and the linear-gradient,
radial-gradient and conic-gradient decorators draw (the theme's
panels, buttons and progress bars and the menu's list use them).
The custom shader decorator is refused, gradients carry at most
sixteen stops, effects nest at most eight layers deep. guiGetStats()
returns what the GUIs cost to render in the last frame (draw calls,
layers, filter passes, mask writes, layer bytes, passes). Test scenes
scene40 to scene43 cover clip masks, layers, filters and gradients.
- The Pi decodes video through its V4L2 memory-to-memory device (the
h264_v4l2m2m family of decoders) when the codec has one, falling back
to software when the device is missing, as on a Pi 5 for H.264. The
trace reports which decoder is in use and the format the device hands
back; --softwarevideo turns it off like the other hardware decoders.
- The Menu.sh and Menu.bat the engine writes now look beside themselves
for a file whose name starts with Singe and run the first one, the way
the launchers BatMaker writes already did, instead of naming the
executable. The executable's name carries its version, so a launcher
that named it stopped working at the next release, and the thing that
would have rewritten it was the launcher itself. With --gamedir the
executable is elsewhere and its full path is still written.
- Third-party libraries brought up to their current releases: ffmpeg
9.0.1 (from a 2023 development snapshot; libpostproc is gone with it),
OpenSSL 3.5.8 LTS (from a 2023 development snapshot), Lua 5.4.9, zlib
1.3.2, zstd 1.5.7, uthash 2.4.0, lsqlite3 0.9.7, luafilesystem 1.9.0,
copas 4.12.0 (its new copas.future module is embedded) and arg_parser
1.21. sqlite3.lversion() reports the real lsqlite3 version instead
of "unknown", and os.tmpname() returns a usable name again (the local
mkstemp edit never filled in its template). LICENSES now records the
version of every vendored library.
- The bundled menu is an RmlUi document (Singe/Menu.rml styled by
Singe/menu.rcss, filled and driven by Singe/Menu.singe): every game in
a scrolling list on the left, up and down to select, left and right to
page, Start or any button to play, and a mouse works too (click a row
to select it, click it again or the Start button to play, wheel to
scroll the description). The service tools are pages of the same
document. A machine without a GPU device, where guiNew fails, gets the
same menu drawn into the overlay instead; see "One menu, two renderers"
below.
- Physics debug drawing. physicsSetDebug(DEBUG_*) draws the bodies in
wireframe (coloured by motion state), joints and their limits, contact
points and velocities as lines over the scene each frame, static shapes
too with DEBUG_STATIC; lineDraw draws a script's own world-space lines
for a frame, navDraw the baked navigation mesh.
- Compressed textures. KTX2 files (Basis Universal, from
util/compressTextures.py) stay compressed on the GPU in the block
format the machine has (BC7, ASTC, BC3, or plain RGBA where nothing
else exists), a quarter of the memory of PNGs or less. A model whose
file uses KHR_texture_basisu loads them on its own; materialSetTexture
and the map calls take a .ktx2 name in place of a sprite; sceneGetStats
reports the texture memory in use. The Sponza test scene went from
393 MB of textures to 99 MB.
- Hot reload. singe --reload (-R) watches the game's loose script files
and runs the game again from its script when one changes; F5 does the
same, and singeReload() from a script. The engine (window, GPU, disc,
controllers) stays; everything the script made is remade by the script.
- Terrain. meshHeightmap makes rolling ground from a greyscale image,
materialSetTiling repeats a texture across it, a SHAPE_MESH body on it
becomes a Jolt height field, and terrainGetHeight reads the ground
height at a point.
- Sprites and text in the scene. nodeSetSprite shows a loaded sprite on
a node as a lit quad (nodeSetSpriteFrame picks a frame), nodeSetText a
line of text in the selected font, and nodeSetBillboard turns any node
to face the camera squarely or about its own up axis (BILLBOARD_*).
- Navigation. navNew, navAddNode and navBuild bake a walkable mesh from
the scene's geometry on Recast; navPath, navNearest, navRaycast and
navRandomPoint query it on Detour; agents (navAgentNew, navAgentMoveTo,
navAgentStop, navAgentGetVelocity, navAgentIsArrived, navAgentSetPlayer)
walk it on DetourCrowd without walking through each other, moving their
nodes or driving a player controller, with onNavArrived when they get
there; navSave and navLoad keep a baked mesh. Recast Navigation is
vendored under thirdparty/recastnavigation (zlib licence).
- Bloom and views. sceneSetBloom makes everything brighter than a
threshold glow, blurred over a half-size chain before the tone curve.
viewNew renders a second camera to a texture every frame (viewSetCamera
points it) that materialSetView shows on any surface: monitors,
mirrors, portals. Each view turns billboards to its own camera and
draws transparency back to front from it.
- Particle extras. Trails (emitterSetTrail: a fading ribbon behind
each particle, 2D and 3D), collisions with a floor or, by ray cast,
the physics world (emitterSetCollide, COLLIDE_*), lit particles shaded
by the lights and sky (emitterSetLit) and soft particles that fade
where they meet geometry (emitterSetSoftness).
- Culling and instancing. Meshes outside the camera's view are skipped,
and copies of one mesh with one material draw as a single instanced
call (their matrices in a per-frame buffer the vertex shaders read), in
the shadow passes too. sceneGetStats reports the draws collected, the
draws in view and the draw calls made.
- Cascaded shadows. A directional light's shadow is split into up to
four maps along the view (sceneSetShadowCascades, three by default),
each fitted to a slice of the camera's view out to
sceneSetShadowDistance and snapped to its texels, so shadows in a
large level are sharp up close and steady as the camera moves.
- Sky, environment light and fog. sceneSetSky wraps the scene in an
equirectangular panorama (Radiance .hdr, PNG or JPEG) drawn behind
everything and lighting it: its diffuse light replaces the ambient and
its reflections show on metals and glossy surfaces, blurred by
roughness (sceneSetEnvironment switches that off, sceneSetSkyIntensity
scales it). sceneSetFog fades meshes and particles toward a colour
between two distances.
- Positional sound. A playing channel can follow a scene node
(soundSetNode) or sit at a point (soundSetPosition) and is then panned
and faded by distance (soundSetRange) as heard from the camera or a
chosen node (soundSetListener); soundGetPosition reports where it lies;
soundSetPan pans a channel for 2D games; soundPlay takes a loop count.
- Animation blending. animationPlay takes a fade in seconds and
crossfades from the clip that was playing, or from the pose the nodes
are in when nothing was (so a ragdoll eases back up); up to four clips
play on layers above the base (animationPlayLayer), each with a weight
(animationSetLayerWeight) and an optional mask limiting it to part of
the skeleton (animationSetLayerMask); animationStop takes a layer.
- Material maps. A material can carry a normal map, an occlusion map, a
metallic-roughness map (glTF's packing) and an emissive map beside its
base texture, and a loaded model brings its own. Every mesh has
tangents: a model's when the file carries them, computed from the UVs
otherwise. New calls: materialSetNormalMap, materialSetOcclusionMap,
materialSetMetallicRoughnessMap, materialSetEmissiveMap.
- Mipmaps. Every scene texture carries a full mipmap chain generated on
the GPU at load and is sampled with anisotropic filtering, so textures
seen small or at a grazing angle no longer sparkle or moire.
materialSetFilter(material, FILTER_NEAREST) keeps pixel art crisp.
- Particles. Emitters spawn, move and age particles from a recipe (rate,
life, speed, direction and spread, gravity, drag, size, colour and spin
over life, a birth radius, a pool cap) in the overlay and in the 3D
scene alike. A 2D emitter draws on the GPU above or beneath the
overlay when the script asks (emitterDraw); a 3D emitter sits on a node
and draws camera-facing billboards after every mesh, depth tested, in
world units. Any loaded sprite is the picture (a random frame per
particle from an animated one); without one, a soft disc. Normal or
additive blending. New calls: emitter*, and the PARTICLE_* constants;
see the Particles chapter.
- 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 each able to cast shadows (cube maps for
point lights), any node as the camera, and glTF 2.0
models (.glb, self-contained) with node animation, skinning and morph
targets, 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; the Raspberry Pi 4 is
the minimum Pi (the Pi 3 has no Vulkan driver). 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
directories) runs it. Nothing in a game has to change to be packed:
every name a script uses (dofile, require, io.open and friends,
spriteLoad, soundLoad, fontLoad, videoLoad, framefiles, controls.cfg)
goes through one lookup that tries a loose directory named like the
database, then the game's data directory, then the database, so
packed games can be modded by dropping files beside them and files a
game writes land under data/<game>/files. Inside a database names
are matched without regard to case and a leading own-directory prefix
("DLe/Cfg/x") is ignored. singe --unpack GAME.game DIRECTORY restores
the files and singe --patch GAME.game SOURCE replaces files from a
directory or a patch database in one transaction. --entry=N picks
which games.dat entry a .game launch runs. Loose games are
untouched by all of this. Scripts get SQLite for their own data with
require("sqlite3"). The archive installer (.game, .tool, and .patch
archives dropped beside the binary) is gone along with libarchive; a
game is a .game file or a directory, copied into place.
- Video is decoded by libavcodec directly; FFMS2 is gone. Seeking is still
frame exact, indexing is faster, and the index files in the data
directory use a new format (old ones are rebuilt automatically).
Multi-channel audio is downmixed to stereo instead of refused. Indexing
is a few seconds at most, so the indexing screen and its artwork are gone.
Video is
decoded by the platform's hardware decoder when there is one (VA-API or
VDPAU on Linux, D3D11VA on Windows, VideoToolbox on macOS) and falls
back to software otherwise; --softwarevideo forces software.
- Singe is built on SDL3 (SDL 3.4, SDL3_image, SDL3_mixer 3.2, SDL3_ttf)
instead of SDL2. Nothing changes for scripts: the same functions,
constants, scancodes, and controls.cfg names. SDL2_gfx is gone; sprite
and video rotation and scaling are done by the engine itself.
- The sprite handle is now the FIRST argument of spriteDraw(), spriteLoop(),
spriteQuality(), spriteRotate(), spriteRotateAndScale(), spriteScale(), and
spriteSetFrame(), matching the video*() functions. Move the last argument
of each call to the front. Games that cannot be edited can set
SINGE_LEGACY_SPRITE_ARGS = true before loading the framework, or add
LEGACY_SPRITE_ARGS = true to their games.dat entry. See "Migrating from
Singe 2.10" in the manual.
- Singe is no longer laserdisc only. A games.dat entry with a VIDEO is a
laserdisc game; one with the line missing or blank runs without a
disc, on a canvas sized by CANVAS_X/CANVAS_Y (default 720x480), the
disc functions become harmless, and discGetState() returns the new
DISC_EJECTED. On the command line --disc (implied by --framefile)
declares the disc, since a video found beside the script no longer
does; --canvas=WxH sizes the canvas. Existing games.dat files need no
change. Scripts can test SINGE_DISC.
- mouseSetEnabled(bool) and singeSetPauseKeyEnabled(bool) added. The old
mouseEnable/mouseDisable and singeEnablePauseKey/singeDisablePauseKey are
now aliases defined in Framework.singe.
- Handles, counts, and frame numbers are returned as Lua integers.
videoIsPlaying() returns a boolean. mouseSetMode() returns nothing.
- onControllerMoved() receives the controller index (0 to 3), not SDL's
instance ID. onMouseMoved() in MANY_MOUSE mode receives real relative
motion in its third and fourth arguments.
- Runtime errors inside callbacks now end the game with a traceback, the way
argument errors always have.
- Held keys no longer auto-repeat in MODE_NORMAL.
- The engine parks the disc on frame 1 before the script runs; the framework
no longer seeks when it is loaded, so threaded games keep their own
startup positioning.
- The pause key now pauses the whole game. The engine freezes the script
(no callbacks, no singeMain), holds sound completions, releases and later
re-presses held inputs, and draws a PAUSED indicator. It acts on the key
press, and SWITCH_PAUSE reaches the script only when the key is disabled
with singeSetPauseKeyEnabled(false). singeSetPauseFlag() still pauses
media without freezing the script. Gamepad and mouse buttons mapped to
pause, quit, screenshot, or grab now work in MODE_FULL too; keyboard
mappings stay raw there.
- Video decoding runs on its own thread per video, so a discSearch() far
from a keyframe no longer stalls the game loop; the previous frame stays
on screen until the new one is ready. The laserdisc and framefile videos
are handed to the GPU as YUV and converted there instead of on the CPU,
and each video reports its keyframe spacing to the program trace when
loaded, with a warning there when seeks may be slow.
- Constants that used to be duplicated in Framework.singe (SWITCH_*,
FONT_QUALITY_*, MODE_*, MOUSE_*, OVERLAY_*, RENDER_*, SOUND_ERROR_*) are
now defined by the engine. New: DISC_STOPPED/DISC_PLAYING/DISC_PAUSED
for discGetState(), SINGE_VERSION_MAJOR/MINOR/STRING, and the SINGE_*
input code layout that Framework.singe builds GAMEPAD_N and MOUSE_N from.
- Audio and video are kept in sync from the audio clock. The engine
measures the audio device's queue at startup instead of guessing, uses
small mixer buffers, and honours a container whose first video frame is
timestamped after its first audio sample. For delay the engine cannot
see (receivers, Bluetooth, DACs) there is singeSetAudioDelay(ms) /
singeGetAudioDelay(), the games.dat key AUDIO_DELAY, and
--audiodelay=MS; positive values mean the audio is heard late.
- The menu has an audio delay calibration screen for the machine's own
delay (DAC, receiver, Bluetooth, display). Press the service key on
the game list; a click plays once a second and the screen flashes.
Adjust with the stick until the flash and the click coincide, then
press button 1 to save. The value is kept in machine.cfg in the data
root and applied to every game on the machine, from the menu or the
command line, on top of any per-game AUDIO_DELAY. Scripts can read and
set it with singeGetAudioCalibration() / singeSetAudioCalibration(),
read the measured device queue with singeGetAudioLatency(), and now
have singeGetTicks() for a wall clock in milliseconds (os.clock() is
processor time and drifts).
- Inside a packed game a relative name with a ".." component resolves
nowhere: io.open returns nil and "NAME reaches outside the game",
io.lines, io.input and io.output raise it, lfs.mkdir and lfs.rmdir
return nil with it, and asset loads report the file as missing. The
packer already refused such names; the lookup now enforces the rule at
run time. lfs.dir on a packed directory lists its entries sorted
without regard to case.
- One rule for the data directory on every launch (command line,
games.dat, scriptPush and scriptExecute): with --datadir, or for a
packed game, a directory named for the game under the base; otherwise
the game's own directory. scriptPush of a loose game without --datadir
used to write under the current directory.
- Color components handed to lightSetColor, materialSetColor (alpha
too), materialSetEmissive, lineDraw and navDraw are clamped to 0..255,
as colorForeground and colorBackground always were; they used to wrap.
- materialDelete on the private material of a sprite or text node is an
error ("belongs to a sprite node; clear the sprite instead") instead
of freeing a slot the node still used.
- lightSetCone raises the outer angle to at least the inner angle.
- Packed games: the data directory is always <base>/<database name>/
<script directory>/. An archive whose script sat in a subdirectory
(Game/Main.singe) wrote to <base>/Game/, shared with every archive of
that layout; its saves now belong under <base>/<name>/Game/ and must be
moved by hand. Archives whose script sits at the root are unaffected.
- Packed games: a path naming the .game file itself (Games/DLe.game,
with or without a trailing separator) addresses the database root, as
"." does from inside: lfs.dir lists it and lfs.attributes reports a
directory, while io.open on it returns nil and lfs.mkdir and lfs.rmdir
refuse it. Engine-owned names (Singe/ and the data directory) may use
".." as long as they stay below that prefix; any other ".." inside a
packed game is still refused.
- lfs.attributes inside a packed game answers as the real lfs does: the
whole table (mode, size, modification and the rest) with one argument,
one field by name (an unknown name raises "invalid attribute name"),
or a table of the caller's filled in; a missing file returns nil, a
message and the error number.
- sceneProject returns false with 0, 0, 0 before the scene has rendered
a frame, instead of inf and nan.
- softNew removes the node's existing soft body before it looks at the
mesh, so a call that fails leaves the node with none; softNew on a
node carrying a rope fails with "needs a mesh" (the tube was the
engine's) instead of reading freed memory.
- navAgentNew keeps the radius and height at 0.01 or more, as navNew
does, so onNavArrived fires for an agent given a radius of zero.
- Three Hypseus names the bytecode-only games turned out to need.
controllerSetPadding(boolean) and controllerGetPadding() are accepted;
the getter always answers 0, because Hypseus's offset marks a switch
code as having come from a gamepad and Singe's mapped switches carry no
such mark. videoGetVolume() with no argument answers the laserdisc's
volume on Hypseus's 0 to 64 scale, which its games double into
musicSetVolume. require("md5") gives the PUC-Rio md5 library Hypseus
bundles, all five of sum, hexsum, exor, crypt and decrypt, so a game
written against it behaves the same here; note that library names its
sumhexa hexsum.
- The ARM64 build carries the V4L2 stateless decoders: AV1, H.264,
HEVC, MPEG-2, VP8 and VP9. These reach the hardware on the boards
whose kernel drivers use the V4L2 Request API rather than the older
memory-to-memory one -- Allwinner's Cedrus, Rockchip's rkvdec and
hantro, the Pi 5's rpivid, i.MX8's hantro -- none of which the engine
could use before. Singe tries them first on ARM, then Rockchip's MPP,
then stateful V4L2, and decodes in software when a board offers none.
Desktop platforms are untouched. The support is Jonas Karlman's
V4L2 Request patchset for FFmpeg, carried in thirdparty/ffmpeg; it
brings a runtime dependency on libudev, which these boards all have.
- The Raspberry Pi build target is now linux-aarch64 and its binary is
Singe-v3.00-Linux-aarch64. Nothing in it was ever Pi specific: the
V4L2 decoder it builds talks to any memory-to-memory device, so the
same binary serves Amlogic, Exynos, Qualcomm and other ARM64 Linux
boards whose kernel offers one. The build system now decides by
architecture, and by whether a sysroot is in use, rather than by a
platform name that stood for both. cmake/zig/piPackages.cmake is
cmake/zig/arm64Packages.cmake, and the command is
./build-all.sh linux aarch64.
- src/main.c now includes windows.h where it reads the processor name
out of the registry, which the Windows build had been failing without.
- A hardware decoded frame stays NV12 the whole way to the texture
instead of being converted to planar YUV on the CPU first. The picture
is identical: the GPU does the same conversion when it samples either
way. Measured on an RX 7900 XTX at 720x480 the difference was inside
the noise, so this is for the small ARM machines where a per-frame
conversion is worth more than it is on a desktop; it has not been
measured on one.
- Rockchip's rkmpp decoders are built and used when the Rockchip Media
Process Platform library is present, and ignored everywhere else. The
superbuild probes for rockchip_mpp with pkg-config rather than asking
for --enable-rkmpp unconditionally, which would fail every configure on
a machine without it. Untested: no Rockchip hardware here.
- The manual now says plainly that the Hypseus API is for compatibility
and not for new games, in the Lua API Reference introduction and again
at the head of the Hypseus Compatibility section. Every Hypseus name,
and every Hypseus argument form on a function that otherwise takes
Singe's own, says so in its own entry.
- videoSetVolume(volume), with one argument instead of three, sets the
laserdisc's volume on Hypseus's 0 to 64 scale, the twin of the bare
videoGetVolume(). A level outside that range is ignored rather than
fatal, as in Hypseus.
- drawTransparent(boolean), getUserString(), mouseGetPadding() and
spriteBlend(boolean, sprite) are registered, completing the set of
names Hypseus's engine exposes. All four are compatibility answers:
Singe carries transparency in the alpha channel rather than a colour
key, sends no statistics and so has no machine ID to report, adds no
offset to mouse input codes, and never has blending switched off.
- allowSocketCall() and setOverlayFullAlpha() are registered so a game
that calls them does not stop on a missing function. The first always
answers false: Hypseus reports whether the user let it send data to a
stats server, and Singe sends nothing anywhere. The second is accepted
and ignored: it asks for an overlay that blends its alpha channel
rather than thresholding it, which Singe's overlay has always done.
- The other Lua modules Hypseus exposes are here too, whether or not a
game asks for them: require("bit") is LuaBitOp, require("random") a
Mersenne Twister generator, require("cjson") Lua CJSON and
require("lpeg") LPeg. Singe already had the rest of Hypseus's list
(lfs, socket, rs232) and opens every standard Lua library besides.
- materialSetCutoff(material, cutoff) discards the texels whose base
colour alpha falls below the cutoff, in the lit pass and in the shadow
pass alike, so cutout foliage casts cutout shadows. What survives the
cutoff is drawn fully opaque, which is what glTF means by MASK as
against BLEND: the sampled alpha is not passed on, so nothing behind
the scene shows through a leaf. glTF's MASK alpha mode now sets the
cutoff from the file's alphaCutoff instead of warning that the material
would be drawn opaque; BLEND and OPAQUE are unchanged. Materials that
never set a cutoff keep drawing every texel.
- --deterministic[=MS] runs the engine on a virtual clock stepped MS
milliseconds (default 15) once a frame instead of on real time, steps
the disc one video frame a frame with it, and seeds Lua's generator
and the engine's from the same number, so a screenshot taken at a
fixed frame number is byte identical between runs. It is a testing
facility: audio and pacing are meaningless in it. Settable as
deterministic in settings.cfg. With the option absent nothing
changes.
- Every image format the bundled FFmpeg can decode now loads, not only
the ones SDL_image reads: TIFF, OpenEXR, JPEG 2000, DDS, PSD, DPX,
and AVIF and HEIC through a vendored dav1d. spriteLoad,
spriteLoadData, spriteLoadFrames, meshHeightmap, --bezel artwork,
glTF embedded textures and the GUI's images all take the same path:
SDL_image first, FFmpeg for whatever it turns down. sceneSetSky
gains the same fallback and keeps a floating point source's range, so
an OpenEXR sky is lit correctly rather than clipped.
- AV1 video plays. FFmpeg's own AV1 decoder is hardware only and said
"Your platform doesn't support hardware accelerated AV1 decoding" on
any machine without an AV1 capable GPU; the vendored dav1d is the
software decoder, and it is what brings AVIF with it.
- Every audio format the bundled FFmpeg can decode now loads as a sound
or as music: AAC, ALAC, AC-3, WMA, APE, TTA, Speex and AMR among
them. An AAC track used to play when it was the audio of a video and
fail when it was a sound effect. Chiptunes arrive with a vendored
game-music-emu (NSF, NSFE, SPC, VGM, GBS, AY, GYM, HES, KSS and
SAP), and the tracker
formats libxmp was already reading -- MOD, S3M, XM, IT and fifty-odd
others -- are now documented rather than merely present.
- MIDI files play, synthesised with TinySoundFont. Singe ships no
sound bank, because a good one is tens of megabytes for a format
almost no game uses: name one with --soundfont FILE, put one at
Singe/soundfont.sf2 so a packed game carries its own, or let it find
the one your distribution installed. The trace header says which was
used. SDL_mixer's own MIDI decoder wants a GUS patch set almost
nobody has, so a .mid failed to load while the trace claimed a MIDI
decoder was there.
- MIDI ports, in and out, for a cabinet driving a real sound module or
reading a keyboard or a control surface as an input device:
midiInputCount, midiInputName, midiOpenInput, midiCloseInput,
midiIsInputOpen and the same five for output; midiSend for any
message at all; midiNoteOn, midiNoteOff, midiProgramChange,
midiControlChange and midiPitchBend; midiRescan for a device plugged
in while the game runs; and an onMidiMessage(status, data1, data2,
bytes) callback. Channels are 1 to 16. ALSA's sequencer on Linux,
CoreMIDI on macOS, the multimedia MIDI calls on Windows. On Linux
the library is opened at run time, as SDL opens the same one for
audio, so a machine without it simply has no ports.
- spriteLoad(name, width, height) rasterises a vector picture -- an SVG
-- to fit that box, keeping its proportions, instead of taking the
file's own size, which for an icon is usually 16 or 24 pixels. Every
other format ignores the size.
- --deinterlace off|auto|on, automatic by default, for a laserdisc rip
that kept its interlaced fields and combs on a progressive display.
Automatic touches only the frames a file marks interlaced, so a
progressive disc pays nothing; on is for a file whose flags are
wrong. Settable as deinterlace in settings.cfg.
- Fonts gain what FreeType was built without: WOFF and WOFF2, colour
bitmap glyphs, colour vector glyphs, and complex script shaping
through HarfBuzz, so Arabic, Hebrew and the Indic scripts join and
order correctly instead of coming out as unjoined letters.
- A framefile opens its segments as the disc reaches them instead of
opening every one at startup. A long framefile -- typing-md2 has 213
segments -- held a demuxer open for each, which cost hundreds of file
descriptors and could push a game past the 1024 that glibc's select
refuses to look past, aborting it inside a fortify check. At most
four segments are open now: the one playing, the one after it (opened
ahead so the changeover costs nothing), and room for a seek to move
between two. Measured on a twelve-segment game, 75 open descriptors
became 24. A framefile naming a file that does not exist still fails
at startup, as it always has.
- The game kit: five things every game was writing for itself.
saveGet, saveSet, saveGetAll, saveSetAll, saveDelete, saveClear and
saveFlush keep a table between runs, as JSON in the game's own data
directory. Values may be numbers, strings, booleans or tables of
them, nested; whole numbers come back whole. Tables are copied in and
out, so what you set is what is saved and what you read back is yours
to change. saveGetAll and saveSetAll read and replace the whole save
in one step, which is how an old save is moved to a new layout without
a moment in between where it is empty. One write at the end of any
frame that changed something, and again at shutdown; saveFlush writes
now, for a cabinet that may lose power. The file is renamed over the
old one, so a write that is interrupted leaves the previous save
rather than half of a new one.
timerAfter, timerEvery, timerCancel and timerIsActive run something
later without counting frames. They use the engine's clock, which is
the virtual one under --deterministic, so a timed game repeats
exactly, and they fire with the rest of the frame's callbacks.
tweenValue moves a number from one value to another over time on a
curve, with tweenCancel and tweenIsActive beside it and twenty-two
EASE_ curves to pick from. It hands the number to your function
rather than moving a node itself, so it works on anything.
collideRects, collideCircles, collideRectCircle, collidePointRect,
collidePointCircle, collidePointPolygon and collideSegments answer
whether two flat things touch. Plain functions, no handles. Jolt
already answered this in three dimensions; nothing answered it for the
overlay, where light gun hitboxes live.
statsEnable and statsIsEnabled show the developer's overlay: frame
time and its worst, frame rate, Lua memory, what is alive, how many
draw batches, and what the disc is doing. Drawn with SDL's own debug
font, so it needs no asset, and drawn after singeScreenshot takes its
picture, so it never lands in a screenshot.
- Four more Lua libraries are bundled: middleclass for classes, lume
for the small things (lerp, clamp, round, shuffle, split, serialize),
inspect for printing a table in a readable shape, and bump for moving
things that must not pass through each other -- which is what the
collide calls deliberately do not do.
- Player handles. The name on a leaderboard is a handle, not an email
address: three to twenty characters of letters, digits, hyphens and
underscores, chosen once. An operator can rename somebody; a player
cannot, and an email address cannot be changed at all. Handles are
checked against a list the operator maintains -- identities nobody
should claim, and words with no place on a screen in a public room.
- Online: accounts, a game catalogue, and high scores. A cabinet can sign
in to a master service (master.singeengine.com by default), browse every
game the service offers, download and update them, and post scores to
leaderboards. Two new service tools cover the setup: Online Account
signs in, creates an account, recovers a password and sets the server
address; Get Games lists the catalogue, marks what is installed here,
and downloads, updates or removes it.
A game posts a score in one line -- scoreSubmit(points) -- plus
scoreBoard, scorePlayerName, scoreWaiting and scoreUpdate. The account
belongs to the machine rather than the game, so a game uses whatever
sign-in the menu made. A score is queued to disk and sent when there is
a connection: a cabinet is often offline, and a game must never stall
on the wire to show its own board.
Nothing blocks a frame. Singe/Net.singe is an asynchronous HTTP client
driven from the frame loop, because Lua here is cooperative and
ssl.https.request would stutter a JSON call and make a large download
look like a hang. It does not use LuaSec's https module at all: that
ships with verify = "none", which encrypts the connection and
authenticates nobody. A connection is verified against a pinned public
key instead.
A download is checked against the digest the catalogue published before
it replaces anything, so a transfer that arrives wrong fails rather than
installing a game that will not run. Downloads resume.
New engine calls: utilSha256(data) for exactly that checking, and
singeGetGameId() for the GAME_ID a games.dat entry can now carry, which
is what a leaderboard is kept under. A game without one can be played
but not ranked.
A game the service withdraws keeps working on the cabinets that have it.
All that stops is being offered an update, and the menu shows it as
"installed, no longer offered" so it can still be removed from there.
scoreBegin() tells the service a play is starting, and the score that
follows carries how long it took measured on the server's own clock.
That is the one number in a submission that has not been through the
player's machine. Nothing is rejected on it -- it is shown to the
operator beside the score, and the judgement stays a person's.
A submitted score is a claim and the design says so: the cabinet is the
player's own machine. The service records who claimed what, rate limits
submissions, and flags outliers for a person rather than pretending to
verify them.
- One menu, two renderers. Singe/MenuClassic.singe is gone: it was the
whole menu written a second time, and the two copies duplicated their
game list, their selection, their menu.dat handling and their games.dat
walk almost line for line. Singe/Menu.singe is now everything the menu
is and draws nothing, and it picks one of two renderers at startup from
the new singeHasGpu(): Singe/MenuDocument.singe for a machine with a
GPU device, Singe/MenuOverlay.singe for one without. A machine with no
Vulkan, Direct3D 12 or Metal driver therefore gets the menu -- and all
ten service tools -- without anyone editing a launcher, which is what
editing Menu.sh used to be for.
Both renderers take the same keys, so the overlay one now moves the
selection with up and down and pages with left and right, as the
document one does; it shows one game at a time rather than a list, and
clips a description too long for its column rather than scrolling it.
MENU_OVERLAY = true in menu.dat forces it on a machine that has a GPU.
singeHasGpu() answers whether the GPU device came up. guiNew and the
scene calls end the script rather than returning false, so a game that
can do without one had no way to ask.
- The service tools. Pressing the SERVICE key on the bundled menu now
opens a list of ten screens rather than the audio delay screen alone:
Audio Delay, Input Test, System Information, Light Gun, Sound Test,
Display, Disc Test, Saved Data, MIDI Ports and Frame Statistics.
Button 2 backs out of a tool, the service key leaves. They live in
Singe/Tools.singe, which the engine extracts beside Menu.singe. Nothing
there draws: a tool builds a list of rows and the menu draws them, so
the same ten run on both renderers and a cabinet builder adding one
writes no drawing code at all.
Four calls were added for them, and are worth having on their own:
singeGetSystemInfo() returns everything the trace header knows in one
table -- version, operating system, processor, renderer, 3D device,
video decoders, audio formats, SoundFont, MIDI, window and canvas
sizes, this game's data directory and the data root every game writes
under -- so a bug report can be a photograph.
vldpGetShift() and vldpSetShift(x, y) move the picture within the
window while the game runs, the running equivalent of --shiftx and
--shifty, beside vldpGetScale and vldpSetScale.
singeSaveGeometry() keeps the scale, shift and rotation for this
machine, in the same machine.cfg the audio delay uses, and the engine
applies them to every game it starts afterwards unless the command
line names its own. A monitor is squared up once, not once per game.
The file is called machine.cfg rather than audio.cfg, because the
audio delay is no longer all it holds.
discGetFrameCount() answers how many frames the disc has. A framefile
answers for the whole disc rather than the segment playing.
SINGE_CONTROLLER_AXES and SINGE_CONTROLLER_BUTTONS join the input
layout globals, so a service screen can walk a pad without knowing the
numbers.
- Subtitles carried inside the video file: discGetSubtitleTracks(),
discGetSubtitleLanguage(track) and srtLoadTrack(track), which reads
one track out of the disc's own container and loads it exactly as
srtLoad loads a .srt beside the game. SubRip, WebVTT, ASS and MOV
text are read, with ASS override tags stripped; picture subtitles
(VobSub, PGS) are counted so the numbering matches other players, but
cannot be read, because there are no words in them.
Fixes
-----
- A lone .m2v played with --framefile now finds the .ogg beside it, as a
framefile's segments always have. A Daphne era elementary stream
carries no audio of its own, so lair.m2v played silently unless it was
listed in a framefile or --altaudio was given. Any other container
keeps using the audio inside it.
- A single-frame image saved as a 1-bit PNG (many overlay assets from
the 2.x era) loads again; spriteLoad reported "Blit combination not
supported" on it. Images with 16 bits per channel or floating point
pixels (16-bit PNGs among them) load as sprites, animation frames and
particle textures; they ended the program.
- lfs.dir(".") and other names for the root work inside a packed game;
they were refused as if the directory did not exist.
- --sindengun and --scalefactor are listed in --help and the manual;
they were hidden because mouse positions ignored the Sinden border and
the overscan shrink. Positions now map into the picture actually
drawn, so a light gun lands where it points inside the border. The
border is drawn the right way round (black ring outside, white ring
inside, the game in the middle); one too wide for the video ends the
program with a message instead of a zero-sized game; and light guns
and tablets reporting absolute positions through ManyMouse are mapped
into the picture like the mouse.
- The manual's 3D Scenes, Physics and Particles chapters are rewritten as
sections in a learning order with worked examples, every reference
entry now shows each form the call accepts with a realistic example,
and prose spelling is American throughout. Manual.pdf and Manual.html
are rendered into .builddir/ by the build and by build-docs.sh alike,
and the binary embeds the PDF from there.
- controllerGetButton() never worked; it rejected every framework button
code.
- The five argument form of spriteDraw() always failed.
- spriteSetFrame() on a still image crashed.
- Unloading the selected font left later fontPrint() calls using freed
memory.
- overlayCircle() drew around the origin instead of the requested center.
- overlaySetResolution() broke mouse coordinates and vldpGetPixel() because
the overlay scale was computed with integer division.
- mouseGetPosition() always returned 0,0 and controllerGetAxis() read the
wrong controller because the axis cache was indexed two different ways.
- Analog stick release events fired with a stale input code. Controllers
that were unplugged and replugged shifted their codes.
- The -o/--audio option was lost when the configuration was copied.
- --option=value forms of numeric options, and options after the script
name, were not accepted.
- Running "singe game.singe" from the game's own directory wrote its data
to the filesystem root.
- Framefile playback never advanced past the first segment, and switching
segments dropped the selected audio track.
- Video timing accumulated main loop latency, drifting behind the audio.
Audio is now the master clock: the picture follows the samples the sound
device has actually consumed, so the two cannot drift apart and the
mixer's buffering latency is accounted for. Silent videos follow the wall
clock. The first frame after a seek was shown one update late.
- Video without an audio track crashed on the first update.
- The video audio stream was shared with the mixer thread without a lock.
- onSoundCompleted was called from the audio thread. It is now delivered
from the game loop.
- Screenshots read the frame buffer through the window surface, which SDL
forbids alongside a renderer, and could corrupt memory on shutdown.
- .patch archives crashed the installer. Archives without an explicit top
level directory entry were rejected. Extraction now refuses paths that
escape the installation directory.
- The Sinden gun arguments could overflow the configuration structure.
- Support files in the Singe directory are refreshed when the installed
copy differs from the running build. Upgrading the binary used to keep
the old Framework.singe and Menu.singe forever.
- The menu background video is encoded with a keyframe every second, so
the menu's jump to the game list is instant.
- Framefiles whose first line is a relative directory produced garbage
video paths (the directory string was freed before use); a lone "." now
means the framefile's own directory.
- A crash prints a backtrace to the console (Linux and macOS) so a report
can say where it happened; run with --program and send trace.txt too.
- Mouse positions are delivered in the video's coordinates again (SDL3
stopped scaling them for us), so crosshairs follow the pointer.
- os.clock() in scripts now returns wall time rather than processor time.
Nearly every existing game times its input debounces and prompts with
it; once the GPU decodes the video the engine mostly sleeps, and those
timers ran seconds slow. A game that wanted processor time is not
known to exist. This diverges from stock Lua and from other Singe
runtimes; new code that must be portable should use singeGetTicks().
- A key or button still held when a game exits no longer counts as a
press in the script that follows. Confirming Exit in a game with
button 1 used to relaunch it from the menu.
- Audio ran late by the device queue depth on most Linux audio servers:
the clock assumed one mixer buffer of latency where PulseAudio and
PipeWire hold several. See the audio sync entry above.
- Windows builds now carry the icon and version resource, with the version
number encoded correctly.
- The build no longer depends on lyx, upx, or xxd; embedded resources, the
version header, and the manual are generated by CMake. The manual is
embedded again and extracted as Singe/Manual.pdf.
- The licence Singe is under and the notices its libraries and assets
require are embedded and extracted as Singe/COPYING and
Singe/LICENSES, the way the manual is. Nothing in the build copied
them beside the binary, so a download carried the program without the
terms it is offered under.
- One command builds Singe from a clean checkout on a Debian based
system: ./build-all.sh <platform> <arch> installs the host packages,
then CMake fetches zig (the compiler for every target), the Pi's
platform packages and the macOS SDK into .builddir/toolchains and
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 universal (Apple silicon and Intel) binary. The toolchains repository is no longer
needed. See INSTALL.
- On a plain video disc, discSearch, discSkipToFrame, discSkipForward,
discSkipBackward, discStepForward and discStepBackward clamp at the
first and last frame instead of wrapping around the disc.
discStepForward and discStepBackward are ignored while the disc is
stopped, like the skips; they used to move the frame behind the blue
screen. On a framefile the skips apply to the frame number
discGetFrame reports, so a skip in any segment after the first lands
where it should.
- Every collision and trigger event of a frame reaches onCollision and
onTrigger; only the first 64 used to, the rest were dropped.
onTrigger fires once per body and trigger pair even when a mesh or
compound shape touches in several places. playerJump is granted
whenever the player is on the ground.
- playerSetEnabled(node, false) removes the player's body from the
world and reports it leaving every trigger it stood in; enabling puts
it back. navPath ends a partial path at the last reachable point.
navLoad rejects data that is not a Detour mesh, or a file shorter
than the mesh its header describes ("the navigation mesh file is
truncated or damaged"). The world holds at most 4096 bodies; bodyNew
raises an error when it is full.
- A body, player, ragdoll, soft body or vehicle whose node was deleted
is released at the start of the next frame, disabled, inactive, static
or paused or not; a disabled player or an inactive ragdoll on a deleted
node kept its slot (16 players, 8 ragdolls) forever. playerExists,
softExists, ragdollExists, vehicleExists and every setter answer false
for a node handle that was deleted and reused instead of driving the
previous owner's record. bodySetTrigger(node, false) no longer
swallows the leave of a trigger the node is standing in.
physicsSetDebug's mask is cleared by a reload.
- Sprite nodes are no longer culled early at the edge of the view, and
shadow fitting sees their real size. Soft particles fade correctly in
a scene with no meshes instead of against stale depth. Point-light
shadows update when a caster's mesh is rewritten (soft bodies), given
morph targets, or swapped with nodeSetMesh at an unchanged transform;
the six cached faces could freeze in the old shape. Cascaded sun
shadows appear in render-to-texture views (viewNew) instead of
vanishing; a view takes the finest cascade that covers each point.
Debug lines no longer pile up while the scene layer is enabled but has
no target.
- modelLoad refuses a skin with more than 128 joints or a skinned mesh
naming a joint numbered 128 or more (the shader used to read past the
matrices); a joint beyond the skins that use the mesh is traced and the
load goes on, so a mesh shared by skins of different sizes loads. A
texture that cannot be decoded, or a missing KTX2 image, is traced and
the load goes on without it instead of leaving modelLastError set.
KHR_materials_emissive_strength is applied. Sparse accessors load,
animation keyframes included, so Blender shape keys are no longer
silently zero; a sampler that cannot be read is traced and skipped,
and a rotation channel with a malformed output no longer produces NaN
transforms. Alpha MASK materials, textures on TEXCOORD_1 and up, and
KHR_texture_transform are traced as unsupported.
- After a script reload, or any stall longer than a tenth of a second,
model animations advance by at most a tenth of a second instead of the
whole gap; a non-looping clip used to be finished before its first
frame. A 3D emitter whose node is deleted is removed even when the
handle is reused by a later nodeNew; it used to jump to the new node.
- Video index files are named <name>-<hash>.index, the hash from the
video's full name, so two videos with one base name in a game stop
rebuilding each other's index. Old <name>.index files are ignored and
can be deleted.
- The packer reports every forbidden file in a directory rather than
stopping at the first. --unpack refuses a database whose stored names
would land outside the target directory. Launching a .game database
from the command line no longer crashes.
- --stretch hands the script mouse positions in the game's coordinates
at any window size. --scalefactor with --sindengun shrinks the picture
inside the Sinden border instead of being cancelled by it. A negative
--audio track number is ignored. With no --datadir the controls.cfg
beside the script is read once, not twice.
- A reload (F5, --reload, singeReload) ignores keys and buttons held
across it until they are released, resets the effect channels and their
pending onSoundCompleted queue, the held switches and the 2D particle
textures, restores the effects volume, mouse mode, mouse enabled state
and mouse capture, parks the disc on frame 1 paused, and no longer
calls into the closed Lua state when it happens while paused, so the
new script starts exactly as a fresh launch does. Under --reload a
script that fails to compile stays watched, so saving the fix triggers
the reload.
- Quitting with a stick past its dead zone no longer crashes; the
release reaches onInputReleased after onShutdown, before the state
closes. Plugging in a gamepad no longer resets the positions
mouseGetPosition reports, and a controls.cfg mapping that lists the
same code twice fires its switch once.
- materialSetVideo(material) on a framefile disc keeps showing the disc
after a segment change.
- In a packed game the controls.cfg at the root of the data directory
loads again, io.open(".") returns nil with a message instead of a
handle whose first read fails, and a directory listing where a loose
file and a packed one differ only in case shows the loose spelling.
- Error messages: an invalid view, navigation mesh or navigation agent
handle reports "No <thing> N." like every other handle; navAgentNew,
vehicleSetGears and viewNew state their limits from the engine's
constants; fontLoad and soundLoad of a missing file say "Unable to open
NAME" instead of the library's "invalid parameter"; navDraw reports its
argument count, and running out of memory in it is an error rather than
a silent blank; scriptExecute and scriptPush that cannot create the new
data directory abort the script with a message instead of exiting.
materialNew, sceneGetSize and sceneGetStats no longer reject extra
arguments.
SINGE 2.10
==========
Released March 26th, 2024
New Features
------------
- Texture samplers now walk the whole mipmap chain. SDL hands max_lod
straight to Vulkan's maxLod and Direct3D 12's MaxLOD, and the scene
and GUI left it at the zero a memset gives, which clamps every lookup
to mip level 0. Mipmapping was therefore off on every backend:
distant surfaces aliased, the 8x anisotropy did far less than it
looked, and the sky cube could not pick a level by roughness.
- An OpenGL ES 3.1 backend for the 3D scene and the GUI. SDL_GPU has
Vulkan, Direct3D 12 and Metal and no OpenGL of any kind, which left the
cheap Linux handhelds and the Raspberry Pi with 2D only: their Mali and
VideoCore parts have a mature GLES driver and either no Vulkan or an
immature one. The subset of SDL_GPU the scene and the GUI use is now
behind one table of function pointers with two implementations, so both
files stay the single description of what Singe draws, and the backend is
chosen at run time -- one aarch64 binary serves a board with Vulkan and one
without. The ES shaders come from the same HLSL through SPIRV-Cross, and
the GL entry points are loaded through SDL rather than linked, so a machine
with no GLES driver simply keeps its 2D renderer. The Raspberry Pi 4
remains the minimum for 3D: the Pi 3 has only OpenGL ES 2.0, which cannot
express the scene's skinning or morph targets.
- spriteDraw() now has two more forms. In addition to being able to draw
regular sprites and stretched sprites it can now draw both using the
sprite's center as the anchor instead of the upper left. This is highly
useful when dealing with rotated sprites.
- Animated sprites! Both animated GIF and WEBP images are supported.
- Sprite anti-aliasing, scaling and rotation! Optional separate X & Y scaling.
- Video anti-aliasing, scaling and rotation! Optional separate X & Y scaling.
- Multiple audio track support for videos. New command line options to select
which default audio track you want (-o or --audio). For games with multiple
languages, you can specify AUDIO_TRACK in the games.dat as well.
- New keyboard methods to make handling input easier, especially multiple
keypresses for diagonal movement.
- controllerGetButton() added.
- SINGE_DEAD_ZONE global variable now available based on the DEAD_ZONE
controller configuration option.
- SINGE_FRAMEWORK_VERSION variable added.
- DIR variable added providing directory to the running script.
- All new Lua library handling. Previously Singe "injected" libraries
directly into the script context. Now there is a proper Lua module search
handler. Scripts can properly require() modules from the following:
- Lua Standard Library
- Lua Auxiliary Library
- LuaFileSystem
- LuaSocket
- LuaSec
- LuaRS232
- timerwheel
- binaryheap
- Copas
- json
- New command line option: -p (or --program). This is similar to the
existing -t (or --trace) option. Where "trace" displays and logs script
execution, "program" displays and logs Singe internals. This should make
troubleshooting difficult setups and odd script crashes easier.
- Auto-installer for the new GAME, TOOL, and PATCH archive formats to make
managing games easier.
- ZeroBrane Studio (https://studio.zerobrane.com) integration for live
debugging of running Singe scripts! A full IDE for developing games!
Fixes
-----
- USB controllers now work in games launched from the included menu.
- Default controller DEAD_ZONE increased.
- Framework.singe was missing keyboard mode constants. MODE_NORMAL and
MODE_FULL are now included.
- SINGE 2.00 added two new input events but never had them in the framework.
SWITCH_BUTTON4 and SWITCH_TILT are now available.
- Taking screenshots could sometimes crash Singe.
- PNG files no longer generate warnings on the console.
- Non-VLDP videos now honor command line volume settings.
- Menu system now performs "natural sorting" of game titles instead of brain
dead "computer sorting". It should make a lot more sense paging through
games now.
- Building Singe depended on a lot of undocumented software cobbled together
from other projects. This has been vastly improved and should be usable by
actual humans now. Humans running Linux anyway.
------------------------------------------------------------------------------
SINGE 2.00
==========
Released October 25th, 2020
New Features
------------
- Complete rewrite! No more dependency on Daphne.
- Support for almost every (non-patent encumbered) video and audio format
that can be played by FFMPEG.
- High definition video.
- 32 bit color.
- Additional videos can be played on top the laserdisc video.
- Scaled sprites.
- Graphics overlay resolution no longer dependent on video resolution.
- Analog controllers.
- LuaSocket and LuaFileSystem added to runtime.
- Ability to chain scripts together for menus or launchers.
- Specify if you want gun games to display on-screen crosshairs or not.
- Menu for launching games included.
- Many updates to Framework.singe to support the new APIs.
- New APIs:
- controllerGetAxis
- discGetHeight
- discGetWidth
- fontUnload
- mouseGetPosition
- mouseSetCaptured
- overlayBox
- overlayCircle
- overlayEllipse
- overlayLine
- overlayPlot
- overlaySetResolution
- scriptExecute
- scriptPush
- singeWantsCrosshairs
- soundUnload
- spriteGetHeight
- spriteGetWidth
- spriteUnload
- videoDraw
- videoGetFrame
- videoGetFrameCount
- videoGetHeight
- videoGetVolume
- videoGetWidth
- videoIsPlaying
- videoLoad
- videoPause
- videoPlay
- videoSeek
- videoSetVolume
- videoUnload
------------------------------------------------------------------------------
SINGE 1.14 - 1.19
=================
Released ?
Notes
-----
- Unofficial fan builds.
- Stand-alone releases.
New Features
------------
- New API functions:
- discPauseAtFrame
- mouseEnable
- mouseDisable
- mouseHowMany
- mouseSetMode
- singeDisablePauseKey
- singeEnablePauseKey
- singeGetScriptPath
- singeSetGameName
- soundFullStop
- soundGetVolume
- soundIsPlaying
- soundPause
- soundResume
------------------------------------------------------------------------------
SINGE 1.10 - 1.14
=================
Released ?
Notes
-----
- Unofficial fan builds.
- DLL releases for Daphne.
New Features
------------
- New API functions:
- discGetState
- keyboardGetMode
- keyboardSetMode
- singeGetPauseFlag
- singeSetPauseFlag
- singeQuit
- singeVersion
- soundStop
------------------------------------------------------------------------------
SINGE 1.00
==========
Released 2006
Notes
-----
- Original add-on for Daphne.
- Shipped with the ActionMax "emulator" and games.