More video optimizations. Font rendering and GUI optimizations. Logo updates. Manual updates. Lots of stuff!

This commit is contained in:
Scott Duensing 2026-09-26 19:57:06 -05:00
parent 25aa909c3d
commit 67e2a1ddcd
138 changed files with 7339 additions and 2534 deletions

477
CHANGELOG
View file

@ -9,6 +9,53 @@ SINGE 3.00
API Changes API Changes
----------- -----------
- controls.cfg, settings.cfg and gamecontrollerdb.txt are read game first.
The four places are the same, but the file beside the game's script --
what its author shipped -- is now read first, then the machine's own in
the directory Singe was started in, then the one above the data directory,
then the game's data directory, and a later file still wins. Before, the
game's file came last and beat the machine's, so a handheld could not keep
a game from putting its test mode on a face button. A machine file only
replaces what it names: INPUT_TEST_MODE = { SCANCODE.F2 } takes test mode
off whatever button a game gave it.
- gamecontrollerdb.txt now beats SDL_GAMECONTROLLERCONFIG. Systems that set
that variable for their built-in pad -- ArkOS does, with a mapping for the
R36S that leaves Select and Start out -- used to win over any file, so no
game ever saw those buttons. The files the first game finds are added to
the variable before SDL starts, where a later line for a pad replaces an
earlier one. With --program, a pad button its mapping leaves out is traced
with its raw number, so a gamecontrollerdb.txt line can name it.
- A controls.cfg input can be a chord: { GAMEPAD_0.BUTTON_BACK,
GAMEPAD_0.BUTTON_START } as one entry of a mapping fires when the buttons
are held together. Each button keeps its own meaning as well.
- fontPrint keeps the strings it renders, keyed by font, quality, colours and
text, so printing the same text every frame costs about what drawing a
sprite of it does. Rendering text with fontToSprite for performance is
deprecated; it still works, and is still the way to get text that can be
measured, scaled or turned. Strings not printed for a second are dropped,
and a font's go when it is unloaded.
- GUIs are redrawn only when their picture changes. When a frame's document
draws exactly what the last frame drew and nothing new was made in between,
the texture already holds the picture and is kept; changes through RML,
RCSS, the RmlUi Lua interface and the gui* calls are all seen.
guiRefresh(gui) forces a redraw, as a way out should a change ever be
missed; a game should not need it. guiGetStats reports zeros on a frame
where nothing was redrawn.
- videoDraw has the GPU draw a video that is not scaled or turned by
videoScale or videoRotate, when it lands on the overlay where nothing has
been drawn since an overlayClear to a transparent colour: under the
overlay, where it looks the same as the frame copied in and costs next to
nothing. Such a frame is scaled to the screen the way the overlay is,
rather than nearest-neighbour into the overlay first. Loaded videos now
decode the way the disc does, including zero-copy, and are converted to
RGB on the CPU only when a CPU draw needs the pixels, so draw a video
before anything that goes over it rather than after.
- Singe can update itself. Once a day the menu asks the master service - Singe can update itself. Once a day the menu asks the master service
whether a newer release exists for this machine's platform, and says so in whether a newer release exists for this machine's platform, and says so in
a line under the game's details; nothing is fetched until a person opens a line under the game's details; nothing is fetched until a person opens
@ -89,7 +136,7 @@ API Changes
virtual clock like everything else, each frame lasting the time it is virtual clock like everything else, each frame lasting the time it is
worth, and the script runs every step. A bare os.time() answers worth, and the script runs every step. A bare os.time() answers
with the virtual clock too, so a game that seeds its generator from with the virtual clock too, so a game that seeds its generator from
the wall clock when a game starts (the one-player Space Pirates) the wall clock when a game starts (a one-player game does)
repeats; os.time with a date table is Lua's own. repeats; os.time with a date table is Lua's own.
- fontSelect given no font (nil, or an index that was never loaded) - fontSelect given no font (nil, or an index that was never loaded)
@ -612,7 +659,7 @@ API Changes
packed games can be modded by dropping files beside them and files a packed games can be modded by dropping files beside them and files a
game writes land under data/<game>/files. Inside a database names game writes land under data/<game>/files. Inside a database names
are matched without regard to case and a leading own-directory prefix are matched without regard to case and a leading own-directory prefix
("DLe/Cfg/x") is ignored. singe --unpack GAME.game DIRECTORY restores ("MyGame/Cfg/x") is ignored. singe --unpack GAME.game DIRECTORY restores
the files and singe --patch GAME.game SOURCE replaces files from a the files and singe --patch GAME.game SOURCE replaces files from a
directory or a patch database in one transaction. --entry=N picks directory or a patch database in one transaction. --entry=N picks
which games.dat entry a .game launch runs. Loose games are which games.dat entry a .game launch runs. Loose games are
@ -733,7 +780,7 @@ API Changes
that layout; its saves now belong under <base>/<name>/Game/ and must be 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. moved by hand. Archives whose script sits at the root are unaffected.
- Packed games: a path naming the .game file itself (Games/DLe.game, - Packed games: a path naming the .game file itself (Games/MyGame.game,
with or without a trailing separator) addresses the database root, as with or without a trailing separator) addresses the database root, as
"." does from inside: lfs.dir lists it and lfs.attributes reports a "." 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 directory, while io.open on it returns nil and lfs.mkdir and lfs.rmdir
@ -1032,6 +1079,82 @@ API Changes
New Features New Features
------------ ------------
- 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.
- Singe runs on the R36S under ArkOS, and on the Rockchip handhelds like it.
On KMSDRM the OpenGL ES context is asked for before the window is made and
SDL's GLES renderer is chosen first, where SDL used to ask EGL for desktop
OpenGL and fail to make a window. The scene's instance data goes through
texture buffers, since Mali has no storage buffers in vertex shaders;
uniforms are written without waiting for the GPU, uploads orphan their
buffers, and targets are invalidated when a frame is done with them, which
is what a tiling GPU needs to keep from copying.
- Compiled GLES shader programs are kept in the Singe folder of the data
directory and loaded from there next time, and the programs a scene will
need are built at the start rather than on the frame that first uses them,
so the first seconds of a game no longer stutter on a small GPU.
- --scenequality low draws the 3D scene at half resolution, without
antialiasing or bloom, for small GPUs.
- --zerocopy (on by default) hands hardware decoded frames to the GPU as
dma-bufs instead of copying them: Rockchip's MPP on the handhelds, VAAPI on
desktops, imported on the device the picture is drawn on. Where the GPU or
the decoder cannot, frames are copied as before. Rockchip's decoder now
hands out cached buffers, so frames that are copied are read far faster.
- --quitchord=BUTTONS names gamepad buttons that quit when held together,
whatever the game maps and even when it has turned its quit key off: a game
quits back to the menu it came from, and the menu quits Singe. A press of
one of the chord's buttons waits a quarter of a second for the rest before
it does what it normally does, so Start does not launch a game from the
menu on the way to quitting it.
- SIGTERM and SIGINT end all of Singe: the running game shuts down as usual,
and the menu it came from does not come back. A second signal while that
is under way ends the process at once.
- --program traces where each frame's time went, every five seconds: per
loop, the disc, the script, the scene, the GUIs, the overlay upload and the
present, with the script split into its Lua and the engine calls it made,
costliest first, and a line for each video's decoding. --perfsync makes
each stage wait for the GPU, so its time shows up in the stage that caused
it rather than all in present. The manual explains the lines.
- A crash prints a backtrace, on Linux and macOS, to the console and to
trace.txt, so a report can say where it happened and trace.txt alone is a
whole report.
- A resolution larger than the display is fitted to it instead of refused.
- Videos loaded with videoLoad build their frame index in the background, so
loading one no longer holds the frame. The audio queue is longer, which
stops the breakup a slow machine suffered while a video decoded.
- A damaged packet in a video is skipped instead of ending the game; a stream
that keeps failing still does.
- Printing and clearing the overlay cost far less. overlayClear refills only
what was drawn since the last clear, when the colour is the same, and the
overlay is sent to the GPU only where it changed. On the R36S, Dragon's
Lair Enhanced went from 31-38 frames a second to the display's full rate.
- The menu no longer needs its background video as the disc; Menu.sh passes
its own arguments on to Singe.
- Forge can judge a shot by the picture rather than by a hitbox. lightSensor - Forge can judge a shot by the picture rather than by a hitbox. lightSensor
is a behaviour of the framework now, the way a home laserdisc gun works: a is a behaviour of the framework now, the way a home laserdisc gun works: a
spot on the picture flashes on the frames a target is shown, and a shot is spot on the picture flashes on the frames a target is shown, and a shot is
@ -1104,8 +1227,8 @@ New Features
stop, from an entity or a list of them in turn, or at a point; and stop, from an entity or a list of them in turn, or at a point; and
soundDone says when a clip playSound or a sound behaviour started has soundDone says when a clip playSound or a sound behaviour started has
ended. Ports of the test library began (FORGE.md section 14): the ended. Ports of the test library began (FORGE.md section 14): the
five ActionMax games are Forge descriptions written by five light gun games of the VHS era are Forge descriptions written by
util/forgePortActionMax.py, and play the same as the emulator under a porting script, and play the same as the emulator under
the same inputs (testScripts/ports/). On the way: a lightSensor the same inputs (testScripts/ports/). On the way: a lightSensor
behaviour, a pointer behaviour, fonts and anchors on the text look, behaviour, a pointer behaviour, fonts and anchors on the text look,
sprites drawn in copies, discPlay and discPause, pointerX and pointerY sprites drawn in copies, discPlay and discPause, pointerX and pointerY
@ -1116,17 +1239,14 @@ New Features
conditions, actions, layers, or events to the manifest for that game conditions, actions, layers, or events to the manifest for that game
alone; the editor loads it when the game opens and forgets it when alone; the editor loads it when the game opens and forgets it when
the game closes, the compiler checks against it, and the built game the game closes, the compiler checks against it, and the built game
carries it into a release. The ActionMax light sensor lives in one carries it into a release. Their light sensor lives in one now,
now, beside those five games, and not in Forge. The Hypseus library's beside those five games, and not in Forge. The Hypseus library's
eleven Karis Framework 3.32b titles (Arcade Xperience 1 to 3, Astroboy, eleven Karis Framework 3.32b titles are ported by the same converter, which
Danmachi, Freddy, Starship Troopers, Sugar Rush, Survival, Esh's
Aurunmilla, and Badlands Lite) are ported by the same converter, which
finds the framework where a game keeps it and notes its version; the finds the framework where a game keeps it and notes its version; the
rest of that library is surveyed in FORGE.md 14.9. A branch on the rest of that library is surveyed in FORGE.md 14.9. A branch on the
branching track can want its move mashed (mash = 6) or held (hold = branching track can want its move mashed (mash = 6) or held (hold =
20). The seven Kimmy Script Engine titles (Brain Dead 13, Future Boy 20). The seven Kimmy Script Engine titles are ported by the same
Conan, Mazinga Z, Fuma Conspiracy, Sonic 1996, Sintel, Ninja Hayate converter and a kimmy behaviour that
1080) are ported by the same converter and a kimmy behaviour that
plays that engine's loop, and play as their originals do (FORGE.md plays that engine's loop, and play as their originals do (FORGE.md
14.11). 14.11).
util/forgePorts.py util/forgePorts.py
@ -1270,12 +1390,13 @@ New Features
- The menu's intro is drawn, not played, and runs straight into the menu. - The menu's intro is drawn, not played, and runs straight into the menu.
A charge goes off, the Singe dragon and name fly out of it toward the A charge goes off, the Singe dragon and name fly out of it toward the
camera with the dragon's wings beating, the tagline fades in, the dragon camera with the dragon's wings beating, the tagline fades in, the dragon
rears and breathes flame -- and then the grid the menu sits on comes up rears and roars with its mouth open, then rears again and breathes flame
out of the dark beneath the logo while the smoke is still clearing, the -- and then the grid the menu sits on comes up out of the dark beneath
logo lifts away, and the menu takes the screen. The dragon settles the logo while the smoke is still clearing, the logo lifts away, and the
centred on the sun rather than above it, with the name below crossing the menu takes the screen. The dragon faces left and settles centred on the
horizon, so the sun is a disc behind the animal instead of a rim around sun rather than above it, with the name below crossing the horizon, so the
the pair of them. It is one scene from the sun is a disc behind the animal instead of a rim around the pair of
them. It is one scene from the
first frame to the last: there is no cut and nothing loads at the join, first frame to the last: there is no cut and nothing loads at the join,
and the menu takes input when the logo has gone rather than when a video and the menu takes input when the logo has gone rather than when a video
frame number passes. Returning from a game starts the same timeline past frame number passes. Returning from a game starts the same timeline past
@ -1394,7 +1515,9 @@ New Features
the top of the neck; before, every part's node sat at the model's origin the top of the neck; before, every part's node sat at the model's origin
and turning a wing swung it around the middle of the animal. The model and turning a wing swung it around the middle of the animal. The model
looks identical -- no part moves by so much as a thousandth -- but it can looks identical -- no part moves by so much as a thousandth -- but it can
now be animated without rigging it again in script each time. now be animated without rigging it again in script each time. The lower
jaw is a part of its own, hinged at the back of the mouth, so the mouth
can open; the printable variants keep it in the head.
- The manual's 3D Scenes, Physics and Particles chapters are rewritten as - The manual's 3D Scenes, Physics and Particles chapters are rewritten as
sections in a learning order with worked examples, every reference sections in a learning order with worked examples, every reference
@ -1482,28 +1605,6 @@ New Features
Fixes Fixes
----- -----
- Two things on the engine update's Windows path. Windows cannot replace a
running image, so the update starts the new Singe beside this one and lets
this one end; but with a console -- the default -- this one then sat in
getchar() waiting for a key, so two engines were alive at once and the old
one held the console the new one was writing to. The key wait now happens
only when nothing took over, and still happens when the start failed, since
that message is worth reading. And setting the running binary aside as
old-<name> ignored whether the rename worked. It usually does: renaming a
running executable is allowed on Windows as it is everywhere, as long as the
new name is on the same volume, and only deleting one is refused. It can
still be beaten by a folder the user cannot write or by a scanner holding the
file, and that was silent. The tool now says so, on screen and in the trace,
and carries on: the launcher runs the newest Singe in the folder, so the old
binary left in place is harmless.
- The menu's grid, horizon and sun were drawn black and unlit through the
intro, which kept the lights off them but not the blast's glow behind
them: a black line across a flash is a line, and the whole grid showed
through the explosion as its outline. They are not drawn at all until
the grid comes up out of the dark. The recording of the backdrop is
re-made with it.
- A PNG with 16-bit samples and no alpha channel (crate.png in the test - A PNG with 16-bit samples and no alpha channel (crate.png in the test
scripts is one) drew with coloured fringes: SDL_image put libpng's scripts is one) drew with coloured fringes: SDL_image put libpng's
big-endian 48-bit pixels into a surface the machine reads big-endian 48-bit pixels into a surface the machine reads
@ -1512,7 +1613,7 @@ Fixes
- musicPlay answers the handle it was given, as Hypseus's does. Its - musicPlay answers the handle it was given, as Hypseus's does. Its
games keep that (sndHandle = musicPlay(music)) to stop the piece, and games keep that (sndHandle = musicPlay(music)) to stop the piece, and
three of them (Freddy, Starship Troopers, Sugar Rush) died on the map three of them died on the map
screen with "musicStop: Argument 1 must be a number" because Singe's screen with "musicStop: Argument 1 must be a number" because Singe's
answered nothing. musicStop takes a nil handle as none. answered nothing. musicStop takes a nil handle as none.
@ -1525,48 +1626,12 @@ Fixes
gun dead with only "No mice detected" to explain it. Only --manymouse gun dead with only "No mice detected" to explain it. Only --manymouse
needs those devices now, and the message names the group. needs those devices now, and the message names the group.
- A game whose art is missing shows that it is missing, rather than taking
the engine down with it. An empty or wrong decorator path could open
something that is not a file, report a nonsense length, and throw
std::bad_alloc out of the GUI's texture loader -- a typo in a games.dat
ended the program. The loader now refuses an impossible size, and both
menu renderers fall back to Singe/missing.png, drawn at build time, so a
bad path reads as "MISSING art" instead of a hole in the page.
- GUIs and 2D particles no longer blink. guiDraw and emitterDraw are
documented as lasting one frame, meaning one onOverlayUpdate -- but their
queues were emptied after every rendered frame, and the script's turn is
throttled to FRAME_TICK_MS while the display refreshes as often as it
can. With a 3D scene enabled it refreshes continuously, so the GUIs and
the 2D particles were drawn only on the frames the script happened to run
on: in the menu, a game list flickering over a grid that did not. The
queues are emptied when the script gets its turn instead.
- The menu shows packed games properly. A .game is a database the engine
presents through the VFS, so lfs.attributes reports it as a directory,
not a file -- and the menu tested the mode before the extension, so every
container went down the loose-directory branch and its games.dat was
loaded with no container recorded. Every CABINET, MARQUEE and ATTRACT
path in it then named a file that exists only inside the database: a
library of .game files showed no art at all and died with "Unable to
open" on the first attract video. The extension is tested first now.
Games packed with earlier releases are correct and do not need repacking.
- A lone .m2v played with --framefile now finds the .ogg beside it, as a - A lone .m2v played with --framefile now finds the .ogg beside it, as a
framefile's segments always have. A Daphne era elementary stream framefile's segments always have. A Daphne era elementary stream
carries no audio of its own, so lair.m2v played silently unless it was carries no audio of its own, so such a .m2v played silently unless it was
listed in a framefile or --altaudio was given. Any other container listed in a framefile or --altaudio was given. Any other container
keeps using the audio inside it. 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; - --sindengun and --scalefactor are listed in --help and the manual;
they were hidden because mouse positions ignored the Sinden border and they were hidden because mouse positions ignored the Sinden border and
the overscan shrink. Positions now map into the picture actually the overscan shrink. Positions now map into the picture actually
@ -1635,96 +1700,15 @@ Fixes
copy differs from the running build. Upgrading the binary used to keep copy differs from the running build. Upgrading the binary used to keep
the old Framework.singe and Menu.singe forever. the old Framework.singe and Menu.singe forever.
- Lua reads a packed game where it lies. Its file handle is a C FILE * and
a database row is not a file, so anything a script opened by name inside
a .game was written out to data/<game>/cache first and the copy opened
instead -- a second copy of the game on disk, made a file at a time.
Everything else in the engine had long since stopped needing that: the
sprite loader, the decoders and the GUI's file interface all read a row
as a stream. Only Lua could not, because the io library is not ours.
It is patched now rather than worked around: io.open, io.lines and
io.input hand back a handle of Singe's own for a name that lives only in
the database, and it reads through the same stream everything else uses.
It answers to every method and format Lua's does, io.type calls it a
file, io.read and io.close take it, and it closes itself on collection or
on leaving a <close> variable's scope; writing to it is an error, which
is what a packed asset is. testScripts/packedIo holds all of that to
Lua's own behaviour. With the last reader of the cache gone, the cache
itself is gone: vfsFilePath answers with a path only when there is a
real file, and a packed name being read is asked for as a stream.
- The menu no longer unpacks a library's artwork to look at it. It asked
whether each picture was there by opening it, and io.open has to hand Lua
a file the C library can read, so every cabinet and marquee in a packed
library was written out to data/<game>/cache first -- none of which was
ever opened that way: spriteLoad and the document's texture loader both
read straight out of the database. The question is asked of the index
now, through lfs.attributes, which opens nothing. The cache remains for
what it is for: a game's own script opening its own files by name.
- A packed game writes to one data directory. Everything that named a file
inside a .game by a path that passes through it -- a script opening one
of its own videos, which is how the menu loads an attract clip -- got a
data directory named after the container including the extension, while
the container itself, and everything else the same game wrote, used the
name without it. A library of packed games therefore grew two data
directories each: the video indexes in one and the saves, the overlay
and the cache in the other. Any component naming a database is reduced
to its stem now, so both land in the same place. An installation that
has both may delete the ".game" one; what was in it is rebuilt.
- Both menus scroll their details. When a games.dat entry has more to say
than fits, the text walks down by itself a line at a time, rests, and
walks back up to the start rather than jumping there: up and down belong
to the library, and a cabinet has no key to spare for scrolling a
paragraph. The document renderer's description box had a scroll bar the
style sheet gave it and nothing to drive it with; the overlay renderer
clipped and left it at that. Page up and page down override the walk for
whoever does have a keyboard, and it carries on from where they leave it.
Both menus say so along the bottom.
- The overlay menu -- the one a machine with no GPU gets -- shows what a
games.dat says about a game. Its details had the attract video's column
and whatever height was left under it, which at 720x480 was four lines,
so the description used most of them and every fact about the game fell
off the bottom. They run the full width now, under the artwork, with the
short fields grouped onto shared lines the way the document renderer
groups them and the description last rather than first. Cabinet and
marquee artwork is scaled to fit its region as well: spriteDraw blits at
the picture's own size, so art larger than the space allowed for it used
to be drawn straight over whatever was beside it.
- The overlay menu says which key does what, along the bottom, as the
document menu always has. There was no way to learn that up and down
move one game and left and right move a page except to be told.
- Left and right move a third of the library, or a screenful, whichever is - Left and right move a third of the library, or a screenful, whichever is
smaller. A page in a library of nine games was ten, so every press smaller. A page in a library of nine games was ten, so every press
landed on the first game or the last and nothing between them could be landed on the first game or the last and nothing between them could be
reached that way at all. reached that way at all.
- The menu's Start button is gone. Nothing but a mouse could ever reach it
-- the document takes no keyboard focus, and a cabinet has no pointer --
and the line beside it already says which button starts a game. Clicking
a game's row still starts it.
- The menu background video carries a keyframe every half second, and the
first frame of its looping section is one of them, so the menu's jumps
into and around it are instant. It has no audio track and is compressed
harder than the clips it replaced -- flat colour with hard edges is easy
to encode -- so what was 26 MB of source clips and 1.3 MB inside the
binary is now 0.9 MB of picture and 1.1 MB of sound.
- Framefiles whose first line is a relative directory produced garbage - Framefiles whose first line is a relative directory produced garbage
video paths (the directory string was freed before use); a lone "." now video paths (the directory string was freed before use); a lone "." now
means the framefile's own directory. 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. - os.clock() in scripts now returns wall time rather than processor time.
Nearly every existing game times its input debounces and prompts with Nearly every existing game times its input debounces and prompts with
it; once the GPU decodes the video the engine mostly sleeps, and those it; once the GPU decodes the video the engine mostly sleeps, and those
@ -1736,10 +1720,6 @@ Fixes
press in the script that follows. Confirming Exit in a game with press in the script that follows. Confirming Exit in a game with
button 1 used to relaunch it from the menu. 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 - Windows builds now carry the icon and version resource, with the version
number encoded correctly. number encoded correctly.
@ -1772,167 +1752,23 @@ Fixes
discGetFrame reports, so a skip in any segment after the first lands discGetFrame reports, so a skip in any segment after the first lands
where it should. 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 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 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 rebuilding each other's index. Old <name>.index files are ignored and
can be deleted. 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 - --stretch hands the script mouse positions in the game's coordinates
at any window size. --scalefactor with --sindengun shrinks the picture at any window size. --scalefactor with --sindengun shrinks the picture
inside the Sinden border instead of being cancelled by it. A negative inside the Sinden border instead of being cancelled by it. A negative
--audio track number is ignored. With no --datadir the controls.cfg --audio track number is ignored. With no --datadir the controls.cfg
beside the script is read once, not twice. 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 - Quitting with a stick past its dead zone no longer crashes; the
release reaches onInputReleased after onShutdown, before the state release reaches onInputReleased after onShutdown, before the state
closes. Plugging in a gamepad no longer resets the positions closes. Plugging in a gamepad no longer resets the positions
mouseGetPosition reports, and a controls.cfg mapping that lists the mouseGetPosition reports, and a controls.cfg mapping that lists the
same code twice fires its switch once. 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.
- A review of the week's work (the io patch, the scheduler, Forge, the
menu, the GLES backend, saves and the GUI) fixed what it found:
Lua's io on a packed asset: f:lines and io.lines with more than one
format handed back only the last value read a step; a later format
that comes up empty now returns what came before it with nil in its
place, as Lua does, and only an empty first read ends the loop.
read("a") reads in blocks rather than a byte at a time, read(0) is the
end-of-file test it is in Lua, and a NUL byte is no longer taken for
part of a number. testScripts/packedIo covers all of it.
Timer and tween handles carry a generation: a handle kept past its
timer's end no longer cancels, or answers for, whatever timer was
given the same slot afterwards.
Forge: a rule's keyHeld condition never fired from a real keyboard
(it compared a key name against the scancodes the engine delivers);
an entity whose look kind is unknown is drawn as a box with a message
instead of ending the game; and the compiler loads the runtime from
Forge/, where it lives, rather than a Singe/Author.singe that was never
shipped. The games.dat Forge writes no longer carries a DATA key the
engine does not read, and the manual's example loses it too.
The menu: a games.dat entry with no ATTRACT clip no longer stops the
engine on either renderer, quitting with no games listed no longer
fails in onShutdown, and a directory entry lfs cannot describe is
skipped rather than indexed.
The GLES backend freed nothing when its framebuffer cache filled and
was reset; the framebuffer objects go now. A save is written with a
single atomic rename everywhere but Windows, so the old save stays
whole until the new one is in place. A GUI texture decoded with a
padded pitch is packed row by row instead of uploaded with its padding
read as pixels. The menu's backdrop stops rewriting its grid colours
every frame once the grid is lit.
- navAgentNew placed an agent from the node's world position as of the
last drawn frame, so an agent made before the first frame -- or in a
game that never draws the scene, which is what a 2D room walking a
navigation mesh is -- started at the origin, off the mesh, and never
moved. The transforms are refreshed first now, as navAddNode already
did.
Net.singe verified a connection against Linux's CA list by path, so
without a pin every HTTPS request failed on Windows, macOS and the
handhelds. The engine now extracts Mozilla's CA list as
Singe/cacerts.pem with the other support files and Net.singe uses it
wherever the platform's own list is missing. A chunked response --
what an HTTP/1.1 server sends when it does not know the length -- is
unwrapped as it arrives rather than handed to the caller with the chunk
sizes still in it. And a mistyped value in a table given to scriptPush
or scriptExecute is reported like any other bad API argument -- the
script line and the call named -- rather than as a bare exit message.
SINGE 2.10 SINGE 2.10
========== ==========
@ -1942,27 +1778,6 @@ SINGE 2.10
New Features 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 - 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 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 sprite's center as the anchor instead of the upper left. This is highly
@ -2197,5 +2012,5 @@ Notes
----- -----
- Original add-on for Daphne. - Original add-on for Daphne.
- Shipped with the ActionMax "emulator" and games. - Shipped with an "emulator" for a VHS game system, and its games.

View file

@ -172,6 +172,10 @@ function(singeEmbedLua path prefix)
set(EMBEDDED_HEADERS ${EMBEDDED_HEADERS} PARENT_SCOPE) set(EMBEDDED_HEADERS ${EMBEDDED_HEADERS} PARENT_SCOPE)
endfunction() endfunction()
# assets/ holds only what is embedded in Singe. Sources those files are made from (the traced
# artwork, OBJ and STL models, the sound samples) and anything else not embedded live in media/.
# Forge, which ships on its own, lives in forge/.
# Images # Images
singeEmbedImage(font) singeEmbedImage(font)
# The icon is drawn large; the window gets a 128 pixel copy and the Windows .ico the usual sizes, # The icon is drawn large; the window gets a 128 pixel copy and the Windows .ico the usual sizes,
@ -226,7 +230,7 @@ singeEmbed(${CMAKE_SOURCE_DIR}/assets/settings.cfg ${GENERATED_DIR}/settings_cfg
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "")
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Tools.singe ${GENERATED_DIR}/Tools_singe.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/Tools.singe ${GENERATED_DIR}/Tools_singe.h "")
# Forge, the authoring tool, is built from assets/Forge but is NOT embedded: it is distributed on # Forge, the authoring tool, is built from forge but is NOT embedded: it is distributed on
# its own, not with the engine. Nothing of it -- not the editor, not the compiler, not the runtime # its own, not with the engine. Nothing of it -- not the editor, not the compiler, not the runtime
# a game it builds loads -- belongs inside Singe. # a game it builds loads -- belongs inside Singe.
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Net.singe ${GENERATED_DIR}/Net_singe.h "") singeEmbed(${CMAKE_SOURCE_DIR}/assets/Net.singe ${GENERATED_DIR}/Net_singe.h "")
@ -326,13 +330,13 @@ if(LUA_INTERPRETER)
add_custom_command( add_custom_command(
OUTPUT ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc OUTPUT ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc
COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_SOURCE_DIR} ${LUA_INTERPRETER} util/forgeVocabulary.lua > ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_SOURCE_DIR} ${LUA_INTERPRETER} util/forgeVocabulary.lua > ${CMAKE_SOURCE_DIR}/docs/ForgeVocabulary.adoc
DEPENDS assets/Forge/Author.singe util/forgeVocabulary.lua DEPENDS forge/Author.singe util/forgeVocabulary.lua
COMMENT "Writing the Forge vocabulary" COMMENT "Writing the Forge vocabulary"
) )
endif() endif()
# The book embeds the tutorials' pictures and includes their shipped descriptions, so any of # The book embeds the tutorials' pictures and includes their shipped descriptions, so any of
# those changing renders it again; the globs are re-run on every build. # those changing renders it again; the globs are re-run on every build.
file(GLOB_RECURSE FORGE_BOOK_PARTS CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/docs/images/tutorials/* ${CMAKE_SOURCE_DIR}/assets/Forge/tutorials/*.game) file(GLOB_RECURSE FORGE_BOOK_PARTS CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/docs/images/tutorials/* ${CMAKE_SOURCE_DIR}/forge/tutorials/*.game)
add_custom_command( add_custom_command(
OUTPUT ${MANUAL_DIR}/Forge.pdf OUTPUT ${MANUAL_DIR}/Forge.pdf
COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR} COMMAND ${CMAKE_COMMAND} -E make_directory ${MANUAL_DIR}
@ -840,14 +844,22 @@ if(NOT SINGE_SHADERCROSS)
message(FATAL_ERROR "SDL_shadercross is needed to compile the scene shaders; build through the superbuild (cmake --preset ...) or set SINGE_SHADERCROSS.") message(FATAL_ERROR "SDL_shadercross is needed to compile the scene shaders; build through the superbuild (cmake --preset ...) or set SINGE_SHADERCROSS.")
endif() endif()
endif() endif()
# SPIRV-Cross turns the same SPIR-V into GLSL ES for the GLES backend (PLAN.md section 56). Unlike # SPIRV-Cross turns the same SPIR-V into GLSL ES for the GLES backend (PLAN.md section 56). The
# shadercross this one is not fatal when missing: the header then carries null ES strings, the GLES # superbuild builds it beside shadercross and hands it over, so there it is fatal when missing: a
# backend declines to start, and every other platform builds exactly as before. # warning let every release binary ship with null ES strings and no GLES backend, unnoticed.
if(NOT SINGE_SPIRV_CROSS) # Configuring this project alone it is only a warning; the header then carries null ES strings,
find_program(SINGE_SPIRV_CROSS spirv-cross) # the GLES backend declines to start, and every other platform builds exactly as before.
endif() if(SINGE_FROM_SUPERBUILD)
if(NOT SINGE_SPIRV_CROSS) if((NOT SINGE_SPIRV_CROSS) OR (NOT EXISTS ${SINGE_SPIRV_CROSS}))
message(WARNING "spirv-cross was not found; this build will have no OpenGL ES shaders and so no GLES backend.") message(FATAL_ERROR "The superbuild did not supply spirv-cross (SINGE_SPIRV_CROSS='${SINGE_SPIRV_CROSS}'); without it there are no OpenGL ES shaders.")
endif()
else()
if(NOT SINGE_SPIRV_CROSS)
find_program(SINGE_SPIRV_CROSS spirv-cross)
endif()
if(NOT SINGE_SPIRV_CROSS)
message(WARNING "spirv-cross was not found; this build will have no OpenGL ES shaders and so no GLES backend.")
endif()
endif() endif()
set(shaderHeader ${CMAKE_BINARY_DIR}/generated/shaders/sceneShaders.h) set(shaderHeader ${CMAKE_BINARY_DIR}/generated/shaders/sceneShaders.h)
add_custom_command( add_custom_command(

12
INSTALL
View file

@ -69,12 +69,12 @@ libibus-1.0-dev libxkbcommon-dev libx11-dev libxext-dev libxfixes-dev
libxi-dev libxcursor-dev libxrandr-dev libxss-dev libxtst-dev libxi-dev libxcursor-dev libxrandr-dev libxss-dev libxtst-dev
libwayland-dev wayland-protocols libdecor-0-dev. libwayland-dev wayland-protocols libdecor-0-dev.
The artwork, the font and the menu video in assets/ are stored with Git The artwork, the font, the models, and the menu video in assets/ and
LFS. Clone with git-lfs present, or those files arrive as small text media/ are stored with Git LFS. Clone with git-lfs present, or those
pointers and the build embeds the pointers instead of the assets; a files arrive as small text pointers and the build embeds the pointers
checkout already made without it is repaired with "git lfs install && instead of the assets; a checkout already made without it is repaired
git lfs pull". build-all.sh stops with an error rather than build a with "git lfs install && git lfs pull". build-all.sh stops with an
binary around them. error rather than build a binary around them.
The Linux release build and the Windows build use zig as the compiler: The Linux release build and the Windows build use zig as the compiler:
the build fetches a pinned zig release (version and SHA-256 in the build fetches a pinned zig release (version and SHA-256 in

View file

@ -74,7 +74,7 @@ Sounds
The menu's intro (assets/menuIntro.flac, written by util/makeMenuSound.py) is built on three The menu's intro (assets/menuIntro.flac, written by util/makeMenuSound.py) is built on three
recordings from SoundBible, each under the Creative Commons Attribution 3.0 licence recordings from SoundBible, each under the Creative Commons Attribution 3.0 licence
(https://creativecommons.org/licenses/by/3.0/) and kept as FLAC under assets/samples: (https://creativecommons.org/licenses/by/3.0/) and kept as FLAC under media/samples:
Explosion Ultra Bass Mark DiAngelo CC BY 3.0 https://soundbible.com/1807-Explosion-Ultra-Bass.html Explosion Ultra Bass Mark DiAngelo CC BY 3.0 https://soundbible.com/1807-Explosion-Ultra-Bass.html
Large Fireball Mike Koenig CC BY 3.0 https://soundbible.com/1348-Large-Fireball.html Large Fireball Mike Koenig CC BY 3.0 https://soundbible.com/1348-Large-Fireball.html

View file

@ -26,7 +26,8 @@
-- The menu's backdrop: the intro and the grid the menu sits on, as one scene. -- The menu's backdrop: the intro and the grid the menu sits on, as one scene.
-- --
-- A charge goes off, the Singe dragon and name fly out of it toward the camera, the tagline fades -- A charge goes off, the Singe dragon and name fly out of it toward the camera, the tagline fades
-- in underneath, the dragon rears and breathes flame -- and then the grid comes up out of the dark -- in underneath, the dragon rears and roars, then rears again and breathes flame -- and then the
-- grid comes up out of the dark
-- beneath the logo while the smoke is still clearing, the logo lifts away, and what is left is the -- beneath the logo while the smoke is still clearing, the logo lifts away, and what is left is the
-- backdrop the menu sits on. One scene from the first frame to the last: no cut, and nothing -- backdrop the menu sits on. One scene from the first frame to the last: no cut, and nothing
-- loads at the join. -- loads at the join.
@ -107,16 +108,27 @@ local LOGO_Y = 0.15 -- Where the logo settles. It is the dragon that
-- worth re-measuring if either model changes. -- worth re-measuring if either model changes.
-- The dragon carries no animation, but util/dragonModel.py builds it out of named parts -- head, -- The dragon carries no animation, but util/dragonModel.py builds it out of named parts -- head,
-- neck, wingL, wingR and the rest -- and every one of them is its own node. They all sit at the -- jaw, neck, wingL, wingR and the rest -- and every one of them is its own node. They all sit at the
-- model's origin though, so turning a wing node would swing the wing round the dragon's middle -- model's origin though, so turning a wing node would swing the wing round the dragon's middle
-- rather than its shoulder. The joints are baked into the model now; coordinates below are the -- rather than its shoulder. The joints are baked into the model now; coordinates below are the
-- model's own: it faces +X, +Y is up, and the wings reach out along +/-Z. -- model's own: it faces +X, +Y is up, and the wings reach out along +/-Z.
local MOUTH = { 3.55, 5.60, 0.00 } -- Where the flame leaves, in model coordinates. local MOUTH = { 2.88, 5.46, 0.00 } -- Where the flame leaves, in model coordinates: in the gap
local HEAD_AT = { 0.62, 5.69, 0.00 } -- The head's baked joint; see assets/DragonPivots.json. -- between the snout and the jaw once the jaw has dropped, back
-- from the nose so the fire comes out of the mouth, not the nose.
local HEAD_AT = { 0.62, 5.69, 0.00 } -- The head's baked joint; see media/DragonPivots.json.
local FLAP_MAX = 34.0 -- Degrees a wing rises and falls. local FLAP_MAX = 34.0 -- Degrees a wing rises and falls.
local FLAP_FAST = 13.0 -- Radians a second while it is flying out of the blast. local FLAP_FAST = 13.0 -- Radians a second while it is flying out of the blast.
local FLAP_SLOW = 3.4 -- and once it has settled. local FLAP_SLOW = 3.4 -- and once it has settled.
local REAR_BACK = 3.05 -- Seconds: the head starts to rear. local ROAR_BACK = 2.20 -- Seconds: the head starts to rear for the roar, which builds in
-- media/samples/dragonFireBreathAndRoar.flac from here,
local ROAR_TOP = 2.50 -- is fully back as the roar reaches its loudest,
local ROAR_HOLD = 2.85 -- held there until the roar falls away,
-- and down again by REAR_BACK, so the rear for the fire reads as a
-- second one rather than as the roar carrying on.
local ROAR_ANGLE = 24.0 -- Degrees the head tips back to roar.
local JAW_OPEN = -30.0 -- Degrees the lower jaw drops, for the roar and for the fire.
local NECK_SHARE = 0.35 -- How much of the head's pitch the neck takes as well.
local REAR_BACK = 3.05 -- Seconds: the head starts to rear for the fire.
local REAR_TOP = 3.40 -- fully back. local REAR_TOP = 3.40 -- fully back.
local REAR_STRIKE = 3.58 -- and thrown forward. local REAR_STRIKE = 3.58 -- and thrown forward.
local BREATH_START = 3.46 local BREATH_START = 3.46
@ -124,7 +136,7 @@ local BREATH_END = 4.35
local REAR_DONE = 4.90 -- Back to where it started. local REAR_DONE = 4.90 -- Back to where it started.
local REAR_ANGLE = 34.0 -- Degrees the head tips back. local REAR_ANGLE = 34.0 -- Degrees the head tips back.
local STRIKE_ANGLE = -14.0 -- and forward through the strike. local STRIKE_ANGLE = -14.0 -- and forward through the strike.
local DRAGON_YAW = -32.0 -- The dragon alone is turned toward the camera, so the wingbeat is local DRAGON_YAW = -148.0 -- The dragon faces left, turned toward the camera so the wingbeat is
-- seen three quarters on rather than edge on; the name stays square -- seen three quarters on rather than edge on; the name stays square
-- to the camera because it has to be read. -- to the camera because it has to be read.
local TAGLINE = "SINGE Is Not a Game Emulator" local TAGLINE = "SINGE Is Not a Game Emulator"
@ -157,7 +169,7 @@ sceneSetTonemap(TONEMAP_ACES)
-- The dragon's parts carry their own joints now: util/objToGlb.py bakes them from -- The dragon's parts carry their own joints now: util/objToGlb.py bakes them from
-- assets/DragonPivots.json, so wingL turns at the shoulder and head at the top of the neck without -- media/DragonPivots.json, so wingL turns at the shoulder and head at the top of the neck without
-- anything being rigged here. This used to build them at run time from bounding boxes, which -- anything being rigged here. This used to build them at run time from bounding boxes, which
-- worked but left every future use of the model to re-derive the same thing. -- worked but left every future use of the model to re-derive the same thing.
local function joint(root, name, parent) local function joint(root, name, parent)
@ -281,11 +293,14 @@ nodeSetScale(dragon, 0.9)
nodeSetScale(logo, 0.5) nodeSetScale(logo, 0.5)
nodeSetVisible(logo, false) nodeSetVisible(logo, false)
-- The rig. The head hangs off the neck so that rearing the neck carries the head with it. -- The rig. The head hangs off the neck so that rearing the neck carries the head with it, and
-- the jaw off the head for the same reason. Tip first: joint measures the parent where it sits,
-- so the jaw has to be hung on the head while the head is still at its own baked joint rather than
-- already moved to be relative to the neck.
local wingLeft = joint(dragon, "wingL") local wingLeft = joint(dragon, "wingL")
local wingRite = joint(dragon, "wingR") local wingRite = joint(dragon, "wingR")
local neck = joint(dragon, "neck") local neck = joint(dragon, "neck")
-- The head hangs off the neck so that rearing the neck carries the head with it. local jaw = joint(dragon, "jaw", nodeFind("head", dragon))
local head = joint(dragon, "head", neck) local head = joint(dragon, "head", neck)
-- The flame, on a node at the dragon's mouth so it follows the head round. -- The flame, on a node at the dragon's mouth so it follows the head round.
@ -534,10 +549,15 @@ function backdropFrame()
nodeSetRotation(wingLeft, -flap, 0, 0) nodeSetRotation(wingLeft, -flap, 0, 0)
nodeSetRotation(wingRite, flap, 0, 0) nodeSetRotation(wingRite, flap, 0, 0)
-- The head rears back, snaps forward, and settles; the flame goes with the strike. -- The head rears to roar and comes down; then it rears again, snaps forward, and settles,
-- and the flame goes with the strike.
local pitch = 0 local pitch = 0
if t < REAR_TOP then if t < ROAR_TOP then
pitch = ROAR_ANGLE * easeOut(span(t, ROAR_BACK, ROAR_TOP))
elseif t < REAR_BACK then
pitch = ROAR_ANGLE * (1 - span(t, ROAR_HOLD, REAR_BACK))
elseif t < REAR_TOP then
pitch = REAR_ANGLE * span(t, REAR_BACK, REAR_TOP) pitch = REAR_ANGLE * span(t, REAR_BACK, REAR_TOP)
elseif t < REAR_STRIKE then elseif t < REAR_STRIKE then
local k = span(t, REAR_TOP, REAR_STRIKE) local k = span(t, REAR_TOP, REAR_STRIKE)
@ -547,11 +567,21 @@ function backdropFrame()
pitch = STRIKE_ANGLE * (1 - easeOut(span(t, REAR_STRIKE, REAR_DONE))) pitch = STRIKE_ANGLE * (1 - easeOut(span(t, REAR_STRIKE, REAR_DONE)))
end end
nodeSetRotation(head, 0, 0, pitch) nodeSetRotation(head, 0, 0, pitch)
nodeSetRotation(neck, 0, 0, pitch * 0.35) nodeSetRotation(neck, 0, 0, pitch * NECK_SHARE)
-- The mouth opens as the head goes back for the roar and shuts as it comes down, then
-- opens again for the fire and stays open until the flame is spent.
local roarJaw = span(t, ROAR_BACK, ROAR_TOP) * (1 - span(t, ROAR_HOLD, REAR_BACK))
local fireJaw = span(t, REAR_BACK, BREATH_START) * (1 - span(t, BREATH_END, REAR_DONE))
local jawPitch = JAW_OPEN * math.max(roarJaw, fireJaw)
nodeSetRotation(jaw, 0, 0, jawPitch)
-- The flame leaves the mouth along the dragon's own forward, which the whole logo has been -- The flame leaves the mouth along the dragon's own forward, which the whole logo has been
-- turning; a 3D emitter takes its direction in world axes, so it is turned here by hand. -- turning; a 3D emitter takes its direction in world axes, so it is turned here by hand.
local aim = math.rad(pitch) -- The head rides on the neck, so its pitch in the world is its own plus the neck's, and the
-- flame follows the middle of the open mouth, half the jaw's drop below the head's line.
local aim = math.rad(pitch * (1 + NECK_SHARE) + jawPitch / 2)
local spin = math.rad(yaw + DRAGON_YAW) local spin = math.rad(yaw + DRAGON_YAW)
emitterSetDirection(breath, math.cos(aim) * math.cos(spin), math.sin(aim), -math.cos(aim) * math.sin(spin)) emitterSetDirection(breath, math.cos(aim) * math.cos(spin), math.sin(aim), -math.cos(aim) * math.sin(spin))

BIN
assets/DragonModel.glb (Stored with Git LFS)

Binary file not shown.

View file

@ -486,6 +486,9 @@ if GAME_COUNT == 0 then
else else
overlaySetResolution(discGetWidth(), discGetHeight()) overlaySetResolution(discGetWidth(), discGetHeight())
-- No disc: the menu is laid out on the canvas, 720x480 unless --canvas says otherwise, and the
-- renderer that needs Singe/menuBackground.mkv loads it itself.
--
-- Three frames of Singe/menuBackground.mkv: where the menu may take the screen, where the -- Three frames of Singe/menuBackground.mkv: where the menu may take the screen, where the
-- grid section it loops begins, and its last frame. The first two are not the same moment -- -- grid section it loops begins, and its last frame. The first two are not the same moment --
-- the menu comes up while the music is still fading out behind it, and only then does the -- the menu comes up while the music is still fading out behind it, and only then does the

View file

@ -74,6 +74,7 @@ local hintFont = nil -- Smaller: the hints are a footnote, and at the
-- of them ran off the end of a 720 pixel overlay. -- of them ran off the end of a 720 pixel overlay.
local titleFont = nil -- Larger: a tool's title on its bar. local titleFont = nil -- Larger: a tool's title on its bar.
local introDone = false local introDone = false
local backdrop = nil -- Singe/menuBackground.mkv, played here rather than as the menu's disc
local videoX, videoY, videoW, videoH = 0, 0, 0, 0 local videoX, videoY, videoW, videoH = 0, 0, 0, 0
local marqueeX, marqueeY, marqueeW, marqueeH = 0, 0, 0, 0 local marqueeX, marqueeY, marqueeW, marqueeH = 0, 0, 0, 0
local textX, textY, textW, textH = 0, 0, 0, 0 local textX, textY, textW, textH = 0, 0, 0, 0
@ -316,41 +317,52 @@ MENU_RENDER.finish = function()
end end
end end
font, hintFont, titleFont = nil, nil, nil font, hintFont, titleFont = nil, nil, nil
if backdrop then
videoUnload(backdrop)
backdrop = nil
end
end end
-- The backdrop is the disc: the engine intro, then the grid section played over and over. A -- The backdrop is a recording: the engine intro, then the grid section played over and over. A
-- machine with no GPU device has nothing to draw a grid with, so this renderer plays the one in -- machine with no GPU device has nothing to draw a grid with, so this renderer plays the one in
-- Singe/menuBackground.mkv; Singe/MenuDocument.singe draws its own instead. -- Singe/menuBackground.mkv; Singe/MenuDocument.singe draws its own instead. It is loaded here
-- rather than handed to the menu as its disc, so the renderer that draws its own never opens it.
MENU_RENDER.backdropBegin = function(showIntro) MENU_RENDER.backdropBegin = function(showIntro)
introDone = not showIntro introDone = not showIntro
discPlay() backdrop = videoLoad("Singe/menuBackground.mkv")
videoPlay(backdrop)
if not showIntro then if not showIntro then
discSkipToFrame(DISC_GRID_START) videoSeek(backdrop, DISC_GRID_START)
end end
end end
MENU_RENDER.backdrop = function() MENU_RENDER.backdrop = function()
-- Loop the grid. A machine too slow to show every disc frame can step over the last one, after -- Loop the grid. A machine too slow to show every frame can step over the last one, after which
-- which the engine starts the disc again from the top; once the intro has played, that is a loop. -- the player starts the video again from the top; once the intro has played, that is a loop.
if discGetFrame() >= DISC_LAST_FRAME or (introDone and discGetFrame() < DISC_GRID_START) then local frame = videoGetFrame(backdrop)
discSkipToFrame(DISC_GRID_START)
if frame >= DISC_LAST_FRAME or (introDone and frame < DISC_GRID_START) then
videoSeek(backdrop, DISC_GRID_START)
end end
if discGetFrame() >= DISC_GRID_START then if frame >= DISC_GRID_START then
introDone = true introDone = true
end end
-- Transparent, so the disc behind the overlay is what shows. -- The recording fills the overlay, and everything else is drawn over it. Cleared see-through
-- rather than black, which looks the same under a picture that covers everything, so the engine
-- can have the GPU draw the recording instead of copying every frame of it into the overlay.
colorBackground(0, 0, 0, 0) colorBackground(0, 0, 0, 0)
overlayClear() overlayClear()
videoDraw(backdrop, 0, 0, overlayGetWidth() - 1, overlayGetHeight() - 1)
end end
MENU_RENDER.backdropReady = function() MENU_RENDER.backdropReady = function()
-- The menu comes up where the drawn backdrop's logo leaves, which is before the picture starts -- The menu comes up where the drawn backdrop's logo leaves, which is before the picture starts
-- repeating: the frames between the two are the grid running on while the music fades out. -- repeating: the frames between the two are the grid running on while the music fades out.
return discGetFrame() >= DISC_MENU_FRAME return videoGetFrame(backdrop) >= DISC_MENU_FRAME
end end

View file

@ -786,11 +786,17 @@ TOOLS[#TOOLS + 1] = {
discResults = {} discResults = {}
discPending = nil discPending = nil
discWorst = 0 discWorst = 0
discShow("Button 1 runs the test.") -- The menu runs without a disc of its own, so there is only something to time when it was
-- started with one (-v or a framefile on its command line).
if discGetFrameCount() == 0 then
discShow("There is no disc to test: start the menu with -v to time one.")
else
discShow("Button 1 runs the test.")
end
end, end,
input = function(what) input = function(what)
if what == SWITCH_BUTTON1 and not discPending then if what == SWITCH_BUTTON1 and not discPending and (discGetFrameCount() > 0) then
discResults = {} discResults = {}
discWorst = 0 discWorst = 0
discPending = { step = 0, started = 0 } discPending = { step = 0, started = 0 }

View file

@ -44,7 +44,9 @@ DEAD_ZONES = {
TRIGGER_THRESHOLD = 0 TRIGGER_THRESHOLD = 0
-- One table per switch, in the order the engine numbers them (SWITCH_UP = 0 ...). -- One table per switch, in the order the engine numbers them (SWITCH_UP = 0 ...).
-- Each entry is a { name, value } pair from the SCANCODE, GAMEPAD_N, or MOUSE_N tables. -- Each entry is a { name, value } pair from the SCANCODE, GAMEPAD_N, or MOUSE_N tables, or a
-- chord: a list of two to four of them held together, such as
-- { GAMEPAD_0.BUTTON_BACK, GAMEPAD_0.BUTTON_START }. Each keeps its own meaning as well.
INPUT_UP = { SCANCODE.UP, SCANCODE.KP_8, GAMEPAD_0.AXIS_LEFT_Y_U, GAMEPAD_0.AXIS_RIGHT_Y_U, GAMEPAD_0.DPAD_UP } INPUT_UP = { SCANCODE.UP, SCANCODE.KP_8, GAMEPAD_0.AXIS_LEFT_Y_U, GAMEPAD_0.AXIS_RIGHT_Y_U, GAMEPAD_0.DPAD_UP }
INPUT_LEFT = { SCANCODE.LEFT, SCANCODE.KP_4, GAMEPAD_0.AXIS_LEFT_X_L, GAMEPAD_0.AXIS_RIGHT_X_L, GAMEPAD_0.DPAD_LEFT } INPUT_LEFT = { SCANCODE.LEFT, SCANCODE.KP_4, GAMEPAD_0.AXIS_LEFT_X_L, GAMEPAD_0.AXIS_RIGHT_X_L, GAMEPAD_0.DPAD_LEFT }
INPUT_DOWN = { SCANCODE.DOWN, SCANCODE.KP_2, GAMEPAD_0.AXIS_LEFT_Y_D, GAMEPAD_0.AXIS_RIGHT_Y_D, GAMEPAD_0.DPAD_DOWN } INPUT_DOWN = { SCANCODE.DOWN, SCANCODE.KP_2, GAMEPAD_0.AXIS_LEFT_Y_D, GAMEPAD_0.AXIS_RIGHT_Y_D, GAMEPAD_0.DPAD_DOWN }

BIN
assets/menuBackground.mkv (Stored with Git LFS)

Binary file not shown.

View file

@ -28,10 +28,10 @@
-- from the same four places it reads controls.cfg, in this order, and a later -- from the same four places it reads controls.cfg, in this order, and a later
-- one wins key by key: -- one wins key by key:
-- --
-- 1. the directory Singe was started in -- 1. the game's own script directory: what the game ships
-- 2. the directory above the game's data directory -- 2. the directory Singe was started in: the machine's own
-- 3. the game's data directory -- 3. the directory above the game's data directory
-- 4. the game's own script directory -- 4. the game's data directory: yours, for that one game
-- --
-- Every key is the long name of a command line option without the leading -- Every key is the long name of a command line option without the leading
-- dashes, so there is no second vocabulary to learn, and the command line -- dashes, so there is no second vocabulary to learn, and the command line
@ -57,6 +57,7 @@
-- fullscreen_window = false -- borderless window covering the desktop -- fullscreen_window = false -- borderless window covering the desktop
-- nologos = false -- skip the splash screens -- nologos = false -- skip the splash screens
-- canvas = "720x480" -- world size for a game with no disc -- canvas = "720x480" -- world size for a game with no disc
-- scenequality = "high" -- 3D detail: high, or low for a small GPU (half resolution, no antialiasing, no bloom)
-- Where the picture sits ------------------------------------------------- -- Where the picture sits -------------------------------------------------
@ -114,6 +115,7 @@
-- idleexit = 0 -- quit after this many seconds with no input; 0 never does -- idleexit = 0 -- quit after this many seconds with no input; 0 never does
-- no_update_check = false -- never look for a newer Singe from the menu (Engine Update still works by hand) -- no_update_check = false -- never look for a newer Singe from the menu (Engine Update still works by hand)
-- quitchord = "back+start" -- first gamepad buttons held together that quit whatever a game maps
-- fvalue = 0 -- one number handed to the game, which reads it with getFValue() -- fvalue = 0 -- one number handed to the game, which reads it with getFValue()
@ -121,9 +123,11 @@
-- reload = false -- rerun the game when one of its loose script files changes -- reload = false -- rerun the game when one of its loose script files changes
-- program = false -- trace engine activity to trace.txt -- program = false -- trace engine activity to trace.txt
-- perfsync = false -- with program, show the GPU's time in each stage of the perf lines; slows everything
-- trace = false -- trace every Lua call to trace.txt -- trace = false -- trace every Lua call to trace.txt
-- showcalculated = false -- print the frame ranges of every framefile segment -- showcalculated = false -- print the frame ranges of every framefile segment
-- softwarevideo = false -- decode video in software even when the machine can do it in hardware -- softwarevideo = false -- decode video in software even when the machine can do it in hardware
-- zerocopy = true -- hand hardware decoded frames to the GPU without copying them, where it can
-- deinterlace = "auto" -- what an interlaced picture gets: off, auto or on. A laserdisc held -- deinterlace = "auto" -- what an interlaced picture gets: off, auto or on. A laserdisc held
-- interlaced fields, and a rip that kept them combs on a progressive -- interlaced fields, and a rip that kept them combs on a progressive
-- display; auto touches only the frames the file marks interlaced -- display; auto touches only the frames the file marks interlaced

View file

@ -117,7 +117,7 @@ function buildAll() {
} }
# assets/ holds the artwork, the font and the menu video in Git LFS. A clone made without git-lfs # assets/ and media/ hold the artwork, the font, the models, and the menu video in Git LFS. A clone made without git-lfs
# leaves pointer stubs -- a hundred and thirty bytes of text where a video should be -- and the # leaves pointer stubs -- a hundred and thirty bytes of text where a video should be -- and the
# resource generators embed them without complaining, so the failure only shows up at run time as a # resource generators embed them without complaining, so the failure only shows up at run time as a
# menu with no background. One sentinel is enough to tell the two apart. # menu with no background. One sentinel is enough to tell the two apart.
@ -213,7 +213,7 @@ sudo apt-get install -y "${G_HOSTPACKAGES[@]}"
if ! checkoutHydrated; then if ! checkoutHydrated; then
echo "error: assets/ holds git-lfs pointer files, not the real artwork, font and video." echo "error: assets/ and media/ hold git-lfs pointer files, not the real artwork, font and video."
echo " The build would embed those stubs and produce a broken binary." echo " The build would embed those stubs and produce a broken binary."
echo " Fix the checkout with: git lfs install && git lfs pull" echo " Fix the checkout with: git lfs install && git lfs pull"
exit 1 exit 1

View file

@ -450,7 +450,7 @@ ExternalProject_Add(singe
SOURCE_DIR ${CMAKE_SOURCE_DIR} SOURCE_DIR ${CMAKE_SOURCE_DIR}
BINARY_DIR ${singeBinary} BINARY_DIR ${singeBinary}
DEPENDS zlib zstd SDL3 SDL3_image SDL3_mixer SDL3_ttf openssl ffmpeg Jolt recastnavigation RmlUi shadercross DEPENDS zlib zstd SDL3 SDL3_image SDL3_mixer SDL3_ttf openssl ffmpeg Jolt recastnavigation RmlUi shadercross
CONFIGURE_COMMAND ${SB_ENV} ${CMAKE_COMMAND} -S ${CMAKE_SOURCE_DIR} -B ${singeBinary} ${SB_CMAKE_ARGS} -DSINGE_SHADERCROSS=${SINGE_SHADERCROSS} -DSINGE_SUPERBUILD=OFF -DKANGAROO_OS=${KANGAROO_OS} -DKANGAROO_ARCH=${KANGAROO_ARCH} -DSINGE_TREE=${SINGE_TREE} CONFIGURE_COMMAND ${SB_ENV} ${CMAKE_COMMAND} -S ${CMAKE_SOURCE_DIR} -B ${singeBinary} ${SB_CMAKE_ARGS} -DSINGE_SHADERCROSS=${SINGE_SHADERCROSS} -DSINGE_SPIRV_CROSS=${SINGE_SPIRV_CROSS} -DSINGE_FROM_SUPERBUILD=ON -DSINGE_SUPERBUILD=OFF -DKANGAROO_OS=${KANGAROO_OS} -DKANGAROO_ARCH=${KANGAROO_ARCH} -DSINGE_TREE=${SINGE_TREE}
BUILD_COMMAND ${SB_ENV} ${CMAKE_COMMAND} --build ${singeBinary} --parallel ${SB_JOBS} BUILD_COMMAND ${SB_ENV} ${CMAKE_COMMAND} --build ${singeBinary} --parallel ${SB_JOBS}
INSTALL_COMMAND ${CMAKE_COMMAND} -E copy ${singeBinary}/${singeBinaryBase}${CMAKE_EXECUTABLE_SUFFIX} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName} INSTALL_COMMAND ${CMAKE_COMMAND} -E copy ${singeBinary}/${singeBinaryBase}${CMAKE_EXECUTABLE_SUFFIX} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName}
BUILD_ALWAYS ON BUILD_ALWAYS ON
@ -458,15 +458,17 @@ ExternalProject_Add(singe
# Forge, packed into .builddir beside the binary so it can be copied out and tested. It is NOT # Forge, packed into .builddir beside the binary so it can be copied out and tested. It is NOT
# part of the engine and nothing of it is embedded; this only puts the distributable where the # part of the engine and nothing of it is embedded; this only puts the distributable where the
# binaries already are. Only a build that produces a runnable binary can pack it, since packing is # binaries already are. Only a build that produces a binary this machine can run can pack it, since
# the engine's own --pack, so cross builds skip it and the host build does the work. The manual, # packing is the engine's own --pack: the architecture has to match as well as the system, or the
# shell is handed an ARM executable and answers "Syntax error". Every other build skips it and the
# host build does the work. The manual,
# rendered by the engine's own build into .builddir, goes in beside the scripts: Forge copies it # rendered by the engine's own build into .builddir, goes in beside the scripts: Forge copies it
# out to its data directory on its first run. # out to its data directory on its first run.
if((KANGAROO_OS STREQUAL "linux") AND (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")) if((KANGAROO_OS STREQUAL "linux") AND (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") AND (KANGAROO_ARCH STREQUAL CMAKE_HOST_SYSTEM_PROCESSOR))
# Everything under assets/Forge -- the scripts, the kit, the tutorials -- and re-globbed on # Everything under forge/ -- the scripts, the kit, the tutorials -- and re-globbed on
# every build, so a file added or changed anywhere in it repacks Forge.game without anyone # every build, so a file added or changed anywhere in it repacks Forge.game without anyone
# having to remember. # having to remember.
file(GLOB_RECURSE forgeSources CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/assets/Forge/*) file(GLOB_RECURSE forgeSources CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/forge/*)
set(forgePack ${CMAKE_SOURCE_DIR}/.builddir/Forge.game) set(forgePack ${CMAKE_SOURCE_DIR}/.builddir/Forge.game)
set(forgeManual ${CMAKE_SOURCE_DIR}/.builddir/Forge.pdf) set(forgeManual ${CMAKE_SOURCE_DIR}/.builddir/Forge.pdf)
set(forgeStage ${SB_PREFIX}/forgepack) set(forgeStage ${SB_PREFIX}/forgepack)
@ -474,7 +476,7 @@ if((KANGAROO_OS STREQUAL "linux") AND (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux"))
OUTPUT ${forgePack} OUTPUT ${forgePack}
# Packing refuses to overwrite, and a stale pack is worse than none. # Packing refuses to overwrite, and a stale pack is worse than none.
COMMAND ${CMAKE_COMMAND} -E rm -rf ${forgePack} ${forgeStage} COMMAND ${CMAKE_COMMAND} -E rm -rf ${forgePack} ${forgeStage}
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/assets/Forge ${forgeStage}/Forge COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/forge ${forgeStage}/Forge
COMMAND ${CMAKE_COMMAND} -E copy ${forgeManual} ${forgeStage}/Forge/Forge.pdf COMMAND ${CMAKE_COMMAND} -E copy ${forgeManual} ${forgeStage}/Forge/Forge.pdf
# The samples that need no art beyond what the engine ships, so the chooser has something to # The samples that need no art beyond what the engine ships, so the chooser has something to
# open on a fresh install. They come from the test set, which is the one copy of them. # open on a fresh install. They come from the test set, which is the one copy of them.

View file

@ -25,7 +25,9 @@ set(entries ${ENTRIES})
set(formats SPIRV DXIL MSL) set(formats SPIRV DXIL MSL)
# GLSL ES 3.10 for the GLES backend. Three switches matter and each is load bearing: # GLSL ES 3.10 for the GLES backend. Three switches matter and each is load bearing:
# --es --version 310 ES 3.1, the floor the scene needs; its storage buffers are ES 3.1 only. # --es --version 310 ES 3.1, the floor the scene needs. The vertex stage's storage buffers become texture
# buffers in this form (SINGE_ESSL below), since ES lets a vertex shader have no
# storage blocks and Mali drivers give it none; renderGles.c adjusts the extension.
# --combined-samplers-inherit-bindings # --combined-samplers-inherit-bindings
# HLSL's separate Texture2D and SamplerState have no ES equivalent, so SPIRV-Cross folds # HLSL's separate Texture2D and SamplerState have no ES equivalent, so SPIRV-Cross folds
# each pair into one sampler and keeps the HLSL t-register as the binding, which is the # each pair into one sampler and keeps the HLSL t-register as the binding, which is the
@ -71,12 +73,18 @@ foreach(entry IN LISTS entries)
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," bytes "${hex}") string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," bytes "${hex}")
string(APPEND header "static const unsigned char _${name}${format}[] = {\n\t${bytes}\n};\n\n") string(APPEND header "static const unsigned char _${name}${format}[] = {\n\t${bytes}\n};\n\n")
endforeach() endforeach()
# The ES form, cross compiled from the SPIR-V above. Emitted as a C string rather than bytes because that is what # The ES form, cross compiled from its own SPIR-V: the same source with SINGE_ESSL defined, which reads the
# glShaderSource takes; the escaping is only quotes and backslashes, neither of which GLSL ES has a use for. # vertex stage's storage buffers as texture buffers. Emitted as a C string rather than bytes because that is
# what glShaderSource takes; the escaping is only quotes and backslashes, neither of which GLSL ES has a use for.
set(esslSymbol "NULL") set(esslSymbol "NULL")
if(SPIRVCROSS) if(SPIRVCROSS)
set(esslSpirv ${work}/${name}.essl.spirv)
execute_process(COMMAND ${SHADERCROSS} ${SOURCE} -s HLSL -d SPIRV -t ${stage} -e ${name} -I ${includeDir} -DSINGE_ESSL -o ${esslSpirv} RESULT_VARIABLE code OUTPUT_VARIABLE out ERROR_VARIABLE err)
if(NOT code EQUAL 0)
message(FATAL_ERROR "Shader ${name} (SPIR-V for GLSL ES) failed to compile:\n${out}\n${err}")
endif()
set(essl ${work}/${name}.essl) set(essl ${work}/${name}.essl)
execute_process(COMMAND ${SPIRVCROSS} ${esslFlags} ${work}/${name}.spirv --output ${essl} RESULT_VARIABLE code OUTPUT_VARIABLE out ERROR_VARIABLE err) execute_process(COMMAND ${SPIRVCROSS} ${esslFlags} ${esslSpirv} --output ${essl} RESULT_VARIABLE code OUTPUT_VARIABLE out ERROR_VARIABLE err)
if(NOT code EQUAL 0) if(NOT code EQUAL 0)
message(FATAL_ERROR "Shader ${name} (GLSL ES) failed to cross compile:\n${out}\n${err}") message(FATAL_ERROR "Shader ${name} (GLSL ES) failed to cross compile:\n${out}\n${err}")
endif() endif()

View file

@ -130,7 +130,7 @@ in the chooser is this file.
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/01-tenMinutes.forge[] include::../forge/tutorials/01-tenMinutes.forge[]
---- ----
=== Tutorial 2: A Platformer === Tutorial 2: A Platformer
@ -231,7 +231,7 @@ is the three types, their entities, the three rules, and the second room.
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/02-platformer.forge[] include::../forge/tutorials/02-platformer.forge[]
---- ----
=== Tutorial 3: Sprites and Sound === Tutorial 3: Sprites and Sound
@ -311,7 +311,7 @@ states are in <<_instances_vars_and_states>> and
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/03-sprites.forge[] include::../forge/tutorials/03-sprites.forge[]
---- ----
=== Tutorial 4: A Shoot-Em-Up === Tutorial 4: A Shoot-Em-Up
@ -412,7 +412,7 @@ runs once per instance of a type, with that instance as `self`, and a
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/04-shmup.forge[] include::../forge/tutorials/04-shmup.forge[]
---- ----
=== Tutorial 5: A Quick-Time Event === Tutorial 5: A Quick-Time Event
@ -475,7 +475,7 @@ image::tutorials/05-rules.png[The rules, 600]
**What happened.** A `disc` layer plays a video underneath everything, and **What happened.** A `disc` layer plays a video underneath everything, and
the rules read its *frame*: `discBetween` is true while the disc is inside the rules read its *frame*: `discBetween` is true while the disc is inside
a window, `frameReached` fires as it passes a frame, and `discTo` sends it a window, `frameReached` fires as it passes a frame, and `discTo` sends it
elsewhere -- the whole of a Dragon's Lair is windows and jumps. `once` elsewhere -- the whole of a branching laserdisc game is windows and jumps. `once`
keeps a rule from firing every frame the window is open, and `keyPressed` keeps a rule from firing every frame the window is open, and `keyPressed`
is the frame a key goes down, where `keyHeld` is every frame it is down. is the frame a key goes down, where `keyHeld` is every frame it is down.
`setVar` and `test` on a var of your own are how a rule remembers what `setVar` and `test` on a var of your own are how a rule remembers what
@ -493,14 +493,14 @@ released game, `games.dat` names the video, as it does for any Singe game.
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/05-qte.forge[] include::../forge/tutorials/05-qte.forge[]
---- ----
=== Tutorial 6: A Light Gun Game === Tutorial 6: A Light Gun Game
image::tutorials/06-playing.png[Draw, 600] image::tutorials/06-playing.png[Draw, 600]
**What you will make.** Mad Dog McCree in miniature: the clip plays, a **What you will make.** A light gun shooting gallery in miniature: the clip plays, a
target rises, and the player shoots it with the mouse or a light gun. The target rises, and the player shoots it with the mouse or a light gun. The
hand that reaches in is not to be shot. Ammo runs out and is reloaded by a hand that reaches in is not to be shot. Ammo runs out and is reloaded by a
shot off the picture. shot off the picture.
@ -579,7 +579,7 @@ with a light gun needs -- calibration, two guns, the recoil -- is in
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/06-gun.forge[] include::../forge/tutorials/06-gun.forge[]
---- ----
=== Tutorial 7: A Point-and-Click Adventure === Tutorial 7: A Point-and-Click Adventure
@ -706,7 +706,7 @@ off. All of it is in
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/07-adventure.forge[] include::../forge/tutorials/07-adventure.forge[]
---- ----
=== Tutorial 8: Into 3D === Tutorial 8: Into 3D
@ -801,7 +801,7 @@ viewport in <<_the_3d_viewport>>.
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/08-scene.forge[] include::../forge/tutorials/08-scene.forge[]
---- ----
=== Tutorial 9: A Rail Shooter === Tutorial 9: A Rail Shooter
@ -920,7 +920,7 @@ in <<_games_in_the_scene>>; painted rooms in
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/09-rail.forge[] include::../forge/tutorials/09-rail.forge[]
---- ----
=== Tutorial 10: Releasing Your Game === Tutorial 10: Releasing Your Game
@ -1003,7 +1003,7 @@ is what a cabinet's coin door shows. Releasing is in
[source,lua] [source,lua]
---- ----
include::../assets/Forge/tutorials/10-release.forge[] include::../forge/tutorials/10-release.forge[]
---- ----
=== Recipes === Recipes
@ -1065,7 +1065,7 @@ ones.
*The board and the bezel* (`fmv.forge`). `bezel` round any game, `credit` *The board and the bezel* (`fmv.forge`). `bezel` round any game, `credit`
on the coin switch, `restart` on start with credits, and `submitScore` at on the coin switch, `restart` on start with credits, and `submitScore` at
the end; `branching` for a Dragon's Lair of windows, moves, and jumps. the end; `branching` for a branching game of windows, moves, and jumps.
*Anything else.* The `lua` action runs a line of Singe Lua in a rule, with *Anything else.* The `lua` action runs a line of Singe Lua in a rule, with
`self`, `other`, and `event` in scope, and the engine's whole API with them: `self`, `other`, and `event` in scope, and the engine's whole API with them:
@ -1309,7 +1309,7 @@ row of bullets or of hearts; the `pointer` behaviour keeps an instance on
the player's pointer, a crosshair, and hides it when the engine says a real the player's pointer, a crosshair, and hides it when the engine says a real
gun wants none drawn; `discPlay` and `discPause` let the disc go and hold gun wants none drawn; `discPlay` and `discPause` let the disc go and hold
it, a still behind a title; `pointerX(1)` and `pointerY(1)` in an it, a still behind a title; `pointerX(1)` and `pointerY(1)` in an
expression say where the pointer is. The ActionMax way of judging a shot expression say where the pointer is. The light sensor way of judging a shot
over video -- the picture under the gun agreeing with a sensor spot across over video -- the picture under the gun agreeing with a sensor spot across
two frames -- is a `lightSensor` behaviour those five games carry in a two frames -- is a `lightSensor` behaviour those five games carry in a
vocabulary of their own (see <<own-vocabulary>>), not one of Forge's. vocabulary of their own (see <<own-vocabulary>>), not one of Forge's.
@ -1323,7 +1323,7 @@ game's script. The game's script and its `Script/addons.singe` run inside
the behaviour, so a game keeps its own scoring and level order. The the behaviour, so a game keeps its own scoring and level order. The
seventeen games of the test library and eleven of the Hypseus library are seventeen games of the test library and eleven of the Hypseus library are
ported this way and play as their originals do (FORGE.md section 14), and ported this way and play as their originals do (FORGE.md section 14), and
so is LINEA, the one game on the MazescaterFramework -- that framework so is the one game on the MazescaterFramework -- that framework
with compound moves (sequences of two or three inputs, circles from any with compound moves (sequences of two or three inputs, circles from any
start, mashes of a direction or a button, a direction with the second or start, mashes of a direction or a button, a direction with the second or
third button), which the behaviour judges from tables of its own once the third button), which the behaviour judges from tables of its own once the
@ -1343,17 +1343,16 @@ where the game carries one, a map between levels -- with the copies'
drift (the later generation's level order, game type, and state numbers) drift (the later generation's level order, game type, and state numbers)
gated by what the game's script declares, and what a title does without gated by what the game's script declares, and what a title does without
declaring it named in a `tweaks` table the converter writes. The declaring it named in a `tweaks` table the converter writes. The
`timegal` behaviour plays the same author's Time Gal remake, a compact `timegal` behaviour plays one of the same author's remakes, a compact
loop of its own (checkpoints, a time-stop choice of three, a tally by loop of its own (checkpoints, a time-stop choice of three, a tally by
deaths, a life per quota, a level order by tiers), over the game's own deaths, a life per quota, a level order by tiers), over the game's own
files in the same way; and the `sdq` behaviour plays Super Don Quixote, files in the same way; and the `sdq` behaviour plays one game that is
the one hybrid of map-mode data under the Karis framework's judging, the one hybrid of map-mode data under the Karis framework's judging,
with its difficulty select, level clear, and trophies. with its difficulty select, level clear, and trophies.
*A hand-written game.* A game that was a program of its own rather than *A hand-written game.* A game that was a program of its own rather than
a framework's tables -- the American Laser Games titles, Mad Dog McCree a framework's tables -- a set of light gun titles, a typing edition of one
first among them and the typing edition of Mad Dog II the ninth, and of them, and a prototype in their shape -- is ported as a vocabulary of its own beside
Platoon, a prototype in their shape -- is ported as a vocabulary of its own beside
its description (see its description (see
<<own-vocabulary>>): a behaviour that is the game's loop rewritten in <<own-vocabulary>>): a behaviour that is the game's loop rewritten in
Forge's terms, its screens and each level's flow, over the game's own Forge's terms, its screens and each level's flow, over the game's own
@ -1476,7 +1475,7 @@ examples; scenes 66 and 67 play them.
=== Arcade Plumbing: Branching Video, Lives, Credits, the Board === Arcade Plumbing: Branching Video, Lives, Credits, the Board
A `branches` track is Dragon's Lair: each branch is a window of disc frames, A `branches` track is a branching laserdisc game: each branch is a window of disc frames,
the `move` (a key) or `switch` it wants, and the frames the disc goes to on the `move` (a key) or `switch` it wants, and the frames the disc goes to on
`success` and on `fail`; a type with `{ kind = "branching", track = "moves" }` `success` and on `fail`; a type with `{ kind = "branching", track = "moves" }`
watches it and raises `branchOpen` (with `event.move`), `branchTaken`, and watches it and raises `branchOpen` (with `event.move`), `branchTaken`, and
@ -1592,7 +1591,7 @@ sets `instance.hearsAll` and reads them in `on`.
A word that turns out to be general belongs in the framework rather than A word that turns out to be general belongs in the framework rather than
beside one game. `lightSensor`, which judges a shot by the picture under the beside one game. `lightSensor`, which judges a shot by the picture under the
gun, began as the five ActionMax games' own vocabulary and is now one of the gun, began as five light gun games' own vocabulary and is now one of the
behaviours above, with `pictureAt` beside it as a condition any rule can ask; behaviours above, with `pictureAt` beside it as a condition any rule can ask;
those five name no vocabulary at all any more. The test scene's `bob` is the those five name no vocabulary at all any more. The test scene's `bob` is the
working example instead, and the rule to apply is the same one: a vocabulary working example instead, and the rule to apply is the same one: a vocabulary
@ -1856,7 +1855,7 @@ drive it.
| `K` | a key at the cursor; in a 3D room, a rail point where the camera stands | `K` | a key at the cursor; in a 3D room, a rail point where the camera stands
| `J`, `L`, `Y`, `H`, `I`, `K` | in a 3D room: orbit, tilt, and zoom the editor camera | `J`, `L`, `Y`, `H`, `I`, `K` | in a 3D room: orbit, tilt, and zoom the editor camera
| `U`, `R` | undo and redo, forty steps deep; a drag or a typed form is one step, and a change by hand ends the redo history | `U`, `R` | undo and redo, forty steps deep; a drag or a typed form is one step, and a change by hand ends the redo history
| `S`, `B`, `P`, `X` | save, build, play, release | `S`, `B`, `P`, `X`, `Shift+P` | save, build, play, release, publish
|=== |===
From a script the same work is `forgeEntityAdd`, `forgeEntityDuplicate`, From a script the same work is `forgeEntityAdd`, `forgeEntityDuplicate`,
@ -1886,7 +1885,7 @@ for every named file there first, so the release runs wherever it is put.
`--pack` turns that directory into a `.game` like any other, and it runs on a `--pack` turns that directory into a `.game` like any other, and it runs on a
machine that has never had Forge on it. machine that has never had Forge on it.
`P` goes one step further: it exports, packs, and sends the package to the `Shift+P` goes one step further: it exports, packs, and sends the package to the
master service for review, in one press. It is not in the catalogue until it master service for review, in one press. It is not in the catalogue until it
has been reviewed, and Forge says which version number it was given. Press has been reviewed, and Forge says which version number it was given. Press
again after a change and a later version goes up on its own, without taking an again after a change and a later version goes up on its own, without taking an

View file

@ -1,4 +1,4 @@
// Generated by util/forgeVocabulary.lua from assets/Forge/Author.singe. Do not edit. // Generated by util/forgeVocabulary.lua from forge/Author.singe. Do not edit.
=== Layers === Layers
@ -107,7 +107,7 @@
| `bounce` (number), `buoyancy` (number), `friction` (number), `mass` (number), `shape` (box, sphere) | `bounce` (number), `buoyancy` (number), `friction` (number), `mass` (number), `shape` (box, sphere)
| `branching` | `branching`
| Dragon's Lair: a track of branches, each a disc frame window, the move it wants, and where the disc goes on success and on failure. A branch with mash wants the move that many times inside the window; one with hold wants it held that many frames. Raises branchOpen, branchTaken, and branchMissed. | A branching laserdisc game: a track of branches, each a disc frame window, the move it wants, and where the disc goes on success and on failure. A branch with mash wants the move that many times inside the window; one with hold wants it held that many frames. Raises branchOpen, branchTaken, and branchMissed.
| `track` (track) | `track` (track)
| `camera` | `camera`
@ -191,7 +191,7 @@
| -- | --
| `sdq` | `sdq`
| Plays Super Don Quixote: map-mode levels, scenes, death clips, order, and map from the game's own files, judged as the Karis 3.31c framework judges, with its difficulty select, level clear, finish, and trophies. Written by util/forgePortKaris.lua from the game's script. | Plays one hybrid map-mode game: map-mode levels, scenes, death clips, order, and map from the game's own files, judged as the Karis 3.31c framework judges, with its difficulty select, level clear, finish, and trophies. Written by util/forgePortKaris.lua from the game's script.
| -- | --
| `seek` | `seek`
@ -231,7 +231,7 @@
| `force` (number), `turn` (number) | `force` (number), `turn` (number)
| `timegal` | `timegal`
| Plays RDG2010's Time Gal (Singe Edition): levels of moves from a difficulty array (mirrored by a random draw), checkpoints, a time-stop choice of three, a tally by deaths, a life per quota, and a level order by tiers, at random, or in sequence. The game's own files are loaded and its data and order functions run as they are; the loop is this. Written by util/forgePortKaris.lua from the game's script. | Plays one of RDG2010's remakes: levels of moves from a difficulty array (mirrored by a random draw), checkpoints, a time-stop choice of three, a tally by deaths, a life per quota, and a level order by tiers, at random, or in sequence. The game's own files are loaded and its data and order functions run as they are; the loop is this. Written by util/forgePortKaris.lua from the game's script.
| -- | --
| `timer` | `timer`

View file

@ -24,22 +24,33 @@ own game are made available through a simple application programming interface
Singe provides numerous features to the game developer. Some of the more Singe provides numerous features to the game developer. Some of the more
interesting ones are: interesting ones are:
* 3D scenes with models, animation, lighting, and shadows
* Physics, including joints, ragdolls, soft bodies, and vehicles
* Menus, option screens, and HUDs built like web pages
* Hardware video decoding, even on small machines
* Online high scores, and a catalogue of games to download and update
* Games packed into a single file
* Object-oriented programming language * Object-oriented programming language
* Animated sprites * Animated sprites
* TrueType font support * TrueType font support
* 32-bit color space with transparency * 32-bit color space with transparency
* Multi-channel, overlapping, stereo sound * Multi-channel, overlapping, stereo sound, and MIDI
* Analog and digital input device support * Analog and digital input device support, including light guns
* Wide support for video and audio formats * Wide support for video and audio formats
Singe runs on Windows, macOS, and Linux, including the Raspberry Pi and
handhelds like the R36S.
For players, Singe allows many unemulated and new games to be enjoyed using any For players, Singe allows many unemulated and new games to be enjoyed using any
desired front end or via the included menu system. desired front end or via the included menu system.
Since the original release of Singe back in 2006, several revisions have been Since the original release of Singe back in 2006, several revisions have been
released, both officially and unofficially. In 2020, Singe 2.00 was released. released, both officially and unofficially. In 2020, Singe 2.00 was released.
This was a total rewrite from the ground up, adding numerous features while This was a total rewrite from the ground up, adding numerous features while
staying compatible with existing 1.xx games. As of the writing of this manual, staying compatible with existing 1.xx games. Singe 3.00 adds 3D, physics,
the latest release is {revnumber}. user interfaces, and much more, while still running the games that came
before it. As of the writing of this manual, the latest release is
{revnumber}.
== Installation and Upgrading == Installation and Upgrading
@ -74,7 +85,7 @@ files. Always refresh the `Singe/` directory when upgrading.
Do not touch the `Singe/` folder. Files in this folder belong to Singe and may Do not touch the `Singe/` folder. Files in this folder belong to Singe and may
be updated or deleted at any time. be updated or deleted at any time.
== Usage == Playing Games
=== Installing Games === Installing Games
@ -127,28 +138,29 @@ Singe/ Support files extracted by the engine
Manual.pdf This manual Manual.pdf This manual
COPYING The GNU General Public License, version 3 COPYING The GNU General Public License, version 3
LICENSES Third-party notices and asset credits LICENSES Third-party notices and asset credits
ActionMax/ One game Collection/ A directory of games
games.dat Menu entries for the games in this directory games.dat Menu entries for the games in this directory
38AmbushAlley.singe A script AsteroidPatrol.singe A script
frame_38AmbushAlley.txt Its framefile (or a video with the same base name) frame_AsteroidPatrol.txt Its framefile (or a video with the same base name)
sprite_*.png, sound_*.wav, font_*.ttf sprite_*.png, sound_*.wav, font_*.ttf
DLe.game A game packed into one file (see Single-File Games) MyGame.game A game packed into one file (see Single-File Games)
data/ data/
machine.cfg This machine's audio delay and picture geometry machine.cfg This machine's audio delay and picture geometry
ActionMax/ Indexes, trace.txt, screenshots, saves for that game Collection/ Indexes, trace.txt, screenshots, saves for those games
---- ----
Where Singe looks for a script's files, and where a script may write, is a Where Singe looks for a script's files, and where a script may write, is a
matter for whoever writes the script; the Singe Reference covers both under matter for whoever writes the script; the Singe Reference covers both under
Packaging Your Game. Packaging Your Game.
[#bundledmenu] [#bundledmenu]
=== The Bundled Menu === The Bundled Menu
`Menu.sh` (or `Menu.bat`) starts `Singe/Menu.singe`, the bundled menu: an `Menu.sh` (or `Menu.bat`) starts `Singe/Menu.singe`, the bundled menu: an
RmlUi document, `Singe/Menu.rml` styled by `Singe/menu.rcss`, drawn over the RmlUi document, `Singe/Menu.rml` styled by `Singe/menu.rcss`, drawn over a
engine's own disc (see xref:Reference.adoc#gui[GUI]). It reads the `games.dat` of every game live 3D backdrop (see xref:Reference.adoc#gui[GUI]). The menu runs without a
disc; a machine with no GPU plays `Singe/menuBackground.mkv`, a recording of
the same backdrop, behind a simpler page drawn on the overlay instead. It reads the `games.dat` of every game
directory and `.game` file beside it and lists every game it finds, sorted by directory and `.game` file beside it and lists every game it finds, sorted by
title, down the left of the screen. Beside the list are the selected game's title, down the left of the screen. Beside the list are the selected game's
cabinet art, its marquee and its attract clip, with the year, genre, cabinet art, its marquee and its attract clip, with the year, genre,
@ -160,9 +172,10 @@ a page at a time; both wrap from either end of the list to the other. Start
or any action button launches the selected game, and the key mapped to or any action button launches the selected game, and the key mapped to
`INPUT_SERVICE` (the `9` key by default) opens the service tools (see `INPUT_SERVICE` (the `9` key by default) opens the service tools (see
<<servicetools,The Service Tools>>). With a mouse, a <<servicetools,The Service Tools>>). With a mouse, a
click on a row selects that game and a second click on the same row, or a click on a row selects that game and a second click on the same row launches
click on the Start button in the footer, launches it; the mouse wheel it; the mouse wheel scrolls a description too long for its panel. If you set
scrolls a description too long for its panel. The menu keeps its selection `--quitchord`, holding those buttons quits a game back to the menu, and quits
the menu back to your system (see <<handhelds,Handhelds and Small Machines>>). The menu keeps its selection
in `menu.dat` in its data directory, so it comes back to the same game when in `menu.dat` in its data directory, so it comes back to the same game when
one quits or when Singe is next started. That file is a Lua fragment the menu one quits or when Singe is next started. That file is a Lua fragment the menu
reads at startup and rewrites at shutdown: `GAME_SELECTED` is the game it was reads at startup and rewrites at shutdown: `GAME_SELECTED` is the game it was
@ -332,15 +345,60 @@ Turns the developer's overlay on and off. It stays on into the game, in the
top left corner. See xref:Reference.adoc#statsenable[statsEnable]. top left corner. See xref:Reference.adoc#statsenable[statsEnable].
[[controls]] [[controls]]
=== Customizing the Controls == Controls
By default, Singe maps controllers as if they were Xbox gamepads. If your By default, Singe maps controllers as if they were Xbox gamepads. If your
controller is not recognized or you wish to change the default mappings as they controller is not recognized or you wish to change the default mappings as they
appear to Singe, you can use the free SDL2 Gamepad Tool from appear to Singe, you can use the free SDL2 Gamepad Tool from
http://generalarcade.com/gamepadtool/. http://generalarcade.com/gamepadtool/.
[[controlscfg]]
=== controls.cfg
In addition to configuring your controller, you can remap all the input options
for Singe as a whole or for any individual game by using `controls.cfg` files.
When determining how inputs are mapped, Singe reads configuration settings in
the following order, and a later file replaces an earlier one input by input
(see <<configorder,Where Configuration Files Are Read From>>):
. The defaults from inside Singe.
. `controls.cfg` from the game's main script directory: what the game's author shipped.
. `controls.cfg` from the directory Singe was started in: the machine's own (the `Singe` directory holds a `controls.cfg.example` template you can copy).
. `controls.cfg` from the directory above the game's data directory.
. `controls.cfg` from the game's data directory: yours, for that one game.
You do not have to specify every option in a custom `controls.cfg` file. Feel
free to just provide the entries you want changed.
After installing Singe, you can find a sample input configuration file at
`<InstallDir>/Singe/controls.cfg.example`. To see the available configuration
options available to be used in this file, read through
`<InstallDir>/Singe/Framework.singe`.
`--keymapfile` names one file to read instead of making that search, which is
useful to a front end that wants no doubt about which mappings a game got.
Each `INPUT_` entry is a list, and any one thing in it works the switch. A list
inside that list is a chord: two to four keys or buttons held together, which
works the switch when the last of them goes down and lets go when any of them
comes up. A handheld with no Escape key quits on Select and Start together
this way:
[source,lua]
----
INPUT_QUIT = { SCANCODE.ESCAPE, { GAMEPAD_0.BUTTON_BACK, GAMEPAD_0.BUTTON_START } }
----
Each key or button in a chord keeps its own meaning too, so pressing Select and
Start to quit this way also drops a coin and presses Start on the way out.
If you want a way to quit that always works, use `--quitchord` instead. When
you press one of its buttons, Singe waits a quarter of a second for the rest
before doing anything else, so quitting does not drop a coin or press Start. A
game can turn its own quit key off, but it cannot turn off `--quitchord`.
[[devices]] [[devices]]
==== Every Device Is a Gamepad === Every Device Is a Gamepad
Singe has one family of controller, `GAMEPAD_0` through `GAMEPAD_3`, and every Singe has one family of controller, `GAMEPAD_0` through `GAMEPAD_3`, and every
device it opens is in it. SDL calls a device a gamepad only when it has a device it opens is in it. SDL calls a device a gamepad only when it has a
@ -369,24 +427,34 @@ have it used instead.
not recognise is not opened at all, exactly as in earlier releases. not recognise is not opened at all, exactly as in earlier releases.
[[gamecontrollerdb]] [[gamecontrollerdb]]
==== gamecontrollerdb.txt === gamecontrollerdb.txt
SDL ships with a large database of controller mappings, and the community keeps SDL ships with a large database of controller mappings, and the community keeps
a larger one -- `gamecontrollerdb.txt`, from a larger one -- `gamecontrollerdb.txt`, from
https://github.com/mdqinc/SDL_GameControllerDB. Singe looks for that file in https://github.com/mdqinc/SDL_GameControllerDB. Singe looks for that file in
the same four places it looks for `controls.cfg`: the same four places it looks for `controls.cfg`, in the same order (see
<<configorder,Where Configuration Files Are Read From>>):
. the game's main script directory;
. the directory Singe was started in; . the directory Singe was started in;
. the directory above the game's data directory; . the directory above the game's data directory;
. the game's data directory; . the game's data directory.
. the game's main script directory.
Every file found is loaded, in that order, so one beside a game wins over a Every file found is loaded, in that order, so the machine's file wins over one
machine-wide one, and both win over SDL's own database. Only a device that a game ships, a file in the game's data directory wins over both, and all of
them win over SDL's own database. Only a device that
none of them names gets the mapping Singe writes for it, so the order of none of them names gets the mapping Singe writes for it, so the order of
precedence is: SDL's built-in database, then your `gamecontrollerdb.txt`, then precedence is: SDL's built-in database, then your `gamecontrollerdb.txt`, then
the synthetic mapping. `--program` reports how many mappings each file added. the synthetic mapping. `--program` reports how many mappings each file added.
The files also beat `SDL_GAMECONTROLLERCONFIG`, the environment variable SDL
takes mappings from, which some handheld systems set for their built-in pad.
ArkOS's mapping for the R36S, for instance, leaves Select and Start out, so no
game sees them; a line in `gamecontrollerdb.txt` for the same pad puts them
back. SDL reads the variable once, when Singe starts, so this holds for the
files the first game finds. A game started later from the menu still reads its
own, but a pad the variable names keeps the variable's mapping.
A line of that file looks like this, and is exactly what the trace prints for a A line of that file looks like this, and is exactly what the trace prints for a
device Singe mapped itself: device Singe mapped itself:
@ -394,30 +462,9 @@ device Singe mapped itself:
ff00d87a475300004900000000007603,Arcade Encoder,platform:Linux,a:b0,b:b1,x:b2,y:b3,back:b4,guide:b5,start:b6,leftx:a0,lefty:a1,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8, ff00d87a475300004900000000007603,Arcade Encoder,platform:Linux,a:b0,b:b1,x:b2,y:b3,back:b4,guide:b5,start:b6,leftx:a0,lefty:a1,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
---- ----
In addition to configuring your controller, you can remap all the input options
for Singe as a whole or for any individual game by using `controls.cfg` files.
When determining how inputs are mapped, Singe reads configuration settings in
the following order:
. The defaults from inside Singe.
. `controls.cfg` from the directory Singe was started in (the `Singe` directory holds a `controls.cfg.example` template you can copy).
. `controls.cfg` from the directory above the game's data directory.
. `controls.cfg` from the game's data directory.
. `controls.cfg` from the game's main script directory.
You do not have to specify every option in a custom `controls.cfg` file. Feel
free to just provide the entries you want changed.
After installing Singe, you can find a sample input configuration file at
`<InstallDir>/Singe/controls.cfg.example`. To see the available configuration
options available to be used in this file, read through
`<InstallDir>/Singe/Framework.singe`.
`--keymapfile` names one file to read instead of making that search, which is
useful to a front end that wants no doubt about which mappings a game got.
[[deadzones]] [[deadzones]]
==== Dead Zones and Triggers === Dead Zones and Triggers
`DEAD_ZONE` in `controls.cfg` is how far an axis must move before it counts as `DEAD_ZONE` in `controls.cfg` is how far an axis must move before it counts as
a pressed switch, in the raw units SDL reports: `32767` at full deflection, so a pressed switch, in the raw units SDL reports: `32767` at full deflection, so
@ -455,7 +502,7 @@ directly: a per cent is `32767 / 100` raw units, so Singe's `DEAD_ZONE` of
`controls.cfg`; `DEAD_ZONES` wins over both, being the most specific. `controls.cfg`; `DEAD_ZONES` wins over both, being the most specific.
[[joymouse]] [[joymouse]]
==== Driving the Mouse with a Stick === Driving the Mouse with a Stick
A light gun game on a cabinet with a stick and no mouse is unplayable unless A light gun game on a cabinet with a stick and no mouse is unplayable unless
the stick can move the cursor. `--joymouse` makes it do so: the **left stick of the stick can move the cursor. `--joymouse` makes it do so: the **left stick of
@ -475,7 +522,7 @@ and reads it with `joyMouseIsEnabled`. A reload puts it back where the command
line left it. line left it.
[[mousedevices]] [[mousedevices]]
==== Mice, Guns, and Who Chooses === Mice, Guns, and Who Chooses
`mouseSetMode(MOUSE_MANY)` is a game's decision to tell up to four mice apart, `mouseSetMode(MOUSE_MANY)` is a game's decision to tell up to four mice apart,
which a two gun cabinet needs and which a game that never makes the call cannot which a two gun cabinet needs and which a game that never makes the call cannot
@ -501,7 +548,7 @@ name. `controls.cfg` leaves it bound to nothing; bind a key to it as well if
you want to see what a game does without pulling a cable. you want to see what a game does without pulling a cable.
[[hotkeys]] [[hotkeys]]
==== Hotkeys === Hotkeys
`Alt-Enter` switches between full screen and a window while the game runs, as `Alt-Enter` switches between full screen and a window while the game runs, as
it does in Hypseus. The game never sees the key. A window that started in it does in Hypseus. The game never sees the key. A window that started in
@ -517,6 +564,11 @@ The other switches a user can press -- quit, pause, screenshot, the console and
the mouse capture toggle -- are ordinary `controls.cfg` mappings and are the mouse capture toggle -- are ordinary `controls.cfg` mappings and are
described with the `INPUT_*` names in the sample file. described with the `INPUT_*` names in the sample file.
== Settings and Options
Almost everything you can put on the command line can also go in a settings
file. Set things up once and every game uses them.
[[settings]] [[settings]]
=== The Settings File === The Settings File
@ -525,15 +577,15 @@ Everything on the command line can also be written down once, in a
same dozen options for every game. It is Lua, like `controls.cfg`, and Singe same dozen options for every game. It is Lua, like `controls.cfg`, and Singe
looks for it in exactly the same four places, in this order: looks for it in exactly the same four places, in this order:
. `settings.cfg` from the game's main script directory.
. `settings.cfg` from the directory Singe was started in (the `Singe` directory holds a `settings.cfg.example` template you can copy). . `settings.cfg` from the directory Singe was started in (the `Singe` directory holds a `settings.cfg.example` template you can copy).
. `settings.cfg` from the directory above the game's data directory. . `settings.cfg` from the directory above the game's data directory.
. `settings.cfg` from the game's data directory. . `settings.cfg` from the game's data directory.
. `settings.cfg` from the game's main script directory.
Each place is read once, and a later file wins key by key, so a machine-wide Each place is read once, and a later file wins key by key, so a machine-wide
file can set the display and the volumes while a file beside one game turns off file can set the display and the volumes while a file in one game's data
its crosshair. You do not have to write every key in a file; write the ones you directory turns off that game's crosshair. You do not have to write every key
want changed. in a file; write the ones you want changed.
Every key is the long name of a command line option with the leading dashes Every key is the long name of a command line option with the leading dashes
removed, so there is no second vocabulary to learn: removed, so there is no second vocabulary to learn:
@ -558,7 +610,32 @@ One option, `--deterministic`, takes a value or leaves it out, and its key does
the same: `deterministic = true` uses the default step, `deterministic = 20` the same: `deterministic = true` uses the default step, `deterministic = 20`
names one, and `deterministic = false` leaves the option off. names one, and `deterministic = false` leaves the option off.
==== What Wins [[configorder]]
=== Where Configuration Files Are Read From
`controls.cfg`, `settings.cfg` and `gamecontrollerdb.txt` are all looked for in
the same four places, in the same order, and a file read later wins over one
read earlier:
. *The game's main script directory,* which for a packed game is inside its
`.game` file. This is what the game's author shipped, so it is read first:
it sets the game's defaults and nothing more.
. *The directory Singe was started in.* The machine's own files, such as a
handheld's button layout, go here, and they win over what a game ships. A
file only replaces what it names, so to take a game's binding off a button,
name that input: `INPUT_TEST_MODE = { SCANCODE.F2 }` in the machine's
`controls.cfg` leaves test mode on the keyboard only, whatever button a game
put it on.
. *The directory above the game's data directory,* for files shared by every
game using that data directory.
. *The game's data directory.* Your own files for one game, which win over
everything else.
Without `--datadir` the data directory can be the game's own directory, in
which case the file there is read once, as the game's. `--keymapfile` replaces
the whole search for `controls.cfg` with one named file.
=== What Wins
. The built in default. . The built in default.
. The settings file. . The settings file.
@ -601,8 +678,8 @@ Singe [OPTIONS] gameName
---- ----
The game name is the only required argument. It may be a `.singe` script, a The game name is the only required argument. It may be a `.singe` script, a
directory containing a script of the same name (`ActionMax` finds directory containing a script of the same name (`MyGame` finds
`ActionMax/ActionMax.singe`), or a packed game (`DLe.game` runs the first `MyGame/MyGame.singe`), or a packed game (`MyGame.game` runs the first
entry of the `games.dat` inside it, `--entry=N` another). A script or directory entry of the `games.dat` inside it, `--entry=N` another). A script or directory
launched this way also runs with its `games.dat` entry when one names it (see launched this way also runs with its `games.dat` entry when one names it (see
xref:Reference.adoc#gamesdat[games.dat]), so a game plays the same from the command line as from xref:Reference.adoc#gamesdat[games.dat]), so a game plays the same from the command line as from
@ -610,79 +687,148 @@ the menu. For a laserdisc game (`--disc`) with no
`--framefile`, Singe looks for a video next to the script with the same base `--framefile`, Singe looks for a video next to the script with the same base
name and any extension FFmpeg can demux, then for a `.txt` framefile. name and any extension FFmpeg can demux, then for a `.txt` framefile.
The options are grouped by what they do.
==== The Game and Its Files
Which game runs, where it and its data live, and what it is played on.
[cols="1,2",options="header"] [cols="1,2",options="header"]
|=== |===
| Option | Purpose | Option | Purpose
| `-A`, `--audiodelay=MS` | Compensate for audio that is heard `MS` milliseconds later than the engine can measure (negative when it is heard early), `-1000` to `1000`. See xref:Reference.adoc#audiosync[Audio Sync].
| `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`).
| `-B`, `--bezel=FILENAME` | Draw cabinet artwork around the picture. The file is looked for in the `bezels` folder of the game directory (a packed game may carry its own) and then in the `bezels` folder of the data directory; `--bezeldir` names a different folder. Any format SDL_image reads will do, and transparency is kept. A sidecar beside it says where the picture goes inside it; with none, the picture keeps the whole window and `--shiftx`, `--shifty` and `--scalefactor` place it. Artwork that cannot be found ends the program. See <<bezels,Bezels>>.
| `-b`, `--scalefactor=PERCENT` | Shrink the picture about its centre to `PERCENT` (`25` to `100`) of the window for displays that lose their edges to overscan. Mouse and light gun positions follow the shrunken picture. A script may change it while the game runs with `vldpSetScale`. Combines with `--shiftx`, `--shifty`, `--rotate` and `--sindengun`; see <<videorect,The Video Rectangle>>.
| `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging.
| `-C`, `--canvas=WxH` | World size for a game without a disc, default 720x480. Ignored when there is a disc. | `-C`, `--canvas=WxH` | World size for a game without a disc, default 720x480. Ignored when there is a disc.
| `-D`, `--disc` | Play a laserdisc video: the one named by `--framefile`, or the video found next to the script. Implied by `--framefile`. Without it a video next to the script is reported and ignored.
| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, saves, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it (for a packed game, one named for the database and then for the script's directory inside it). Defaults to `data` in the game directory. Singe 2.x wrote beside the game instead; game directories may be read only, so nothing is written there any more. | `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, saves, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it (for a packed game, one named for the database and then for the script's directory inside it). Defaults to `data` in the game directory. Singe 2.x wrote beside the game instead; game directories may be read only, so nothing is written there any more.
| `-D`, `--disc` | Play a laserdisc video: the one named by `--framefile`, or the video found next to the script. Implied by `--framefile`. Without it a video next to the script is reported and ignored.
| `-E`, `--entry=N` | Run the Nth entry of the `games.dat` inside a `.game` file (default 1). See xref:Reference.adoc#singlefile[Single-File Games]. | `-E`, `--entry=N` | Run the Nth entry of the `games.dat` inside a `.game` file (default 1). See xref:Reference.adoc#singlefile[Single-File Games].
| `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`.
| `-F`, `--bezelflip` | Draw the bezel artwork in front of the picture instead of behind it, which is what Hypseus does with its own bezels; the artwork's transparency is then the hole the game shows through. Without it the artwork is behind everything and the picture covers its cutout. A script chooses whether the overlay and the GUIs stay above the artwork with `setOverlayOnTop`. See <<bezels,Bezels>>.
| `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution.
| `-G`, `--gamedir=PATHNAME` | The directory holding the games, the packed `.game` files and the `Singe` support folder, when the executable lives somewhere else. It becomes the working directory, so the game name, a relative `--datadir` and the packer's names all count from it; absolute paths work as they are. Defaults to the current directory.
| `-g`, `--sindengun='PARAMS'` | Draw the border a Sinden light gun tracks and map mouse and light gun positions into the picture inside it: the black ring, when one is given, runs along the edge of the video, the white ring sits inside it, and the game is drawn in the middle. `PARAMS` is one quoted list of numbers: `WW` (white border width in video pixels); `WW WB` (white width, then a black border outside it); `RW GW BW WW` (a custom border color and width); `RW GW BW WW WB` (the same plus a black width); or `RW GW BW WW RB GB BB WB` (custom colors and widths for both). A border that leaves no room for the game (twice the white and black widths together reaching the smaller side of the video), or a negative width, ends the program with a message. The rings are drawn around whatever `--shiftx`, `--shifty` and `--scalefactor` left, so a border stays a border however the picture is placed; see <<videorect,The Video Rectangle>>.
| `-H`, `--softwarevideo` | Decode video in software even when the platform offers a hardware decoder (VA-API or VDPAU on Linux, the V4L2 decoder on the Pi, D3D11VA on Windows, VideoToolbox on macOS). Use it to rule the hardware path in or out when a video misbehaves; the program trace says which decoder is in use.
| `-h`, `--help` | Show the option summary and exit.
| `-I`, `--bezeldir=PATHNAME` | The folder holding `--bezel`'s artwork, in place of `bezels`. Relative to the game directory, or absolute. Only that folder is searched. See <<bezels,Bezels>>.
| `-k`, `--nologos` | Skip the splash screens.
| `-l`, `--volume_vldp=PERCENT` | Laserdisc volume, `0` to `100`.
| `-m`, `--nomouse` | Disable mouse and lightgun input entirely.
| `-n`, `--nocrosshair` | Ask the game not to draw its own crosshair. Exposed to scripts as `singeWantsCrosshairs()`.
| `-o`, `--audio=TRACK` | Default audio track (zero based) for videos with several, such as multi-language releases.
| `-P`, `--pack=DIRECTORY` | Pack the game in `DIRECTORY` into the database named after the options, then exit. See xref:Reference.adoc#singlefile[Single-File Games].
| `-p`, `--program` | Trace engine activity to the console and to `trace.txt` in the data directory. The file opens with a header describing the machine; see xref:Reference.adoc#traceheader[The Trace Header].
| `-S`, `--sindenedge=WHERE` | Where the Sinden border sits: `video` draws it around the picture, `window` at the edge of the screen. Without the option a bezel decides: artwork around the picture puts the border at the window's edge, because the gun's camera sees the whole screen, and with no artwork it goes around the picture as it always has. At the window's edge the two rings are drawn as rings, last of all, so the artwork between them and the picture stays visible. See <<bezels,Bezels>>.
| `-s`, `--nosound` | Mute all audio.
| `-T`, `--patch=GAME.game` | Replace files in the packed game from the directory or patch database named after the options, then exit.
| `-t`, `--trace` | Trace every Lua API call, with the script line that made it, to the console and to `trace.txt`. The file opens with a header describing the machine; see xref:Reference.adoc#traceheader[The Trace Header].
| `-r`, `--rotate=DEGREES` | Turn the whole presentation clockwise by `0`, `90`, `180` or `270` degrees, for a cabinet whose panel is mounted on its side. The video, the overlay, the GUIs, the 3D scene and the particles all turn together, a quarter turn swaps the shape of the picture so a portrait window is filled, and mouse and light gun positions are turned back so a click still lands where it is drawn. Only the four right angles are accepted. A script may change it with `vldpSetRotate`.
| `-R`, `--reload` | Watch the game's loose script files and run the game again from scratch when one changes; `F5` does the same on demand. A script that fails to compile stays watched, so saving the fix reloads it. For working on a game; see Reloading While You Work.
| `-U`, `--unpack=GAME.game` | Write the packed game's files into the directory named after the options, then exit. A database whose stored names would land outside that directory is refused.
| `-u`, `--stretch` | Stretch the video to fill the window instead of keeping its aspect ratio.
| `-v`, `--framefile=FILENAME` | Video file or framefile to use instead of the one found next to the script. | `-v`, `--framefile=FILENAME` | Video file or framefile to use instead of the one found next to the script.
| `-G`, `--gamedir=PATHNAME` | The directory holding the games, the packed `.game` files and the `Singe` support folder, when the executable lives somewhere else. It becomes the working directory, so the game name, a relative `--datadir` and the packer's names all count from it; absolute paths work as they are. Defaults to the current directory.
| `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging.
|===
==== Display
The window and how the picture fills it.
[cols="1,2",options="header"]
|===
| Option | Purpose
| `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`).
| `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution.
| `-w`, `--fullscreen_window` | Borderless window covering the desktop. | `-w`, `--fullscreen_window` | Borderless window covering the desktop.
| `-X`, `--shiftx=PERCENT` | Move the picture horizontally inside the room `--scalefactor` left it, `-100` (hard against the left) to `100` (hard against the right), `0` centred. As in Hypseus, the shift is a percentage of that room and nothing more, so at `--scalefactor=100` there is no room and the shift does nothing: reduce the scale first. See <<videorect,The Video Rectangle>>.
| `-x`, `--xresolution=VALUE` | Window width. The height is taken from the matching entry of the built in resolution table when omitted.
| `-Y`, `--shifty=PERCENT` | Move the picture vertically inside the room `--scalefactor` left it, `-100` (hard against the top) to `100` (hard against the bottom), `0` centred. It does nothing at full scale, for the same reason. See <<videorect,The Video Rectangle>>.
| `-y`, `--yresolution=VALUE` | Window height.
| `-z`, `--noconsole` | Print nothing (and open no console window on Windows).
| `--absolutes_only` | Keep only the mice that report an absolute position, which is what a real light gun does and an ordinary mouse does not. ManyMouse cannot be asked what a device is, so a device counts as absolute once it has reported an absolute position and not before; see <<mousedevices,Mice, Guns, and Who Chooses>>. Hypseus writes it `-absolutes-only`; the name here uses an underscore because a settings file key has to be a Lua name. Default: off.
| `--altaudio=SUFFIX` | Play `<base><SUFFIX>.ogg` beside the disc's video instead of the audio inside it, for a release whose other languages ship as separate files: `--altaudio=-es` next to `lair.m2v` plays `lair-es.ogg`. Every segment of a framefile is switched together. A file that is not there leaves the game's own audio playing and prints a warning. The `AUDIO_SUFFIX` key in `games.dat` does the same for one game, and a script changes it while running with `discAudioSuffix`. Default: none.
| `--apiversion` | Print one machine readable line describing this build to standard output and exit, for front ends. See <<apiversion,The Version Line>>. Nothing else is printed.
| `--deinterlace=MODE` | What an interlaced picture gets: `auto` (the default) deinterlaces only the frames the file marks interlaced, `on` deinterlaces every frame for a file whose flags are wrong, and `off` never touches the picture. A laserdisc held interlaced fields and a rip that kept them combs on a progressive display; a rip that was deinterlaced when it was made needs nothing here. One picture comes out for each one that goes in, so frame numbers never move. Default: `auto`.
| `--deterministic[=MS]` | For testing only: ignore real time and run the whole engine on a virtual clock that moves `MS` milliseconds every frame, so the same frame number always means the same moment. The disc follows the virtual clock too, its frames lasting the time they are worth, and the random generators are seeded from the same number, so a run repeats to the pixel. The value is optional and is both the step and the seed, `1` to `1000`. Default when given without one: `15`, the frame time the engine's own rate implies. Audio and pacing are meaningless in this mode; see xref:Reference.adoc#deterministic[Deterministic Test Mode]. Default: off.
| `--fvalue=NUMBER` | One number handed from the launcher to the game, which reads it with `getFValue()`. Singe does nothing with it. `0` to `100000`, kept to three decimals as Hypseus keeps it. Default: `0`.
| `--gamepad_reorder=DIGITS` | Which physical pad fills which gamepad slot, as enumeration positions counting from `0`, one for each slot in turn: `--gamepad_reorder=10` makes the second pad found player one and the first player two. Written as bare digits (`3210`) or separated by commas or spaces, as Hypseus writes it; a repeated position is refused. Positions not named fill the slots that are left, in the order SDL found them, and a position with no pad behind it leaves its slot empty. Default: SDL's own order.
| `--haptic=STEP` | The strongest rumble step a game may use, `0` to `4`. `0` turns rumble off altogether, and a lower number quietens a game that asks for more: `controllerDoRumble` never rumbles harder than this. Default: `4`, which is every step a game asks for.
| `--idleexit=SECONDS` | Quit after `SECONDS` with no input of any kind -- no key, no button, no pad axis, no mouse. For an attract cabinet that should hand the screen back. `0` never quits. `0` to `86400`. Default: `0`.
| `--joymouse=BOOLEAN` | Let the left stick of gamepad slot `0` drive the mouse cursor, for a light gun game on a cabinet with no mouse. `--joymouse=false` is Hypseus's `-nojoymouse`, and a script sets it with `joyMouseEnable()`. See <<joymouse,Driving the Mouse With a Stick>>. Default: `false`, where Hypseus's default is on -- the shipped `controls.cfg` already binds that stick to the four directions.
| `--js_range=SPEED` | How fast that stick drives it, `1` to `20`, in video pixels a frame at full deflection. Default: `5`, as in Hypseus.
| `--keymapfile=FILENAME` | Read the control mappings from this file instead of searching the four places for `controls.cfg`, so a front end can be certain which mappings a game got. The built in defaults still apply underneath it, so a partial file still works, and a file that is not there ends the program. Default: the four place search, described in <<controls,Customizing the Controls>>.
| `--linearscale=BOOLEAN` | Smooth the overlay as the window scales it (`true`) or take the nearest pixel (`false`, which keeps pixel art crisp). Takes `true`, `false`, `yes`, `no`, `on`, `off`, `1` or `0`. A script changes it while running with `setOverlayLinearScale`. Default: `true`, which is what every Singe release has drawn; Hypseus's own default is the opposite. | `--linearscale=BOOLEAN` | Smooth the overlay as the window scales it (`true`) or take the nearest pixel (`false`, which keeps pixel art crisp). Takes `true`, `false`, `yes`, `no`, `on`, `off`, `1` or `0`. A script changes it while running with `setOverlayLinearScale`. Default: `true`, which is what every Singe release has drawn; Hypseus's own default is the opposite.
| `--manymouse` | Tell up to four mice apart whatever the game asks for, which a two gun cabinet needs from a game that never calls `mouseSetMode(MOUSE_MANY)` itself. The user's choice outranks the game's: `mouseSetMode(MOUSE_SINGLE)` no longer switches it off. Default: off, and the game decides. | `-k`, `--nologos` | Skip the splash screens.
| `--mapjoysticks=BOOLEAN` | Write a gamepad mapping for a device SDL does not recognise, so an arcade encoder, a spinner, a wheel or a flight stick is opened as `GAMEPAD_0` to `GAMEPAD_3` like anything else. `false` leaves such a device unopened, as earlier releases did. See <<devices,Every Device Is a Gamepad>>. Default: `true`. | `-r`, `--rotate=DEGREES` | Turn the whole presentation clockwise by `0`, `90`, `180` or `270` degrees, for a cabinet whose panel is mounted on its side. The video, the overlay, the GUIs, the 3D scene and the particles all turn together, a quarter turn swaps the shape of the picture so a portrait window is filled, and mouse and light gun positions are turned back so a click still lands where it is drawn. Only the four right angles are accepted. A script may change it with `vldpSetRotate`.
| `--monochrome` | Start with the disc picture in grey. The overlay, the GUIs and the 3D scene keep their colour; a script does the same with `vldpSetMonochrome`, and undoes it. Default: off.
| `--nogamepad` | Ignore every gamepad, the counterpart of `--nomouse`. None is opened and no pad event reaches the game, so a stuck arcade encoder cannot press anything. Default: off.
| `--no_update_check` | Never ask the service whether a newer Singe exists. The menu otherwise looks once a day and shows what it found under the game's details; the Engine Update tool still works by hand. Default: off, so the daily look happens.
| `--refresh` | Set the `Singe` support folder aside as `old-Singe-<version>`, named for the version that wrote it, and write a fresh one. For upgrading: a file an older release shipped and this one does not is left in the folder that was set aside rather than sitting in the live one. Nothing is deleted, and a second run sets its own aside as `old-Singe-<version>-1`. The Engine Update tool passes this to the Singe it restarts into.
| `--screen=N` | Open the window on display `N`, counting from `1` as Hypseus counts them. A number larger than the number of displays lists the displays that are there and exits. Default: the primary display. | `--screen=N` | Open the window on display `N`, counting from `1` as Hypseus counts them. A number larger than the number of displays lists the displays that are there and exits. Default: the primary display.
| `-u`, `--stretch` | Stretch the video to fill the window instead of keeping its aspect ratio.
| `-x`, `--xresolution=VALUE` | Window width. The height is taken from the matching entry of the built in resolution table when omitted. A size larger than the display -- asked for here or by a game's `games.dat` -- is shrunk to fit it, keeping its shape, and the trace says so.
| `-y`, `--yresolution=VALUE` | Window height.
|===
==== Picture Placement, Bezels and Guns
For a cabinet: where the picture sits, the artwork around it, and the border a Sinden gun tracks. See <<cabinet,Setting Up a Cabinet>>.
[cols="1,2",options="header"]
|===
| Option | Purpose
| `-B`, `--bezel=FILENAME` | Draw cabinet artwork around the picture. The file is looked for in the `bezels` folder of the game directory (a packed game may carry its own) and then in the `bezels` folder of the data directory; `--bezeldir` names a different folder. Any format SDL_image reads will do, and transparency is kept. A sidecar beside it says where the picture goes inside it; with none, the picture keeps the whole window and `--shiftx`, `--shifty` and `--scalefactor` place it. Artwork that cannot be found ends the program. See <<bezels,Bezels>>.
| `-I`, `--bezeldir=PATHNAME` | The folder holding `--bezel`'s artwork, in place of `bezels`. Relative to the game directory, or absolute. Only that folder is searched. See <<bezels,Bezels>>.
| `-F`, `--bezelflip` | Draw the bezel artwork in front of the picture instead of behind it, which is what Hypseus does with its own bezels; the artwork's transparency is then the hole the game shows through. Without it the artwork is behind everything and the picture covers its cutout. A script chooses whether the overlay and the GUIs stay above the artwork with `setOverlayOnTop`. See <<bezels,Bezels>>.
| `-b`, `--scalefactor=PERCENT` | Shrink the picture about its centre to `PERCENT` (`25` to `100`) of the window for displays that lose their edges to overscan. Mouse and light gun positions follow the shrunken picture. A script may change it while the game runs with `vldpSetScale`. Combines with `--shiftx`, `--shifty`, `--rotate` and `--sindengun`; see <<videorect,The Video Rectangle>>.
| `-X`, `--shiftx=PERCENT` | Move the picture horizontally inside the room `--scalefactor` left it, `-100` (hard against the left) to `100` (hard against the right), `0` centred. As in Hypseus, the shift is a percentage of that room and nothing more, so at `--scalefactor=100` there is no room and the shift does nothing: reduce the scale first. See <<videorect,The Video Rectangle>>.
| `-Y`, `--shifty=PERCENT` | Move the picture vertically inside the room `--scalefactor` left it, `-100` (hard against the top) to `100` (hard against the bottom), `0` centred. It does nothing at full scale, for the same reason. See <<videorect,The Video Rectangle>>.
| `-S`, `--sindenedge=WHERE` | Where the Sinden border sits: `video` draws it around the picture, `window` at the edge of the screen. Without the option a bezel decides: artwork around the picture puts the border at the window's edge, because the gun's camera sees the whole screen, and with no artwork it goes around the picture as it always has. At the window's edge the two rings are drawn as rings, last of all, so the artwork between them and the picture stays visible. See <<bezels,Bezels>>.
| `-g`, `--sindengun='PARAMS'` | Draw the border a Sinden light gun tracks and map mouse and light gun positions into the picture inside it: the black ring, when one is given, runs along the edge of the video, the white ring sits inside it, and the game is drawn in the middle. `PARAMS` is one quoted list of numbers: `WW` (white border width in video pixels); `WW WB` (white width, then a black border outside it); `RW GW BW WW` (a custom border color and width); `RW GW BW WW WB` (the same plus a black width); or `RW GW BW WW RB GB BB WB` (custom colors and widths for both). A border that leaves no room for the game (twice the white and black widths together reaching the smaller side of the video), or a negative width, ends the program with a message. The rings are drawn around whatever `--shiftx`, `--shifty` and `--scalefactor` left, so a border stays a border however the picture is placed; see <<videorect,The Video Rectangle>>.
|===
==== Sound
[cols="1,2",options="header"]
|===
| Option | Purpose
| `--altaudio=SUFFIX` | Play `<base><SUFFIX>.ogg` beside the disc's video instead of the audio inside it, for a release whose other languages ship as separate files: `--altaudio=-es` next to `game.m2v` plays `game-es.ogg`. Every segment of a framefile is switched together. A file that is not there leaves the game's own audio playing and prints a warning. The `AUDIO_SUFFIX` key in `games.dat` does the same for one game, and a script changes it while running with `discAudioSuffix`. Default: none.
| `-o`, `--audio=TRACK` | Default audio track (zero based) for videos with several, such as multi-language releases.
| `-A`, `--audiodelay=MS` | Compensate for audio that is heard `MS` milliseconds later than the engine can measure (negative when it is heard early), `-1000` to `1000`. See xref:Reference.adoc#audiosync[Audio Sync].
| `-s`, `--nosound` | Mute all audio.
| `--soundfont=FILE` | The SoundFont (`.sf2`) MIDI files are synthesised with. Singe ships none, because a good one is tens of megabytes for a format almost no game uses. Without this option it looks for `Singe/soundfont.sf2` -- so a packed game can carry its own -- and then in the places a distribution installs one. The `SoundFont:` line of the trace header says which was used, or that none was found. See xref:Reference.adoc#midi[MIDI]. Default: the search. | `--soundfont=FILE` | The SoundFont (`.sf2`) MIDI files are synthesised with. Singe ships none, because a good one is tens of megabytes for a format almost no game uses. Without this option it looks for `Singe/soundfont.sf2` -- so a packed game can carry its own -- and then in the places a distribution installs one. The `SoundFont:` line of the trace header says which was used, or that none was found. See xref:Reference.adoc#midi[MIDI]. Default: the search.
| `--startsilent` | Start muted and stay muted until the first input of any kind, then play at the configured volumes. For an attract cabinet in a quiet room. `--nosound` outranks it. Default: off. | `--startsilent` | Start muted and stay muted until the first input of any kind, then play at the configured volumes. For an attract cabinet in a quiet room. `--nosound` outranks it. Default: off.
| `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`.
| `-l`, `--volume_vldp=PERCENT` | Laserdisc volume, `0` to `100`.
|===
==== Input
See <<controls,Controls>>.
[cols="1,2",options="header"]
|===
| Option | Purpose
| `--absolutes_only` | Keep only the mice that report an absolute position, which is what a real light gun does and an ordinary mouse does not. ManyMouse cannot be asked what a device is, so a device counts as absolute once it has reported an absolute position and not before; see <<mousedevices,Mice, Guns, and Who Chooses>>. Hypseus writes it `-absolutes-only`; the name here uses an underscore because a settings file key has to be a Lua name. Default: off.
| `--gamepad_reorder=DIGITS` | Which physical pad fills which gamepad slot, as enumeration positions counting from `0`, one for each slot in turn: `--gamepad_reorder=10` makes the second pad found player one and the first player two. Written as bare digits (`3210`) or separated by commas or spaces, as Hypseus writes it; a repeated position is refused. Positions not named fill the slots that are left, in the order SDL found them, and a position with no pad behind it leaves its slot empty. Default: SDL's own order.
| `--haptic=STEP` | The strongest rumble step a game may use, `0` to `4`. `0` turns rumble off altogether, and a lower number quietens a game that asks for more: `controllerDoRumble` never rumbles harder than this. Default: `4`, which is every step a game asks for.
| `--joymouse=BOOLEAN` | Let the left stick of gamepad slot `0` drive the mouse cursor, for a light gun game on a cabinet with no mouse. `--joymouse=false` is Hypseus's `-nojoymouse`, and a script sets it with `joyMouseEnable()`. See <<joymouse,Driving the Mouse With a Stick>>. Default: `false`, where Hypseus's default is on -- the shipped `controls.cfg` already binds that stick to the four directions.
| `--js_range=SPEED` | How fast that stick drives it, `1` to `20`, in video pixels a frame at full deflection. Default: `5`, as in Hypseus.
| `--keymapfile=FILENAME` | Read the control mappings from this file instead of searching the four places for `controls.cfg`, so a front end can be certain which mappings a game got. The built in defaults still apply underneath it, so a partial file still works, and a file that is not there ends the program. Default: the four place search, described in <<controls,Controls>>.
| `--manymouse` | Tell up to four mice apart whatever the game asks for, which a two gun cabinet needs from a game that never calls `mouseSetMode(MOUSE_MANY)` itself. The user's choice outranks the game's: `mouseSetMode(MOUSE_SINGLE)` no longer switches it off. Default: off, and the game decides.
| `--mapjoysticks=BOOLEAN` | Write a gamepad mapping for a device SDL does not recognise, so an arcade encoder, a spinner, a wheel or a flight stick is opened as `GAMEPAD_0` to `GAMEPAD_3` like anything else. `false` leaves such a device unopened, as earlier releases did. See <<devices,Every Device Is a Gamepad>>. Default: `true`.
| `-n`, `--nocrosshair` | Ask the game not to draw its own crosshair. Exposed to scripts as `singeWantsCrosshairs()`.
| `--nogamepad` | Ignore every gamepad, the counterpart of `--nomouse`. None is opened and no pad event reaches the game, so a stuck arcade encoder cannot press anything. Default: off.
| `-m`, `--nomouse` | Disable mouse and lightgun input entirely.
| `--quitchord=BUTTONS` | Buttons on the first gamepad that quit when held together, whatever the game maps and even when it has turned its quit key off: the way out on a handheld with no keyboard. A game quits back to the menu it was started from, and the menu quits Singe. Two to four of SDL's gamepad button names joined by `+`, such as `back+start` (Select and Start); the names are `a`, `b`, `x`, `y`, `back`, `guide`, `start`, `leftstick`, `rightstick`, `leftshoulder`, `rightshoulder`, `dpup`, `dpdown`, `dpleft` and `dpright`, as the pad's mapping gives them, and `--program` traces the name of every button pressed. A press of one of the chord's buttons waits a quarter of a second for the rest before it does what it normally does, so Start does not launch a game from the menu on the way to quitting it. Belongs in the machine's `settings.cfg`. Default: none.
| `--trigger_threshold=PERCENT` | How far an analogue trigger must travel before it counts as a button, as a per cent of full travel -- Hypseus's unit, whose own default is `99.5`. `0` leaves the triggers on `DEAD_ZONE`, which is what Singe has always given them. Wins over `TRIGGER_THRESHOLD` in `controls.cfg`; `DEAD_ZONES` wins over both. `0` to `100`; see <<deadzones,Dead Zones and Triggers>>. Default: `0`. | `--trigger_threshold=PERCENT` | How far an analogue trigger must travel before it counts as a button, as a per cent of full travel -- Hypseus's unit, whose own default is `99.5`. `0` leaves the triggers on `DEAD_ZONE`, which is what Singe has always given them. Wins over `TRIGGER_THRESHOLD` in `controls.cfg`; `DEAD_ZONES` wins over both. `0` to `100`; see <<deadzones,Dead Zones and Triggers>>. Default: `0`.
| `--xratio=FACTOR` | The horizontal scale a light gun game reads with `ratioGetX()` to stretch its own gun coordinates for a display whose shape does not match the video. Singe does nothing with it; the game does the arithmetic, exactly as in Hypseus. `0` to `100`, kept to two decimals as Hypseus keeps it. Default: `0`, which every game that reads it treats as `1`. | `--xratio=FACTOR` | The horizontal scale a light gun game reads with `ratioGetX()` to stretch its own gun coordinates for a display whose shape does not match the video. Singe does nothing with it; the game does the arithmetic, exactly as in Hypseus. `0` to `100`, kept to two decimals as Hypseus keeps it. Default: `0`, which every game that reads it treats as `1`.
| `--yratio=FACTOR` | The same for `ratioGetY()`. Default: `0`. | `--yratio=FACTOR` | The same for `ratioGetY()`. Default: `0`.
|=== |===
==== Video Decoding and Performance
Mostly for small machines; see <<handhelds,Handhelds and Small Machines>>.
[cols="1,2",options="header"]
|===
| Option | Purpose
| `--deinterlace=MODE` | What an interlaced picture gets: `auto` (the default) deinterlaces only the frames the file marks interlaced, `on` deinterlaces every frame for a file whose flags are wrong, and `off` never touches the picture. A laserdisc held interlaced fields and a rip that kept them combs on a progressive display; a rip that was deinterlaced when it was made needs nothing here. One picture comes out for each one that goes in, so frame numbers never move. Default: `auto`.
| `--monochrome` | Start with the disc picture in grey. The overlay, the GUIs and the 3D scene keep their colour; a script does the same with `vldpSetMonochrome`, and undoes it. Default: off.
| `--scenequality=LEVEL` | How much the 3D scene draws: `high`, everything a game asks for, or `low` for a small GPU -- a handheld or an early Raspberry Pi -- which draws the scene at half resolution each way and leaves off antialiasing and bloom whatever the game asks. The 3D picture is softer and bright things have no glow, and it runs much faster where the GPU was the limit; the disc, the overlay and the GUIs are untouched. Belongs in the machine's `settings.cfg` rather than on a game's command line, since it describes the machine. Default: `high`.
| `-H`, `--softwarevideo` | Decode video in software even when the platform offers a hardware decoder (VA-API or VDPAU on Linux, the V4L2 decoder on the Pi, D3D11VA on Windows, VideoToolbox on macOS). Use it to rule the hardware path in or out when a video misbehaves; the program trace says which decoder is in use.
| `--zerocopy=BOOLEAN` | Hand hardware decoded video frames straight to the GPU instead of copying them through the CPU: Rockchip's MPP decoder and VA-API on Linux, drawn with OpenGL ES. It saves most of the cost of full screen video on a small machine. Singe falls back to copying by itself when the driver will not take a frame, or when a game reads or changes the picture's pixels, and the program trace says which it is doing; `false` makes it copy from the start, to rule this path in or out. Default: `true`.
|===
==== Cabinets and Front Ends
[cols="1,2",options="header"]
|===
| Option | Purpose
| `--apiversion` | Print one machine readable line describing this build to standard output and exit, for front ends. See <<apiversion,The Version Line>>. Nothing else is printed.
| `--fvalue=NUMBER` | One number handed from the launcher to the game, which reads it with `getFValue()`. Singe does nothing with it. `0` to `100000`, kept to three decimals as Hypseus keeps it. Default: `0`.
| `-h`, `--help` | Show the option summary and exit.
| `--idleexit=SECONDS` | Quit after `SECONDS` with no input of any kind -- no key, no button, no pad axis, no mouse. For an attract cabinet that should hand the screen back. `0` never quits. `0` to `86400`. Default: `0`.
| `--no_update_check` | Never ask the service whether a newer Singe exists. The menu otherwise looks once a day and shows what it found under the game's details; the Engine Update tool still works by hand. Default: off, so the daily look happens.
| `-z`, `--noconsole` | Print nothing (and open no console window on Windows).
| `--refresh` | Set the `Singe` support folder aside as `old-Singe-<version>`, named for the version that wrote it, and write a fresh one. For upgrading: a file an older release shipped and this one does not is left in the folder that was set aside rather than sitting in the live one. Nothing is deleted, and a second run sets its own aside as `old-Singe-<version>-1`. The Engine Update tool passes this to the Singe it restarts into.
|===
==== Making and Testing Games
See <<troubleshooting,When Something Goes Wrong>> for the traces.
[cols="1,2",options="header"]
|===
| Option | Purpose
| `--deterministic[=MS]` | For testing only: ignore real time and run the whole engine on a virtual clock that moves `MS` milliseconds every frame, so the same frame number always means the same moment. The disc follows the virtual clock too, its frames lasting the time they are worth, and the random generators are seeded from the same number, so a run repeats to the pixel. The value is optional and is both the step and the seed, `1` to `1000`. Default when given without one: `15`, the frame time the engine's own rate implies. Audio and pacing are meaningless in this mode; see xref:Reference.adoc#deterministic[Deterministic Test Mode]. Default: off.
| `-P`, `--pack=DIRECTORY` | Pack the game in `DIRECTORY` into the database named after the options, then exit. See xref:Reference.adoc#singlefile[Single-File Games].
| `-T`, `--patch=GAME.game` | Replace files in the packed game from the directory or patch database named after the options, then exit.
| `--perfsync` | With `--program`, wait for the GPU to finish each stage of a frame before timing the next, so the perf lines say which stage the GPU's time belongs to rather than leaving it all in `present`. Everything runs slower while it is on, so it is for measuring, not for playing; see <<perflines,Reading the Perf Lines>>. Default: off.
| `-p`, `--program` | Trace engine activity to the console and to `trace.txt` in the data directory. The file opens with a header describing the machine; see xref:Reference.adoc#traceheader[The Trace Header]. Every five seconds it also says where the frame's time went; see <<perflines,Reading the Perf Lines>>.
| `-R`, `--reload` | Watch the game's loose script files and run the game again from scratch when one changes; `F5` does the same on demand. A script that fails to compile stays watched, so saving the fix reloads it. For working on a game; see Reloading While You Work.
| `-t`, `--trace` | Trace every Lua API call, with the script line that made it, to the console and to `trace.txt`. The file opens with a header describing the machine; see xref:Reference.adoc#traceheader[The Trace Header].
| `-U`, `--unpack=GAME.game` | Write the packed game's files into the directory named after the options, then exit. A database whose stored names would land outside that directory is refused.
|===
Options may be given as `-x 640`, `-x640`, or `--xresolution=640`. Numeric Options may be given as `-x 640`, `-x640`, or `--xresolution=640`. Numeric
values are validated; a bad value prints the usage text and exits. The options values are validated; a bad value prints the usage text and exits. Newer options
at the end of the table have no short letter; the alphabet ran out, and a letter have no short letter; the alphabet ran out, and a letter picked at random would
picked at random would help nobody. help nobody.
Anything here except the options that name the game, name a directory, or must Anything here except the options that name the game, name a directory, or must
act before a file can be found may also be set in a settings file, so a cabinet act before a file can be found may also be set in a settings file, so a cabinet
@ -690,7 +836,7 @@ does not need a launcher script for every game. See <<settings,The Settings
File>>. File>>.
[[apiversion]] [[apiversion]]
==== The Version Line === The Version Line
`--apiversion` prints one line and exits with status `0`: `--apiversion` prints one line and exits with status `0`:
@ -711,6 +857,16 @@ This is a contract for front ends and will not change shape:
* Later releases may add fields. A reader must ignore keys it does not know * Later releases may add fields. A reader must ignore keys it does not know
rather than treating them as an error. rather than treating them as an error.
[[cabinet]]
== Setting Up a Cabinet
Arcade cabinets need a few things a desktop does not: moving the picture away
from edges the monitor hides, turning it for a sideways screen, artwork around
it, a border for a Sinden light gun, and a scoreboard. You only need this
chapter if you are building a cabinet. Set these once in the cabinet's
`settings.cfg`, or use the Display tool in the menu (see
<<servicetools,The Service Tools>>) to adjust the picture while you look at it.
[[videorect]] [[videorect]]
=== The Video Rectangle === The Video Rectangle
@ -872,6 +1028,192 @@ xref:Reference.adoc#guidrawscreen[`guiDrawScreen`].
Hypseus's `scoreBezelEnable` takes a second argument choosing among its five Hypseus's `scoreBezelEnable` takes a second argument choosing among its five
scoreboard backends. Singe has one, so the argument is accepted and ignored. scoreboard backends. Singe has one, so the argument is accepted and ignored.
[[handhelds]]
== Handhelds and Small Machines
Singe runs on the Raspberry Pi and on inexpensive ARM handhelds like the R36S.
The same games work there as on a desktop, but a small machine needs a little
help to run them well.
=== What a Small Machine Needs
Sprites, text, sound, and video work everywhere. The 3D scene and the menu's
fancy look need a graphics chip with Vulkan or OpenGL ES 3.1.
* A Raspberry Pi 4 or later has both, so everything works. A Pi 3 only has
OpenGL ES 2.0, which is not enough for 3D. It still runs 2D games.
* Most handhelds, the R36S included, have a Mali graphics chip with OpenGL ES
3.1, so 3D works there too.
* On a machine without either, the menu uses a simpler look that does not need
them.
The first time you run a 3D game, Singe builds its shaders, which can take a
few seconds on a small machine. They are saved in the `Singe` folder inside the
data directory, so this only happens once.
=== Settings for a Small Machine
Put these in the `settings.cfg` beside the Singe binary so every game uses
them:
`scenequality = "low"`::
Draws the 3D scene at half size, without antialiasing or bloom. 3D looks a
little softer, but runs much faster. Video, sprites, and menus are not
affected.
`zerocopy = true`::
This is already the default. Video goes from the hardware decoder straight to
the graphics chip without the CPU copying it. This is what lets a handheld play
laserdisc video at full speed. Only turn it off if a video misbehaves and you
want to rule it out.
`quitchord = "back+start"`::
Handhelds do not have an Escape key. Holding Select and Start quits the game
back to the menu, and quits the menu back to your system, no matter how the
game is set up.
=== The R36S and ArkOS
The R36S and similar handhelds running ArkOS use the Linux `aarch64` build of
Singe. Put the Singe binary, its `Singe` folder, `Menu.sh`, and your games in
one folder, such as `/roms/singe`. Run `Menu.sh` once so Singe can create its
support files.
To start Singe from the Ports menu, add a script like the one below to
`/roms/ports`. These handhelds are slow to speed up their processor and
graphics chip, so the script sets them to full speed while Singe runs and puts
them back when it exits.
[source,bash]
----
#!/bin/bash
# /roms/ports/Singe.sh: Singe in the Ports menu.
SINGE_DIR=/roms/singe
[ "$(id -u)" -eq 0 ] && ESUDO="" || ESUDO="sudo"
GOVERNORS=()
for g in /sys/devices/system/cpu/cpufreq/policy*/scaling_governor /sys/class/devfreq/*/governor; do
[ -f "$g" ] || continue
GOVERNORS+=("$g:$(cat "$g")")
echo performance | $ESUDO tee "$g" > /dev/null 2>&1
done
restore() {
for entry in "${GOVERNORS[@]}"; do
echo "${entry##*:}" | $ESUDO tee "${entry%:*}" > /dev/null 2>&1
done
}
trap restore EXIT
cd "$SINGE_DIR" || exit 1
./Menu.sh "$@"
----
ArkOS does not tell games about the Select and Start buttons, so the quit chord
will not work until you add this `gamecontrollerdb.txt` beside the Singe binary
(see <<gamecontrollerdb,gamecontrollerdb.txt>>):
----
1900bb3e4b4800000011000000010000,GO-Super Gamepad,a:b0,b:b1,x:b2,y:b3,back:b12,start:b13,leftshoulder:b4,rightshoulder:b5,dpup:b8,dpdown:b9,dpleft:b10,dpright:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:b6,righttrigger:b7,platform:Linux,
----
If a button does not do what you expect, run Singe once with `--program` and
press it. The trace shows the name of each button you press, or its number if
the mapping above does not include it.
Finally, add a `controls.cfg` beside the Singe binary. This one puts coin and
start on Select and Start, and keeps a game's test and service menus off the
face buttons, where some games put them.
[source,lua]
----
INPUT_1P_COIN = { SCANCODE.MAIN_5, SCANCODE.C, GAMEPAD_0.BUTTON_BACK }
INPUT_1P_START = { SCANCODE.MAIN_1, GAMEPAD_0.BUTTON_START }
INPUT_PAUSE = { SCANCODE.P }
INPUT_SCREENSHOT = { SCANCODE.F12, SCANCODE.F11 }
INPUT_SERVICE = { SCANCODE.MAIN_9 }
INPUT_TEST_MODE = { SCANCODE.F2 }
----
This file wins over the one a game includes (see
<<configorder,Where Configuration Files Are Read From>>). Pause and screenshot
are moved to the keyboard because Singe normally puts them on Start and
Select.
[[troubleshooting]]
== When Something Goes Wrong
=== The Trace
Run Singe with `--program` (or `-p`) to see what it is doing. Everything is
printed to the console and saved to `trace.txt` in the game's data directory.
If you report a problem, include `trace.txt` from a run with `--program`.
`--trace` (or `-t`) also logs every Lua call a game makes, which is mostly
useful when writing a game.
If Singe crashes on Linux or macOS, it writes where it crashed to the console
and to `trace.txt`.
`trace.txt` only has Singe's own messages. Messages from the sound system, the
video decoder, and the graphics driver only go to the console. When those might
be the problem, save the console too. On Linux:
----
./Menu.sh --program &> singe.log
----
[[perflines]]
=== Reading the Perf Lines
With `--program`, every five seconds the trace says where the time went, as
averages per trip round the frame loop:
----
Perf: 58.9 frames/s shown, 353 loops; per loop 13.1 ms (worst 82.3): disc 1.1, script 0.1, scene 0.0, gui 0.4, overlay 0.0, compose 0.0, present 11.4, other 0.1
Perf script: per loop 0.1 ms of Lua and 0.0 in engine calls it made; 0.0 in engine calls altogether: overlayClear 0.01 (0.8 calls), discGetFrame 0.00 (0.8 calls)
Video 3 perf: 23.8 frames/s decoded; per frame 0.5 ms, of which read back 0.0, convert 0.0
----
* `disc` -- advancing the laserdisc video and putting its frame on the GPU.
* `script` -- the game's `onOverlayUpdate`, which the `Perf script` line splits
into the Lua itself and the engine calls it made, costliest calls first.
* `scene` -- drawing the 3D scene.
* `gui` -- laying out and drawing the GUI documents.
* `overlay` -- sending what changed on the overlay to the GPU.
* `compose` -- only with `--perfsync`: the GPU drawing the picture, the
overlay and the GUIs together.
* `present` -- showing the frame, which includes waiting for the screen's next
refresh.
* `other` -- everything else: input, sound, physics and the rest.
A video's own line says how fast it decodes and how much of each frame went on
copying it back from the hardware decoder and converting its colours; zero for
both means its frames go to the GPU without being copied.
The GPU works behind the CPU, so without `--perfsync` its time lands wherever
the CPU next has to wait for it, which is usually `present`. A large `present`
at the display's full rate is only the wait for the refresh; a large `present`
below it means the GPU is busy. `--perfsync` makes each stage wait for the GPU
before the next starts, so `scene`, `gui`, `overlay` and `compose` include
their GPU time and `present` is left with the wait for the refresh. The frame
rate drops while it is on, because the CPU and the GPU no longer work at the
same time; read the stages against each other, not against the frame rate.
=== A Button Does Nothing
Open the Input Test tool in the menu (see <<servicetools,The Service Tools>>)
and press the button. If the tool sees it, the game is ignoring it, so check the
game's `controls.cfg`. If the tool does not see it, check your own
`controls.cfg`, or your controller's mapping in `gamecontrollerdb.txt`.
Running with `--program` shows every button as you press it.
=== Sound and Picture Are Out of Sync
Some televisions, receivers, and wireless speakers delay the sound. Use the
Audio Delay tool in the menu to measure the delay. Singe remembers it for every
game. See xref:Reference.adoc#audiosync[Audio Sync] for details.
For choppy or stuttering sound, see the Frequently Asked Questions.
== Frequently Asked Questions == Frequently Asked Questions
*Why is it named Singe?* *Why is it named Singe?*
@ -880,17 +1222,18 @@ Singe is the name of the dragon in Dragon's Lair. As Singe (the program) began
as an add-on to Daphne (the princess in Dragon's Lair) the Dragon's Lair theme as an add-on to Daphne (the princess in Dragon's Lair) the Dragon's Lair theme
was kept. was kept.
*What is the difference between Daphne, Singe, Hypseus, and Singe 2.xx?* *What is the difference between Daphne, Singe, Hypseus, and Singe 3.xx?*
Daphne is an actual laserdisc game emulator that can run a handful of classic Daphne is an actual laserdisc game emulator that can run a handful of classic
laser games. Daphne/Singe (Singe 1.xx) is the original version of Singe that laser games. Daphne/Singe (Singe 1.xx) is the original version of Singe that
was an add-on for the Daphne emulator -- originally shipped as a game DLL for was an add-on for the Daphne emulator -- originally shipped as a game DLL for
Daphne, then later combined into a single binary with the Daphne emulation Daphne, then later combined into a single binary with the Daphne emulation
features removed. Hypseus-Singe is an enhanced fork of Daphne with Singe 1.xx features removed. Hypseus-Singe is an enhanced fork of Daphne with Singe 1.xx
and some Singe 2.xx support. Finally, Singe 2.xx is an all-new, built-from- and some Singe 2.xx support. Singe 2.xx is an all-new, built-from-scratch
scratch upgrade of the original Singe. In short, if you want accuracy, use upgrade of the original Singe, and Singe 3.xx builds on it with 3D, physics,
Daphne or Hypseus if they support emulating the desired game. If it is a Singe and much more. In short, if you want accuracy, use Daphne or Hypseus if they
game, use Singe 2.xx, or try Hypseus. support emulating the desired game. If it is a Singe game, use Singe, or try
Hypseus.
*Why should we not call it an "emulator"?* *Why should we not call it an "emulator"?*
@ -900,10 +1243,18 @@ perspective, it is business as usual. Singe, on the other hand, requires that
every game be re-implemented (ported) to run on Singe. The end result may be every game be re-implemented (ported) to run on Singe. The end result may be
very similar, but how it works is entirely different. very similar, but how it works is entirely different.
*Can I run Singe 1.xx games on Singe 2.xx?* *Can I run Singe 1.xx and 2.xx games on Singe 3.xx?*
Yes, probably. But you likely will not want to. Almost everything from Singe Yes, probably. Almost everything from Singe 1.xx has been converted and
1.xx has been converted and enhanced for Singe 2.xx. enhanced for newer versions of Singe, and most Singe 2.xx games run as they
are. Games that use the older sprite functions may need a small change; see
xref:Reference.adoc#migrating[Migrating from Singe 2.10].
*Do I need a laserdisc game to use Singe?*
No. Games do not need a disc at all. They can use sprites, video, 3D, and
anything else Singe offers. See xref:Reference.adoc#withoutadisc[Games Without
a Disc].
*Why does the game I just installed not show in the menu?* *Why does the game I just installed not show in the menu?*
@ -911,9 +1262,11 @@ Whoever packaged the game failed to include a proper `games.dat` file.
*Why is HD video slow?* *Why is HD video slow?*
Due to the way Singe accesses video files to provide frame seeking, it is It should not be any more. Singe now uses your video card to decode video when
unable to offload video decoding to the video card. High definition video your system supports it, even on the Raspberry Pi and many handhelds. If HD
requires a lot of CPU. video is still slow, your system may not have hardware video decoding, and
then high definition video requires a lot of CPU. Running with `--program`
shows which decoder Singe is using.
*Why does my audio stutter on Windows?* *Why does my audio stutter on Windows?*
@ -935,7 +1288,7 @@ do not allow you to use stereo, you can use Virtual Audio Cable to fix this:
. You should have sound again. Load up Singe and your audio will be working . You should have sound again. Load up Singe and your audio will be working
and lag-free. and lag-free.
*Does 3D work on a Raspberry Pi?* *Does 3D work on a Raspberry Pi or a handheld?*
On a Raspberry Pi 4 or later, yes. They have both a Vulkan driver and OpenGL On a Raspberry Pi 4 or later, yes. They have both a Vulkan driver and OpenGL
ES 3.1, and Singe uses whichever it finds. The Pi 3 has only OpenGL ES 2.0, ES 3.1, and Singe uses whichever it finds. The Pi 3 has only OpenGL ES 2.0,
@ -943,7 +1296,7 @@ which cannot express the scene's skinning or morph targets, so it runs 2D games
(2D physics included) exactly as before and refuses the first 3D call. The Pi 4 (2D physics included) exactly as before and refuses the first 3D call. The Pi 4
is the minimum for any game that uses the 3D Scenes chapter. The same applies to is the minimum for any game that uses the 3D Scenes chapter. The same applies to
the cheap handhelds: a Mali-G31 or G610 has the ES 3.1 the scene needs, whether the cheap handhelds: a Mali-G31 or G610 has the ES 3.1 the scene needs, whether
or not it has Vulkan. or not it has Vulkan. See <<handhelds,Handhelds and Small Machines>>.
*Why does my audio stutter on the Raspberry Pi?* *Why does my audio stutter on the Raspberry Pi?*

View file

@ -187,11 +187,11 @@ installing the game is copying the file next to the executable. The menu
lists every `.game` file it finds beside the game directories. lists every `.game` file it finds beside the game directories.
---- ----
Singe --pack DLe DLe.game Pack the DLe directory into DLe.game Singe --pack MyGame MyGame.game Pack the MyGame directory into MyGame.game
Singe DLe.game Run the first games.dat entry in it Singe MyGame.game Run the first games.dat entry in it
Singe --entry=2 DLe.game Run its second entry Singe --entry=2 MyGame.game Run its second entry
Singe --unpack DLe.game DLe Write the files back out Singe --unpack MyGame.game MyGame Write the files back out
Singe --patch DLe.game fixes Replace files from a directory (or a patch database) Singe --patch MyGame.game fixes Replace files from a directory (or a patch database)
---- ----
Nothing in a game has to change to be packed. Every name a script uses, Nothing in a game has to change to be packed. Every name a script uses,
@ -203,7 +203,7 @@ exactly as before. For a packed game a name is tried in three places, in
this order, and the first hit wins: this order, and the first hit wins:
. A loose directory named like the database without its extension . A loose directory named like the database without its extension
(`DLe/` beside `DLe.game`). This is how you keep editing a packed game, (`MyGame/` beside `MyGame.game`). This is how you keep editing a packed game,
and how a player mods one: files dropped there beat the packed copies. and how a player mods one: files dropped there beat the packed copies.
. The game's data directory, under `files/`. Everything a game writes . The game's data directory, under `files/`. Everything a game writes
with `io.output` or `io.open(name, "w")` lands here, so a saved with `io.output` or `io.open(name, "w")` lands here, so a saved
@ -212,9 +212,9 @@ this order, and the first hit wins:
Inside a database, names are matched without regard to case, with either Inside a database, names are matched without regard to case, with either
path separator, and relative to the game root. A leading component equal to path separator, and relative to the game root. A leading component equal to
the game's own directory name is ignored, so `DLe/Cfg/game.cfg`, the game's own directory name is ignored, so `MyGame/Cfg/game.cfg`,
`Cfg/game.cfg`, and `DIR .. "Cfg/game.cfg"` all find the same file. The `Cfg/game.cfg`, and `DIR .. "Cfg/game.cfg"` all find the same file. The
name of the database itself (`DLe.game`, with or without a trailing name of the database itself (`MyGame.game`, with or without a trailing
separator) is the game root, as `.` is from inside. Names separator) is the game root, as `.` is from inside. Names
starting with `Singe/` or with the data directory stay on the filesystem; starting with `Singe/` or with the data directory stay on the filesystem;
`Singe/Framework.singe` and `singeGetDataPath()` work as always, and such a `Singe/Framework.singe` and `singeGetDataPath()` work as always, and such a
@ -268,7 +268,7 @@ patched game, so a patch cannot leave a dangling path behind either.
`--patch` takes either a directory laid out like the game, whose files `--patch` takes either a directory laid out like the game, whose files
replace the packed ones, or a `.patch` file made by `--pack` from such a replace the packed ones, or a `.patch` file made by `--pack` from such a
directory (`Singe --pack fixes DLe-1.1.patch`; a patch needs no directory (`Singe --pack fixes MyGame-1.1.patch`; a patch needs no
`games.dat`). Files the patch does not name are left alone. Either way the `games.dat`). Files the patch does not name are left alone. Either way the
change is one transaction: the game is intact if it is interrupted. A patch change is one transaction: the game is intact if it is interrupted. A patch
may also carry a `removed(path)` table naming files to delete. A `.patch` may also carry a `removed(path)` table naming files to delete. A `.patch`
@ -292,15 +292,15 @@ gain. Nothing else in Singe needs this; a game is only ever written by the
packer. packer.
A packed game's data directory is named for the database and then for the A packed game's data directory is named for the database and then for the
script's directory inside it: `DLe/` for a `DLe.game` whose script sits at script's directory inside it: `MyGame/` for a `MyGame.game` whose script sits at
its root under the data directory (`data/DLe/` by default), which is where the loose its root under the data directory (`data/MyGame/` by default), which is where the loose
`DLe/DLe.singe` has it, so saves and settings are shared between a loose `MyGame/MyGame.singe` has it, so saves and settings are shared between a loose
install and a packed one; a script packed in a subdirectory install and a packed one; a script packed in a subdirectory
(`Game/Main.singe`) gets `DLe/Game/`. The video index goes there too. (`Game/Main.singe`) gets `MyGame/Game/`. The video index goes there too.
NOTE: Earlier 3.00 builds gave a packed game whose script sat in a NOTE: Earlier 3.00 builds gave a packed game whose script sat in a
subdirectory the data directory `Game/` under the base, which every archive subdirectory the data directory `Game/` under the base, which every archive
with that layout shared. Such a game's saves now belong under `DLe/Game/`; with that layout shared. Such a game's saves now belong under `MyGame/Game/`;
move them by hand. move them by hand.
When Singe runs a game from a database the menu passes `CONTAINER` in the When Singe runs a game from a database the menu passes `CONTAINER` in the
`games.dat` entry it launches; the field is set by the menu, never by hand. `games.dat` entry it launches; the field is set by the menu, never by hand.
@ -556,26 +556,26 @@ of your game. An example containing multiple games:
---- ----
GAMES = { GAMES = {
{ {
TITLE = ".38 Ambush Alley", TITLE = "Asteroid Patrol",
SCRIPT = "ActionMax/38AmbushAlley.singe", SCRIPT = "Collection/AsteroidPatrol.singe",
VIDEO = "ActionMax/frame_38AmbushAlley.txt", VIDEO = "Collection/frame_AsteroidPatrol.txt",
STRETCH = false, STRETCH = false,
NO_MOUSE = false, NO_MOUSE = false,
RESOLUTION_X = 720, RESOLUTION_X = 720,
RESOLUTION_Y = 480, RESOLUTION_Y = 480,
SINDEN_GUN = "", SINDEN_GUN = "",
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10", GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10",
CABINET = "ActionMax/cabinet_38AmbushAlley.png", CABINET = "Collection/cabinet_AsteroidPatrol.png",
MARQUEE = "ActionMax/marquee_ActionMax.png", MARQUEE = "Collection/marquee_Collection.png",
ATTRACT = "ActionMax/video_38AmbushAlley.mkv", ATTRACT = "Collection/video_AsteroidPatrol.mkv",
ATTRACT_START = 3000, ATTRACT_START = 3000,
ATTRACT_END = 3500, ATTRACT_END = 3500,
YEAR = 1987, YEAR = 2026,
PLATFORM = "ActionMax", PLATFORM = "Laserdisc",
DEVELOPER = "Sourcing International, Ltd.", DEVELOPER = "Your Studio",
PUBLISHER = "Worlds of Wonder, Inc.", PUBLISHER = "Your Studio",
GENRE = "Shooter", GENRE = "Shooter",
DESCRIPTION = "Get your target practice in with real police officers then hit the streets.", DESCRIPTION = "Keep the asteroids away from the station for as long as you can.",
CREATOR = "Scott Duensing", CREATOR = "Scott Duensing",
SOURCE = "http://kangaroopunch.com" SOURCE = "http://kangaroopunch.com"
}, },
@ -601,7 +601,7 @@ A script launched directly, by name or by its directory, gets the same
treatment: Singe looks for a `games.dat` in the script's directory and up to treatment: Singe looks for a `games.dat` in the script's directory and up to
three directories above it, and if an entry's `SCRIPT` names the launched three directories above it, and if an entry's `SCRIPT` names the launched
script (entry paths count from the directory above the `games.dat`, so script (entry paths count from the directory above the `games.dat`, so
`ActionMax/games.dat` names `ActionMax/38AmbushAlley.singe`), that entry's `Collection/games.dat` names `Collection/AsteroidPatrol.singe`), that entry's
keys apply. The one difference from a menu launch is precedence: an option keys apply. The one difference from a menu launch is precedence: an option
typed on the command line (`--framefile`, `--disc`, `--stretch`, `--nomouse`, typed on the command line (`--framefile`, `--disc`, `--stretch`, `--nomouse`,
`--xresolution`, `--yresolution`, `--sindengun`, `--audio`, `--audiodelay`, `--xresolution`, `--yresolution`, `--sindengun`, `--audio`, `--audiodelay`,
@ -639,8 +639,9 @@ document written in RML (an HTML-like markup) and styled by RCSS (a CSS-like
style sheet), laid out and drawn by RmlUi, with text wrapping, flexbox, style sheet), laid out and drawn by RmlUi, with text wrapping, flexbox,
nine-slice panels from your own artwork, transitions, animations, form nine-slice panels from your own artwork, transitions, animations, form
controls and a focus model a gamepad can drive. A *GUI* is one RmlUi controls and a focus model a gamepad can drive. A *GUI* is one RmlUi
context of a fixed size rendered into its own texture every frame, and that context of a fixed size rendered into its own texture, redrawn whenever its
texture goes wherever a texture can: composited over the overlay with picture changes (see <<guirefresh,guiRefresh>>), and that texture goes
wherever a texture can: composited over the overlay with
`guiDraw`, or shown on a material in the 3D scene with `materialSetGui`, so `guiDraw`, or shown on a material in the 3D scene with `materialSetGui`, so
the same options page works flat on the screen and on the monitor of a the same options page works flat on the screen and on the monitor of a
modeled arcade cabinet. A GUI needs the GPU device the 3D scene uses, so it modeled arcade cabinet. A GUI needs the GPU device the 3D scene uses, so it
@ -684,9 +685,9 @@ under `Singe/`, and absolute paths.
The same rule read the other way lets a document that lives under `Singe/` The same rule read the other way lets a document that lives under `Singe/`
reach a game's files: a relative name resolves from the document's own reach a game's files: a relative name resolves from the document's own
directory, so from `Singe/Menu.rml` the name `../ActionMax/cabinet.png` is directory, so from `Singe/Menu.rml` the name `../Collection/cabinet.png` is
`ActionMax/cabinet.png` in the game directory, and a name under `Collection/cabinet.png` in the game directory, and a name under
`../DLe.game/` is a file inside that packed game. The bundled menu does `../MyGame.game/` is a file inside that packed game. The bundled menu does
exactly this for the cabinet and marquee art of the selected game, setting exactly this for the cabinet and marquee art of the selected game, setting
an image decorator on the art panel from Lua so the picture is fitted an image decorator on the art panel from Lua so the picture is fitted
inside it whatever its size: inside it whatever its size:
@ -952,11 +953,13 @@ the script makes them again when it runs; nothing from the `rmlui` object
model survives, so do not keep those objects in globals a reload would model survives, so do not keep those objects in globals a reload would
reuse. reuse.
Every GUI updates and renders into its texture every frame whether or not Every GUI updates every frame whether or not it is drawn anywhere, so a
it is drawn anywhere, so a document keeps running its animations and document keeps running its animations and scripts off screen, and a GUI
scripts off screen, and a GUI that is finished with should be deleted that is finished with should be deleted rather than merely hidden. It is
rather than merely hidden. The cost is the size of the texture and the redrawn into its texture only when its picture changes, so a page or a HUD
complexity of the document: on a desktop a full screen page is cheap, on a that stands still costs the GPU almost nothing (see
<<guirefresh,guiRefresh>>). The cost of a frame that does change is the
size of the texture and the complexity of the document: on a desktop a full screen page is cheap, on a
Raspberry Pi keep a GUI to the overlay's size or smaller and prefer one Raspberry Pi keep a GUI to the overlay's size or smaller and prefer one
GUI with several documents to several GUIs. Each font face costs an atlas GUI with several documents to several GUIs. Each font face costs an atlas
per size it is used at, so a page that uses one face at three sizes pays per size it is used at, so a page that uses one face at three sizes pays
@ -964,6 +967,31 @@ for three atlases; a dozen sizes is a dozen. Sixteen GUIs may exist at
once, each with thirty-two documents, and sixteen `guiDraw` calls are once, each with thirty-two documents, and sixteen `guiDraw` calls are
honored per frame. honored per frame.
What a document can show, and what it cannot:
* *Pictures come from files.* Images, sprite sheets and image decorators
load PNG and the other formats Singe reads, from disk or from a packed
game. A document cannot show a Singe sprite, a video, the 3D scene or
anything drawn while the game runs, and a script cannot hand it pixels.
The other way round works: a GUI's texture goes onto a 3D material with
`materialSetGui`.
* *Decorators:* `image`, `text`, `tiled-horizontal`, `tiled-vertical`,
`tiled-box`, `ninepatch`, the two-colour `gradient` (and its
`horizontal-gradient` and `vertical-gradient` names), and `linear-gradient`,
`radial-gradient` and `conic-gradient` with their `repeating-` forms, with at
most sixteen colour stops. The custom `shader` decorator is refused.
* *Filters:* `opacity`, `blur`, `drop-shadow`, `brightness`, `contrast`,
`invert`, `grayscale`, `sepia`, `hue-rotate` and `saturate`. Any other
name is ignored with a warning in the trace.
* *Layers:* filters, `opacity` below 1, mask images and box shadows each
draw through a layer the size of the GUI, and at most eight stack at
once; one past that draws without its layer, and so without its effect.
* *Images* larger than 64 MB are refused.
* *RML and RCSS* are RmlUi's, close to HTML and CSS but not the same: there
is flexbox and no CSS grid, and a document is scripted in Lua, through
`guiSetHandler` and the `rmlui` object, rather than in JavaScript. RmlUi's
own documentation is the authority on what the languages hold.
==== A Worked Example ==== A Worked Example
An options page over the disc: a panel from the shipped theme with a name An options page over the disc: a panel from the shipped theme with a name
@ -2336,6 +2364,7 @@ manual: `docs/Forge.adoc` in the source tree, built to `Forge.html` and
`Forge.pdf` beside this one. `Forge.pdf` beside this one.
[[migrating]]
=== Migrating from Singe 2.10 === Migrating from Singe 2.10
Singe 3.00 moved the sprite handle to the first argument of `spriteDraw`, Singe 3.00 moved the sprite handle to the first argument of `spriteDraw`,
@ -2393,6 +2422,11 @@ changed shape, but a few behaviors did:
is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered
to the script only when the key has been disabled. It acts on the key to the script only when the key has been disabled. It acts on the key
press rather than the release. See <<pausing,Pausing>>. press rather than the release. See <<pausing,Pausing>>.
* `fontPrint` keeps the strings it renders, and `overlayClear` refills only
what was drawn since the last clear, so a HUD reprinted every frame is
cheap. Rendering text with `fontToSprite` for performance is deprecated;
it still works, and is still the way to get text you can measure, scale,
or turn. See <<fonttosprite,fontToSprite>>.
=== Engine Constants === Engine Constants
@ -2496,8 +2530,8 @@ of audio arriving early.
The bundled menu has a calibration screen for that downstream delay. Press the The bundled menu has a calibration screen for that downstream delay. Press the
key mapped to `INPUT_SERVICE` (the `9` key by default) on the game list and key mapped to `INPUT_SERVICE` (the `9` key by default) on the game list and
choose Audio Delay from the service tools (see <<servicetools,The Service choose Audio Delay from the service tools (see xref:Manual.adoc#servicetools[The Service
Tools>>). The screen is a page of the menu's document: it covers the menu in Tools]). The screen is a page of the menu's document: it covers the menu in
black, shows the delay being tried and the device queue the engine measured, black, shows the delay being tried and the device queue the engine measured,
plays a click once a second and turns white for one frame at each flash. plays a click once a second and turns white for one frame at each flash.
Adjust with left and right (10 ms) and up and down (1 ms) until the flash and Adjust with left and right (10 ms) and up and down (1 ms) until the flash and
@ -2664,19 +2698,6 @@ morph target weights are all driven. See the Animation part of
<<scenes3d,3D Scenes>>. <<scenes3d,3D Scenes>>.
[#animationgettime] [#animationgettime]
==== allowSocketCall
[source,text]
----
allowed = allowSocketCall()
----
Hypseus extension, always `false` in Singe. Hypseus answers whether the user let it send data to a stats server, which its `-noserversend` turns off; the sending itself is commented out there. Singe sends nothing anywhere, so nothing is permitted and the answer never changes. It exists so a game that asks does not stop on a missing function.
*Returns:* a boolean, always `false`.
*Since:* 3.00
==== animationGetTime ==== animationGetTime
[source,text] [source,text]
@ -3806,6 +3827,7 @@ met but *where the mover ends up* -- sliding along a wall rather than stopping
dead in it -- `require("bump")` and let it do that; it is bundled for exactly dead in it -- `require("bump")` and let it do that; it is bundled for exactly
that reason. that reason.
[#collidecircles]
==== collideCircles ==== collideCircles
[source,text] [source,text]
@ -3825,6 +3847,7 @@ Whether two circles overlap. Cheaper than it looks: nothing takes a square root
*Since:* 3.00. *Since:* 3.00.
*See also:* <<colliderectcircle,collideRectCircle>> *See also:* <<colliderectcircle,collideRectCircle>>
[#collidepointcircle]
==== collidePointCircle ==== collidePointCircle
[source,text] [source,text]
@ -3843,6 +3866,7 @@ Whether a point falls within a circle. The edge counts as inside.
*Since:* 3.00. *Since:* 3.00.
[#collidepointpolygon]
==== collidePointPolygon ==== collidePointPolygon
[source,text] [source,text]
@ -3877,6 +3901,7 @@ function onInputPressed(key)
end end
---- ----
[#collidepointrect]
==== collidePointRect ==== collidePointRect
[source,text] [source,text]
@ -3907,6 +3932,7 @@ if collidePointRect(x, y, enemyX, enemyY, spriteGetWidth(enemy), spriteGetHeight
end end
---- ----
[#colliderectcircle]
==== collideRectCircle ==== collideRectCircle
[source,text] [source,text]
@ -3925,6 +3951,7 @@ Whether a rectangle and a circle overlap, by finding the rectangle's nearest poi
*Since:* 3.00. *Since:* 3.00.
[#colliderects]
==== collideRects ==== collideRects
[source,text] [source,text]
@ -3944,6 +3971,7 @@ Whether two rectangles overlap. The one you will reach for most.
*Since:* 3.00. *Since:* 3.00.
*See also:* <<collidepointrect,collidePointRect>> *See also:* <<collidepointrect,collidePointRect>>
[#collidesegments]
==== collideSegments ==== collideSegments
[source,text] [source,text]
@ -4285,7 +4313,7 @@ for slot = 0, 3 do
end end
---- ----
[#controllerhowmany] [#controllergetpadding]
==== controllerGetPadding ==== controllerGetPadding
[source,text] [source,text]
@ -4300,6 +4328,7 @@ Hypseus extension, always `0` in Singe. Hypseus adds this offset to a switch cod
*Since:* 3.00 *Since:* 3.00
*See also:* <<controllersetpadding,controllerSetPadding>> *See also:* <<controllersetpadding,controllerSetPadding>>
[#controllerhowmany]
==== controllerHowMany ==== controllerHowMany
[source,text] [source,text]
@ -4307,7 +4336,7 @@ Hypseus extension, always `0` in Singe. Hypseus adds this offset to a switch cod
count = controllerHowMany() count = controllerHowMany()
---- ----
Hypseus extension. How many gamepads are open, of the four slots Singe has. Since every joystick Singe can open becomes a gamepad (see <<everydeviceisagamepad,Every Device Is a Gamepad>>), an arcade encoder or a spinner is counted here as well. Hypseus extension. How many gamepads are open, of the four slots Singe has. Since every joystick Singe can open becomes a gamepad (see xref:Manual.adoc#devices[Every Device Is a Gamepad]), an arcade encoder or a spinner is counted here as well.
The count changes as devices are plugged in and out, so read it when you need it rather than once at startup. It says nothing about which slots are filled: a device may leave slot 0 empty while slot 1 is in use, so <<controllerisvalid,`controllerIsValid`>> is still the way to ask about a particular slot. The count changes as devices are plugged in and out, so read it when you need it rather than once at startup. It says nothing about which slots are filled: a device may leave slot 0 empty while slot 1 is in use, so <<controllerisvalid,`controllerIsValid`>> is still the way to ask about a particular slot.
@ -4357,12 +4386,7 @@ function drawAttract()
end end
---- ----
[#debug] [#controllersetpadding]
=== Debug
Two calls for seeing what a script is doing: `debugPrint` writes text, and `lineDraw` draws a line over the 3D scene for one frame (`physicsSetDebug` and `navDraw` draw through the same lines). Printed output goes to the console Singe was started from, so it is visible during development and harmless in a cabinet, where `--noconsole` silences it. When Singe runs with `--trace`, every API call including this one is also written with its arguments to `trace.txt` in the data directory, which is the way to get a permanent record. Command line options are listed in the chapter on running Singe; the ZeroBrane Studio integration in `zbstudio/` offers a debugger when printing is not enough.
[[traceheader]]
==== controllerSetPadding ==== controllerSetPadding
[source,text] [source,text]
@ -4377,6 +4401,12 @@ Hypseus extension, accepted and ignored. See <<controllergetpadding,controllerGe
*Since:* 3.00 *Since:* 3.00
*See also:* <<controllergetpadding,controllerGetPadding>> *See also:* <<controllergetpadding,controllerGetPadding>>
[#debug]
=== Debug
Two calls for seeing what a script is doing: `debugPrint` writes text, and `lineDraw` draws a line over the 3D scene for one frame (`physicsSetDebug` and `navDraw` draw through the same lines). Printed output goes to the console Singe was started from, so it is visible during development and harmless in a cabinet, where `--noconsole` silences it. When Singe runs with `--trace`, every API call including this one is also written with its arguments to `trace.txt` in the data directory, which is the way to get a permanent record. Command line options are listed in the chapter on running Singe; the ZeroBrane Studio integration in `zbstudio/` offers a debugger when printing is not enough.
[[traceheader]]
==== The Trace Header ==== The Trace Header
Whichever of `--program` and `--trace` opened it, `trace.txt` begins with a Whichever of `--program` and `--trace` opened it, `trace.txt` begins with a
@ -4386,14 +4416,14 @@ whole into a bug report:
---- ----
---------------------------------------------------------------- ----------------------------------------------------------------
Singe: v3.00 Singe: v3.00
Command: ./Singe -w -p -d data --screen 2 ActionMax Command: ./Singe -w -p -d data --screen 2 MyGame
OS: Linux 6.8.0-124-generic (x86_64) OS: Linux 6.8.0-124-generic (x86_64)
CPU: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz, 8 cores, 20177 MB CPU: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz, 8 cores, 20177 MB
Renderer: gpu, GPU driver vulkan Renderer: gpu, GPU driver vulkan
Decoder: hardware vaapi or vdpau where the codec allows, software otherwise Decoder: hardware vaapi or vdpau where the codec allows, software otherwise
SDL: built 3.4.16, linked 3.4.16 SDL: built 3.4.16, linked 3.4.16
Settings: haptic=0 screen=2 volume_vldp=80 Settings: haptic=0 screen=2 volume_vldp=80
Game: ActionMax/ActionMax.singe Game: MyGame/MyGame.singe
---------------------------------------------------------------- ----------------------------------------------------------------
---- ----
@ -4479,19 +4509,6 @@ end
The *disc* is the main laserdisc video, the one Singe was originally built to emulate. A game has at most one disc, opened at startup from the `VIDEO` named in `games.dat` or from `--disc` / `--framefile` on the command line; a game that names none runs without a disc (see <<withoutadisc,Games Without a Disc>>). All `disc*` functions operate on this single implicit disc and take no handle. Frame numbers are zero based positions in the video file (or in the combined framefile) and are the time reference for every branch decision in a laserdisc game. Without a disc the query functions return `0`, `discGetState` reports `DISC_EJECTED`, and the transport functions trace and do nothing. Extra clips layered on top of the disc are the job of the <<video,Video>> family. The *disc* is the main laserdisc video, the one Singe was originally built to emulate. A game has at most one disc, opened at startup from the `VIDEO` named in `games.dat` or from `--disc` / `--framefile` on the command line; a game that names none runs without a disc (see <<withoutadisc,Games Without a Disc>>). All `disc*` functions operate on this single implicit disc and take no handle. Frame numbers are zero based positions in the video file (or in the combined framefile) and are the time reference for every branch decision in a laserdisc game. Without a disc the query functions return `0`, `discGetState` reports `DISC_EJECTED`, and the transport functions trace and do nothing. Extra clips layered on top of the disc are the job of the <<video,Video>> family.
[#discaudio] [#discaudio]
==== drawTransparent
[source,text]
----
drawTransparent(boolean)
----
Hypseus extension, accepted and ignored. It decides whether Hypseus sets colour key 0 on the sprites loaded after it, which is how that engine carries transparency. Singe carries transparency in the alpha channel, as <<spriteresetcolorkey,`spriteResetColorKey`>> already describes, so there is no key to turn on or off.
*Returns:* nothing.
*Since:* 3.00
==== discAudio ==== discAudio
[source,text] [source,text]
@ -6091,7 +6108,7 @@ titleFont = fontLoad("Singe/FreeSansBold.ttf", 32)
hudFont = fontLoad("Singe/FreeSansBold.ttf", 18) hudFont = fontLoad("Singe/FreeSansBold.ttf", 18)
fontSelect(titleFont) fontSelect(titleFont)
titleSprite = fontToSprite("SPACE ACE") titleSprite = fontToSprite("GAME OVER")
fontSelect(hudFont) fontSelect(hudFont)
---- ----
@ -6103,7 +6120,7 @@ fontSelect(hudFont)
fontPrint(x, y, text) fontPrint(x, y, text)
---- ----
Renders a string with the selected font, the current quality, and the current foreground color (plus the background color for `FONT_QUALITY_SHADED`), then blits it onto the overlay with its top-left corner at `(x, y)`. Call it from `onOverlayUpdate`. A new text image is rendered and discarded on every call, which is fine for a few lines but expensive for text that never changes; `fontToSprite` renders once and draws many times. With no font selected the script ends with an error. Renders a string with the selected font, the current quality, and the current foreground color (plus the background color for `FONT_QUALITY_SHADED`), then blits it onto the overlay with its top-left corner at `(x, y)`. Call it from `onOverlayUpdate`, every frame if need be: the engine keeps each string it renders, keyed by the font, quality, colors, and text, and a later call asking for the same one only blits it. Strings not printed for about a second are forgotten, and a font's strings go when it is unloaded, so text that changes costs no more than it always did and text that does not is nearly free. A whole page of high scores reprinted every frame is fine. With no font selected the script ends with an error.
*Parameters:* *Parameters:*
@ -6150,7 +6167,7 @@ Selects how `fontPrint` and `fontToSprite` rasterize text from now on. The setti
---- ----
-- Blended text over the video, but a shaded label box for the debug readout. -- Blended text over the video, but a shaded label box for the debug readout.
fontQuality(FONT_QUALITY_BLENDED) fontQuality(FONT_QUALITY_BLENDED)
titleSprite = fontToSprite("DRAGON'S LAIR") titleSprite = fontToSprite("HIGH SCORES")
colorBackground(0, 0, 0, 255) colorBackground(0, 0, 0, 255)
fontQuality(FONT_QUALITY_SHADED) fontQuality(FONT_QUALITY_SHADED)
@ -6178,7 +6195,7 @@ Makes a loaded font the one used by `fontPrint` and `fontToSprite`. The selectio
function onOverlayUpdate() function onOverlayUpdate()
overlayClear() overlayClear()
fontSelect(titleFont) fontSelect(titleFont)
fontPrint(TITLE_X, 40, "DRAGON'S LAIR") fontPrint(TITLE_X, 40, "HIGH SCORES")
fontSelect(hudFont) fontSelect(hudFont)
fontPrint(8, 8, "Score: " .. score) fontPrint(8, 8, "Score: " .. score)
return OVERLAY_UPDATED return OVERLAY_UPDATED
@ -6193,7 +6210,9 @@ end
result = fontToSprite(text) result = fontToSprite(text)
---- ----
Renders a string with the selected font, quality, and colors into a new sprite and returns its handle. The sprite is independent of the font from then on, so it survives `fontUnload`, and it works with every `sprite*` call: draw it, measure it, scale or rotate it, and free it with `spriteUnload`. Render once and draw many times for labels and headings; for a string that changes, render a new sprite and unload the old one. With no font selected the script ends with an error. Renders a string with the selected font, quality, and colors into a new sprite and returns its handle. The sprite is independent of the font from then on, so it survives `fontUnload`, and it works with every `sprite*` call: draw it, measure it, scale or rotate it, and free it with `spriteUnload`. For a string that changes, render a new sprite and unload the old one. With no font selected the script ends with an error.
NOTE: Rendering text to a sprite for performance is deprecated. `fontPrint` now keeps what it renders, so printing the same string every frame costs about what drawing a sprite made from it does, without the sprite to manage. Use `fontToSprite` when the text has to be a sprite: to scale it, rotate it, flip it, or measure it with `spriteGetWidth` and `spriteGetHeight`.
*Returns:* integer sprite handle. *Returns:* integer sprite handle.
@ -6203,18 +6222,15 @@ Renders a string with the selected font, quality, and colors into a new sprite a
.Example .Example
[source,lua] [source,lua]
---- ----
-- Pre-render the game description, wrapping lines to the panel width. -- A title that spins and grows in: text that has to be a sprite, to be turned and scaled.
descriptionLines = {} titleSprite = fontToSprite("GAME OVER")
for _, line in ipairs(wrapText(GAME_LIST[selected].DESCRIPTION, TEXT_W)) do titleFrames = 0
table.insert(descriptionLines, fontToSprite(line))
end
function onOverlayUpdate() function onOverlayUpdate()
local y = TEXT_Y titleFrames = titleFrames + 1
for _, line in ipairs(descriptionLines) do spriteRotate(titleSprite, (titleFrames * 12) % 360)
spriteDraw(line, TEXT_X, y) spriteScale(titleSprite, math.min(titleFrames / 30, 1))
y = y + spriteGetHeight(line) + 1 spriteDraw(titleSprite, overlayGetWidth() / 2, overlayGetHeight() / 2, true)
end
return OVERLAY_UPDATED return OVERLAY_UPDATED
end end
---- ----
@ -6368,20 +6384,6 @@ end
---- ----
[#guigetheight] [#guigetheight]
==== getUserString
[source,text]
----
id = getUserString()
----
Hypseus extension, always an empty string. Despite the name it is not a string the user set: Hypseus answers the machine install ID it sends with network statistics, taken from the Windows `MachineGuid` or the Linux machine ID. Singe sends nothing anywhere and will not hand a game a fingerprint of the machine it is running on.
*Returns:* a string, always empty.
*Since:* 3.00
*See also:* <<allowsocketcall,allowSocketCall>>
==== guiGetHeight ==== guiGetHeight
[source,text] [source,text]
@ -6413,7 +6415,7 @@ guiDraw(dialog, x, y)
stats = guiGetStats() stats = guiGetStats()
---- ----
What rendering every GUI cost in the last completed frame, for tuning documents that use filters, masks and shadows (each of which draws through a layer the size of the GUI). The counters cover all GUIs together. What rendering every GUI cost in the last completed frame, for tuning documents that use filters, masks and shadows (each of which draws through a layer the size of the GUI). The counters cover all GUIs together. They are all zero on a frame where no GUI's picture changed, since nothing was redrawn (see <<guirefresh,guiRefresh>>).
*Returns:* A table with the fields `drawCalls` (indexed draws, including mask writes, composites and filter passes), `layerPushes` (layers entered for filters, mask images and box shadows), `filterPasses` (full-size passes and blits inside filter chains; a blur costs several), `maskWrites` (draws into the stencil clip mask), `layerBytes` (bytes of layer textures currently allocated; each is the size of its GUI at four bytes a pixel) and `passes` (render passes begun; every layer switch begins one). *Returns:* A table with the fields `drawCalls` (indexed draws, including mask writes, composites and filter passes), `layerPushes` (layers entered for filters, mask images and box shadows), `filterPasses` (full-size passes and blits inside filter chains; a blur costs several), `maskWrites` (draws into the stencil clip mask), `layerBytes` (bytes of layer textures currently allocated; each is the size of its GUI at four bytes a pixel) and `passes` (render passes begun; every layer switch begins one).
@ -6590,6 +6592,30 @@ function onOverlayUpdate()
end end
---- ----
[#guirefresh]
==== guiRefresh
[source,text]
----
guiRefresh(gui)
----
Draws the GUI again on the next frame even if nothing about it seems to have changed. A GUI is only redrawn when its picture changes: when a frame's document draws exactly what the last frame drew, and nothing new was made in between, the texture already holds that picture and is kept, which saves the GPU most of a GUI's cost on the frames a HUD or a menu stands still. Changes made through RML, RCSS, the RmlUi Lua interface or the `gui*` calls are all seen. This call is the way out should one ever be missed -- a GUI that stops updating when it should -- and a game should not need it; if it does, that is worth reporting. A GUI that does not exist raises an error.
*Since:* 3.00.
*See also:* <<guidraw,guiDraw>>, <<guigetstats,guiGetStats>>
.Example
[source,lua]
----
-- A workaround while a missed change is reported: redraw the HUD every frame.
function onOverlayUpdate()
guiRefresh(hud)
guiDraw(hud)
return OVERLAY_UPDATED
end
----
[#guisethandler] [#guisethandler]
==== guiSetHandler ==== guiSetHandler
@ -6887,8 +6913,8 @@ end
Every controller Singe opens is a gamepad and is read through the `controller*` Every controller Singe opens is a gamepad and is read through the `controller*`
calls; a device SDL does not recognise reaches them through a mapping Singe calls; a device SDL does not recognise reaches them through a mapping Singe
writes for it, so there is nothing else to call. See <<devices,Every Device Is writes for it, so there is nothing else to call. See xref:Manual.adoc#devices[Every Device Is
a Gamepad>>. The two calls here are about letting a stick stand in for a mouse. a Gamepad]. The two calls here are about letting a stick stand in for a mouse.
[#joymouseenable] [#joymouseenable]
==== joyMouseEnable ==== joyMouseEnable
@ -8360,6 +8386,7 @@ interfaces and any synthesiser or other program registering one. macOS uses
CoreMIDI and Windows the multimedia MIDI calls, where output device `0` is CoreMIDI and Windows the multimedia MIDI calls, where output device `0` is
usually the synthesiser the system itself provides. usually the synthesiser the system itself provides.
[#midicloseinput]
==== midiCloseInput ==== midiCloseInput
[source,text] [source,text]
@ -8378,6 +8405,7 @@ Stops listening to the input port. <<onmidimessage,`onMidiMessage`>> stops firin
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midiopeninput,midiOpenInput>> *See also:* <<midiopeninput,midiOpenInput>>
[#midicloseoutput]
==== midiCloseOutput ==== midiCloseOutput
[source,text] [source,text]
@ -8396,6 +8424,7 @@ Releases the output port. The sending calls answer `false` from now on. Closing
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midiopenoutput,midiOpenOutput>> *See also:* <<midiopenoutput,midiOpenOutput>>
[#midicontrolchange]
==== midiControlChange ==== midiControlChange
[source,text] [source,text]
@ -8423,6 +8452,7 @@ Moves a controller on the open output port: volume is controller `7`, pan is `10
midiControlChange(1, 7, 40) midiControlChange(1, 7, 40)
---- ----
[#midiinputcount]
==== midiInputCount ==== midiInputCount
[source,text] [source,text]
@ -8441,6 +8471,7 @@ How many MIDI input ports this machine has. `0` when there are none, or when the
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midiinputname,midiInputName>> *See also:* <<midiinputname,midiInputName>>
[#midiinputname]
==== midiInputName ==== midiInputName
[source,text] [source,text]
@ -8467,6 +8498,7 @@ for i = 0, midiInputCount() - 1 do
end end
---- ----
[#midiisinputopen]
==== midiIsInputOpen ==== midiIsInputOpen
[source,text] [source,text]
@ -8485,6 +8517,7 @@ Whether an input port is open.
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midiopeninput,midiOpenInput>> *See also:* <<midiopeninput,midiOpenInput>>
[#midiisoutputopen]
==== midiIsOutputOpen ==== midiIsOutputOpen
[source,text] [source,text]
@ -8503,6 +8536,7 @@ Whether an output port is open. Worth asking before a burst of messages, since e
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midiopenoutput,midiOpenOutput>> *See also:* <<midiopenoutput,midiOpenOutput>>
[#midinoteoff]
==== midiNoteOff ==== midiNoteOff
[source,text] [source,text]
@ -8522,6 +8556,7 @@ Releases a note on the open output port. A note left on sounds until something s
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midinoteon,midiNoteOn>> *See also:* <<midinoteon,midiNoteOn>>
[#midinoteon]
==== midiNoteOn ==== midiNoteOn
[source,text] [source,text]
@ -8550,6 +8585,7 @@ midiNoteOn(10, 49, 110)
midiNoteOff(10, 49) midiNoteOff(10, 49)
---- ----
[#midiopeninput]
==== midiOpenInput ==== midiOpenInput
[source,text] [source,text]
@ -8576,6 +8612,7 @@ if midiInputCount() > 0 then
end end
---- ----
[#midiopenoutput]
==== midiOpenOutput ==== midiOpenOutput
[source,text] [source,text]
@ -8594,6 +8631,7 @@ Takes one output port for the sending calls. Whichever output port was open is c
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midicloseoutput,midiCloseOutput>> *See also:* <<midicloseoutput,midiCloseOutput>>
[#midioutputcount]
==== midiOutputCount ==== midiOutputCount
[source,text] [source,text]
@ -8612,6 +8650,7 @@ How many MIDI output ports this machine has.
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midioutputname,midiOutputName>> *See also:* <<midioutputname,midiOutputName>>
[#midioutputname]
==== midiOutputName ==== midiOutputName
[source,text] [source,text]
@ -8630,6 +8669,7 @@ The name of an output port.
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midioutputcount,midiOutputCount>> *See also:* <<midioutputcount,midiOutputCount>>
[#midipitchbend]
==== midiPitchBend ==== midiPitchBend
[source,text] [source,text]
@ -8648,6 +8688,7 @@ Bends a channel. `8192` is the middle, meaning no bend; how far `0` and `16383`
*Since:* 3.00. *Since:* 3.00.
[#midiprogramchange]
==== midiProgramChange ==== midiProgramChange
[source,text] [source,text]
@ -8666,6 +8707,7 @@ Chooses the sound a channel plays. The numbers are the receiving device's; on a
*Since:* 3.00. *Since:* 3.00.
[#midirescan]
==== midiRescan ==== midiRescan
[source,text] [source,text]
@ -8684,6 +8726,7 @@ Looks for ports again, for a device plugged in while the game is running. An ope
*Since:* 3.00. *Since:* 3.00.
*See also:* <<midiinputcount,midiInputCount>>, <<midioutputcount,midiOutputCount>> *See also:* <<midiinputcount,midiInputCount>>, <<midioutputcount,midiOutputCount>>
[#midisend]
==== midiSend ==== midiSend
[source,text] [source,text]
@ -8927,7 +8970,7 @@ for device = 0, mouseHowMany() - 1 do
end end
---- ----
[#mousegetposition] [#mousegetpadding]
==== mouseGetPadding ==== mouseGetPadding
[source,text] [source,text]
@ -8942,6 +8985,7 @@ Hypseus extension, always `0`, for the reason <<controllergetpadding,`controller
*Since:* 3.00 *Since:* 3.00
*See also:* <<controllergetpadding,controllerGetPadding>> *See also:* <<controllergetpadding,controllerGetPadding>>
[#mousegetposition]
==== mouseGetPosition ==== mouseGetPosition
[source,text] [source,text]
@ -11034,7 +11078,7 @@ end
overlayClear() overlayClear()
---- ----
Fills the whole overlay with the current background color. The background starts as transparent black, so with no `colorBackground` call this erases the overlay back to invisible and the video shows through. A partly opaque background dims the video; a fully opaque one hides it. Most games call it first thing in `onOverlayUpdate`. Any arguments are ignored. Fills the whole overlay with the current background color. When the color is the same as the last clear's, only what was drawn since is filled again, so clearing an overlay that holds a few lines of text costs next to nothing whatever its size. The background starts as transparent black, so with no `colorBackground` call this erases the overlay back to invisible and the video shows through. A partly opaque background dims the video; a fully opaque one hides it. Most games call it first thing in `onOverlayUpdate`. Any arguments are ignored.
*Since:* 1.x *Since:* 1.x
*See also:* <<colorbackground,colorBackground>>, <<onoverlayupdate,onOverlayUpdate>> *See also:* <<colorbackground,colorBackground>>, <<onoverlayupdate,onOverlayUpdate>>
@ -11329,7 +11373,7 @@ MARQUEE_X = overlayGetWidth() - MARGIN_X - MARQUEE_W
CABINET_H = overlayGetHeight() - MARGIN_Y - CABINET_Y CABINET_H = overlayGetHeight() - MARGIN_Y - CABINET_Y
---- ----
[#setoverlaylinearscale] [#setoverlayfullalpha]
==== setOverlayFullAlpha ==== setOverlayFullAlpha
[source,text] [source,text]
@ -11343,6 +11387,7 @@ Hypseus extension, accepted and ignored. Hypseus blits its overlay with the alph
*Since:* 3.00 *Since:* 3.00
[#setoverlaylinearscale]
==== setOverlayLinearScale ==== setOverlayLinearScale
[source,text] [source,text]
@ -12407,6 +12452,7 @@ Whole numbers come back as whole numbers. JSON has only one kind of number and
Lua has two, so a score saved as `100` would otherwise return as `100.0` and Lua has two, so a score saved as `100` would otherwise return as `100.0` and
print with a decimal point. print with a decimal point.
[#saveclear]
==== saveClear ==== saveClear
[source,text] [source,text]
@ -12424,6 +12470,7 @@ Forgets everything and writes the empty save out. For a service menu's "reset hi
*Since:* 3.00. *Since:* 3.00.
[#savedelete]
==== saveDelete ==== saveDelete
[source,text] [source,text]
@ -12441,6 +12488,7 @@ Removes one key. Deleting a key that was never there is not an error.
*Since:* 3.00. *Since:* 3.00.
[#saveflush]
==== saveFlush ==== saveFlush
[source,text] [source,text]
@ -12459,6 +12507,7 @@ Writes the save out now rather than at the end of the frame. Use it after someth
*Since:* 3.00. *Since:* 3.00.
*See also:* <<saveset,saveSet>> *See also:* <<saveset,saveSet>>
[#saveget]
==== saveGet ==== saveGet
[source,text] [source,text]
@ -12493,6 +12542,7 @@ saveSet("highScores", best)
saveFlush() saveFlush()
---- ----
[#savegetall]
==== saveGetAll ==== saveGetAll
[source,text] [source,text]
@ -12518,6 +12568,7 @@ A copy of the whole save, for reading: dumping it while you are debugging, or wa
debugPrint(require("inspect")(saveGetAll())) debugPrint(require("inspect")(saveGetAll()))
---- ----
[#saveset]
==== saveSet ==== saveSet
[source,text] [source,text]
@ -12537,6 +12588,7 @@ Keeps a value until the game is uninstalled. The write happens at the end of the
*Since:* 3.00. *Since:* 3.00.
*See also:* <<saveget,saveGet>>, <<saveflush,saveFlush>> *See also:* <<saveget,saveGet>>, <<saveflush,saveFlush>>
[#savesetall]
==== saveSetAll ==== saveSetAll
[source,text] [source,text]
@ -13164,8 +13216,8 @@ Replaces the running script with the one the table describes. The current script
function onInputPressed(what) function onInputPressed(what)
if what == SWITCH_START1 then if what == SWITCH_START1 then
scriptExecute({ scriptExecute({
SCRIPT = DIR .. "../Lair/Lair.singe", SCRIPT = DIR .. "../MyGame/MyGame.singe",
VIDEO = DIR .. "../Lair/Lair.mp4", VIDEO = DIR .. "../MyGame/MyGame.mp4",
}) })
end end
end end
@ -13205,6 +13257,35 @@ end
Functions in the `singe*` namespace control the engine itself: the window, screenshots, the pause system, quitting and reloading, the audio delay used for lip sync, and a few paths and flags that come from the command line and `games.dat`. Nothing here takes a handle. Pause behavior is described in <<pausing,Pausing>> and the audio delay values in <<audiosync,Audio Sync>>. Functions in the `singe*` namespace control the engine itself: the window, screenshots, the pause system, quitting and reloading, the audio delay used for lip sync, and a few paths and flags that come from the command line and `games.dat`. Nothing here takes a handle. Pause behavior is described in <<pausing,Pausing>> and the audio delay values in <<audiosync,Audio Sync>>.
[#allowsocketcall]
==== allowSocketCall
[source,text]
----
allowed = allowSocketCall()
----
Hypseus extension, always `false` in Singe. Hypseus answers whether the user let it send data to a stats server, which its `-noserversend` turns off; the sending itself is commented out there. Singe sends nothing anywhere, so nothing is permitted and the answer never changes. It exists so a game that asks does not stop on a missing function.
*Returns:* a boolean, always `false`.
*Since:* 3.00
[#getuserstring]
==== getUserString
[source,text]
----
id = getUserString()
----
Hypseus extension, always an empty string. Despite the name it is not a string the user set: Hypseus answers the machine install ID it sends with network statistics, taken from the Windows `MachineGuid` or the Linux machine ID. Singe sends nothing anywhere and will not hand a game a fingerprint of the machine it is running on.
*Returns:* a string, always empty.
*Since:* 3.00
*See also:* <<allowsocketcall,allowSocketCall>>
[#singedisablepausekey] [#singedisablepausekey]
==== singeDisablePauseKey ==== singeDisablePauseKey
@ -13917,7 +13998,7 @@ Sets the title of the Singe window. Call it once at startup with the game's disp
.Example .Example
[source,lua] [source,lua]
---- ----
singeSetGameName("Space Ace") singeSetGameName("Asteroid Patrol")
keyboardSetMode(MODE_NORMAL) keyboardSetMode(MODE_NORMAL)
hudFont = fontLoad(DIR .. "fonts/FreeSansBold.ttf", 18) hudFont = fontLoad(DIR .. "fonts/FreeSansBold.ttf", 18)
---- ----
@ -14843,7 +14924,21 @@ Sprites are 2D bitmaps loaded from disk with `spriteLoad`, or rendered from text
*Sprite sheets.* Hypseus Singe extends the family with sprite sheets: `spriteLoadFrames` slices a single row strip of equal frames into an animation that is not playing, and `spriteDrawFrame` draws one chosen frame (1-based) without touching playback. A sheet is otherwise an ordinary animation, so `spriteSetFrame` (0-based), `spritePlay`, `spriteDraw`, `nodeSetSprite` and `emitterSetTexture` all accept it, and `spriteFrameWidth` and `spriteFrameHeight` report the unscaled size of one frame. The entries marked "Hypseus extension" exist so games written for Hypseus Singe run unchanged. *Sprite sheets.* Hypseus Singe extends the family with sprite sheets: `spriteLoadFrames` slices a single row strip of equal frames into an animation that is not playing, and `spriteDrawFrame` draws one chosen frame (1-based) without touching playback. A sheet is otherwise an ordinary animation, so `spriteSetFrame` (0-based), `spritePlay`, `spriteDraw`, `nodeSetSprite` and `emitterSetTexture` all accept it, and `spriteFrameWidth` and `spriteFrameHeight` report the unscaled size of one frame. The entries marked "Hypseus extension" exist so games written for Hypseus Singe run unchanged.
[#spritedraw] [#drawtransparent]
==== drawTransparent
[source,text]
----
drawTransparent(boolean)
----
Hypseus extension, accepted and ignored. It decides whether Hypseus sets colour key 0 on the sprites loaded after it, which is how that engine carries transparency. Singe carries transparency in the alpha channel, as <<spriteresetcolorkey,`spriteResetColorKey`>> already describes, so there is no key to turn on or off.
*Returns:* nothing.
*Since:* 3.00
[#spriteblend]
==== spriteBlend ==== spriteBlend
[source,text] [source,text]
@ -14857,6 +14952,7 @@ Hypseus extension, accepted and ignored. It is a workaround for that engine: Hyp
*Since:* 3.00 *Since:* 3.00
[#spritedraw]
==== spriteDraw ==== spriteDraw
[source,text] [source,text]
@ -15770,7 +15866,7 @@ if srtLoad("subs/" .. language .. ".srt") then
end end
---- ----
[#srtposition] [#srtloadtrack]
==== srtLoadTrack ==== srtLoadTrack
[source,text] [source,text]
@ -15805,6 +15901,7 @@ end
srtEnable(true) srtEnable(true)
---- ----
[#srtposition]
==== srtPosition ==== srtPosition
[source,text] [source,text]
@ -15988,6 +16085,7 @@ function onInputPressed(key)
end end
---- ----
[#statsisenabled]
==== statsIsEnabled ==== statsIsEnabled
[source,text] [source,text]
@ -16066,6 +16164,7 @@ the game catches up rather than firing the twenty times it missed.
Everything is forgotten when the script reloads, so a timer from the last run Everything is forgotten when the script reloads, so a timer from the last run
never fires into the next one. never fires into the next one.
[#timerafter]
==== timerAfter ==== timerAfter
[source,text] [source,text]
@ -16095,6 +16194,7 @@ timerAfter(1500, function()
end) end)
---- ----
[#timercancel]
==== timerCancel ==== timerCancel
[source,text] [source,text]
@ -16112,6 +16212,7 @@ Stops a timer. Cancelling one that has already fired is not an error, so nothing
*Since:* 3.00. *Since:* 3.00.
[#timerevery]
==== timerEvery ==== timerEvery
[source,text] [source,text]
@ -16146,6 +16247,7 @@ clock = timerEvery(1000, function()
end) end)
---- ----
[#timerisactive]
==== timerIsActive ==== timerIsActive
[source,text] [source,text]
@ -16181,6 +16283,7 @@ to sharp; `EASE_BACK_*` overshoots and comes back; `EASE_ELASTIC_*` springs;
`EASE_BOUNCE_*` drops and bounces. Each comes in `_IN` (slow at the start), `EASE_BOUNCE_*` drops and bounces. Each comes in `_IN` (slow at the start),
`_OUT` (slow at the end) and `_IN_OUT` (both). `_OUT` (slow at the end) and `_IN_OUT` (both).
[#tweencancel]
==== tweenCancel ==== tweenCancel
[source,text] [source,text]
@ -16198,6 +16301,7 @@ Stops a tween where it is. Neither function is called again, and the value stays
*Since:* 3.00. *Since:* 3.00.
[#tweenisactive]
==== tweenIsActive ==== tweenIsActive
[source,text] [source,text]
@ -16215,6 +16319,7 @@ Whether the tween is still running.
*Since:* 3.00. *Since:* 3.00.
[#tweenvalue]
==== tweenValue ==== tweenValue
[source,text] [source,text]
@ -16956,6 +17061,8 @@ videoDraw(id, x, y, centered)
Advances a video to the frame its clock calls for and draws that frame on the overlay. The clock follows the video's audio when it has any and wall time otherwise, so a call may advance zero, one or several frames; a video that reaches its end loops back to frame `0`. The five argument form stretches the frame into the rectangle from `(x, y)` to `(x2, y2)` inclusive, always with nearest-neighbor sampling and ignoring `videoScale`, `videoRotate` and `videoQuality`. The four argument form draws the frame at its native size transformed by `videoScale` and `videoRotate`, anchored at its top left corner or, when `centered` is `true`, at its center; the transformed frame is rebuilt only when the frame or the transform changes, and rotated corners are transparent. If the decoder has not produced a frame yet nothing is drawn. Advances a video to the frame its clock calls for and draws that frame on the overlay. The clock follows the video's audio when it has any and wall time otherwise, so a call may advance zero, one or several frames; a video that reaches its end loops back to frame `0`. The five argument form stretches the frame into the rectangle from `(x, y)` to `(x2, y2)` inclusive, always with nearest-neighbor sampling and ignoring `videoScale`, `videoRotate` and `videoQuality`. The four argument form draws the frame at its native size transformed by `videoScale` and `videoRotate`, anchored at its top left corner or, when `centered` is `true`, at its center; the transformed frame is rebuilt only when the frame or the transform changes, and rotated corners are transparent. If the decoder has not produced a frame yet nothing is drawn.
A frame that is not scaled or turned by `videoScale` or `videoRotate`, drawn on the overlay where nothing has been drawn since an `overlayClear` to a transparent color -- a background video drawn first, say -- is drawn by the GPU instead, under the overlay, and costs next to nothing; anything drawn afterwards still goes over it. It is then scaled to the screen the way the overlay is, rather than nearest-neighbor into the overlay first. Everywhere else the frame is copied into the overlay on the CPU, which on a small machine costs several milliseconds a frame, so draw a video before anything that goes over it rather than after.
*Parameters:* *Parameters:*
* `id` -- video handle from `videoLoad`. * `id` -- video handle from `videoLoad`.
@ -18239,7 +18346,7 @@ function onKeyPressed(keysym, scancode)
end end
---- ----
[#onmousemoved] [#onmidimessage]
==== onMidiMessage ==== onMidiMessage
[source,text] [source,text]
@ -18286,6 +18393,7 @@ function onMidiMessage(status, data1, data2, bytes)
end end
---- ----
[#onmousemoved]
==== onMouseMoved ==== onMouseMoved
[source,text] [source,text]
@ -18558,8 +18666,10 @@ real engine calls under <<overlay,Overlay>>; `mainBezelLoaded` and the seven
`scoreBezel` calls are real engine calls under <<bezelref,Bezel>>; and `scoreBezel` calls are real engine calls under <<bezelref,Bezel>>; and
`getFValue`, `ratioGetX` and `ratioGetY` are real engine calls under `getFValue`, `ratioGetX` and `ratioGetY` are real engine calls under
<<commandlineref,Command Line>>; `joyMouseEnable` is a real engine call <<commandlineref,Command Line>>; `joyMouseEnable` is a real engine call
under <<joystick,Joystick>>; and the whole `music` family is now a set of real under <<joystick,Joystick>>; `allowSocketCall` and `getUserString` are real
engine calls under <<musicref,Music>>. The six `spriteAnim` names below are engine calls under <<singe,Singe>>, and `drawTransparent` under <<sprite,Sprite>>;
and the whole `music` family is now a set of real engine calls under
<<musicref,Music>>. The six `spriteAnim` names below are
plain aliases set after the legacy sprite wrapping, so they take whichever plain aliases set after the legacy sprite wrapping, so they take whichever
argument order the rest of the sprite family is taking: Hypseus's own (handle argument order the rest of the sprite family is taking: Hypseus's own (handle
last) under `LEGACY_SPRITE_ARGS`, and Singe 3.00's (handle first) without it. last) under `LEGACY_SPRITE_ARGS`, and Singe 3.00's (handle first) without it.

View file

@ -183,6 +183,14 @@ white, this is why.
=== The Trap: fontPrint Is Not Free === The Trap: fontPrint Is Not Free
NOTE: Rendering text to a sprite for performance is deprecated. Singe 3.00's
`fontPrint` keeps every string it renders and only copies it again while the
string, font, quality, and colours stay the same, so printing a title every
frame now costs about what drawing a sprite of it does. The trap below is the
one earlier versions set, and the pattern still works; new games can just
`fontPrint`, and keep `fontToSprite` for text that has to be a sprite -- to be
measured, scaled, or turned.
Here is the thing the reference says plainly and everyone reads past. Here is the thing the reference says plainly and everyone reads past.
`fontPrint` does not draw a picture it made earlier. Every single call `fontPrint` does not draw a picture it made earlier. Every single call
@ -475,9 +483,10 @@ thing, several hundred lines away from the `fontUnload` that caused it.
* Colour is not an argument. `colorForeground` is a setting; set it, then draw. * Colour is not an argument. `colorForeground` is a setting; set it, then draw.
* `fontQuality` picks how letters are rasterized, and `FONT_QUALITY_BLENDED` * `fontQuality` picks how letters are rasterized, and `FONT_QUALITY_BLENDED`
is the usual answer. is the usual answer.
* `fontPrint` re-renders the string on every single call. * `fontPrint` keeps what it renders, so printing the same text every frame is
* `fontToSprite` renders once into a sprite, which `spriteDraw` then copies cheap.
cheaply and `spriteGetWidth` measures. * `fontToSprite` renders into a sprite, which `spriteGetWidth` measures and the
`sprite*` calls can scale and turn. Using it only for speed is deprecated.
* Measuring is how you centre or right align text, because nothing else can * Measuring is how you centre or right align text, because nothing else can
tell you how wide a string will be. tell you how wide a string will be.
* A sprite's colour is fixed when it is made, not when it is drawn. * A sprite's colour is fixed when it is made, not when it is drawn.

View file

@ -5255,41 +5255,80 @@ end
-- ----- Timers, sounds, clips ---------------------------------------------------------------- -- ----- Timers, sounds, clips ----------------------------------------------------------------
-- The framework's two countdowns, the timer and the joystick delay, arithmetic and all: each sums
-- os.clock() deltas as (sum + now) - last and compares the sum. One that subtracted two times
-- instead lands a hair either side of the limit on the frame it is due, which is a frame late for
-- the rest of the game (FORGE.md 14.19). A paused game's countdown holds, as the framework's does.
-- keys names the countdown's three fields on q.
local TIMER_KEYS = { limit = "timerLimit", secs = "timerSecs", last = "timerLast" }
local JOY_KEYS = { limit = "joyLimit", secs = "joySecs", last = "joyLast" }
local function countdownStart(q, keys, seconds)
q[keys.limit] = seconds
q[keys.secs] = 0
q[keys.last] = os.clock()
end
local function countdownDue(q, keys)
if q[keys.limit] == nil then
return true
end
local now = os.clock()
if q.bPause then
q[keys.last] = now
return false
end
if now ~= q[keys.last] then
q[keys.secs] = q[keys.secs] + now - q[keys.last]
q[keys.last] = now
end
if q[keys.secs] >= q[keys.limit] then
q[keys.limit] = nil
return true
end
return false
end
function Q.timerON(q, seconds) function Q.timerON(q, seconds)
q.timerLimit = seconds countdownStart(q, TIMER_KEYS, seconds)
q.timerFrom = authorTime()
end end
function Q.timerDue(q) function Q.timerDue(q)
if q.timerLimit == nil then return countdownDue(q, TIMER_KEYS)
return true
end
if authorTime() - q.timerFrom >= q.timerLimit then
q.timerLimit = nil
return true
end
return false
end end
function Q.joyDelayON(q, seconds) function Q.joyDelayON(q, seconds)
q.joyLimit = seconds countdownStart(q, JOY_KEYS, seconds)
q.joyFrom = authorTime()
end end
function Q.joyDelayDue(q) function Q.joyDelayDue(q)
if q.joyLimit == nil then return countdownDue(q, JOY_KEYS)
return true end
end
if authorTime() - q.joyFrom >= q.joyLimit then
q.joyLimit = nil
return true
end
return false
-- The framework's setLang: the disc's audio in the language the dips name, or the default when
-- that file is missing. readConfig calls it only when the saved config has a dip_Lang line, which
-- is exactly when the description's snapshot carries one, so the loops call it on that condition.
-- Beyond the language, the call itself holds the disc for a frame, and the traces see it.
function Q.setLang(q)
if (q.LangOpt == nil) or (q.snapshot.dips == nil) or (q.snapshot.dips.dip_Lang == nil) then
return
end
if q.dip_Lang + 1 > #q.LangOpt then
q.dip_Lang = 0
end
if not discAudioSuffix(q.LangOpt[q.dip_Lang + 1][2]) then
debugPrint(q.LangOpt[q.dip_Lang + 1][1] .. " audio file unavailable, using default instead.")
discAudioSuffix("")
end
end end
@ -5311,13 +5350,19 @@ function Q.sound(q, name)
end end
-- The framework's 3.32b, the Hypseus library's copy, where it parts from 3.31c.
function Q.karis332(q)
return q.snapshot.framework == "3.32b"
end
-- The secret level's combination on the attract screens: the framework's 3.32b asks for -- The secret level's combination on the attract screens: the framework's 3.32b asks for
-- BUTTON4 and UP, the earlier versions for BUTTON2, BUTTON3, UP, and RIGHT. -- BUTTON4 and UP, the earlier versions for BUTTON2, BUTTON3, UP, and RIGHT.
function Q.secretPressed(q) function Q.secretPressed(q)
if not q.AllowSecret then if not q.AllowSecret then
return false return false
end end
if q.snapshot.framework == "3.32b" then if Q.karis332(q) then
return q.p1BUTTON4 and q.p1UP return q.p1BUTTON4 and q.p1UP
end end
@ -7205,7 +7250,9 @@ function Q.doGameOver(q)
if q.bGOAlt then if q.bGOAlt then
Q.setupClip(q, q.offsetGameOverAlt, q.offsetGameOverAltend) Q.setupClip(q, q.offsetGameOverAlt, q.offsetGameOverAltend)
q.bGOAlt = false q.bGOAlt = false
elseif q.currentFrame == q.offsetContinueend then elseif (not Q.karis332(q)) and (q.currentFrame == q.offsetContinueend) then
-- 3.31c plays on from the continue clip when the game over follows it on the disc;
-- 3.32b always seeks.
q.iFrameEnd = q.offsetGameOverend q.iFrameEnd = q.offsetGameOverend
else else
Q.setupClip(q, q.offsetGameOver, q.offsetGameOverend) Q.setupClip(q, q.offsetGameOver, q.offsetGameOverend)
@ -7384,7 +7431,7 @@ function Q.loadSprite(q, name, frames)
if probe ~= nil then if probe ~= nil then
probe:close() probe:close()
if frames then if frames then
handle = spriteLoadFrames(path, frames) handle = spriteLoadFrames(frames, path)
else else
handle = spriteLoad(path) handle = spriteLoad(path)
end end
@ -7606,7 +7653,6 @@ function Q.shim(q)
videoSetVolume = function() end, videoSetVolume = function() end,
videoDraw = function() end, videoDraw = function() end,
overlayClear = function() end, overlayClear = function() end,
colorForeground = function() end,
autoSave = function() end, autoSave = function() end,
writeSave = function() end, writeSave = function() end,
loadSave = function() end, loadSave = function() end,
@ -7614,7 +7660,13 @@ function Q.shim(q)
cfgWritePath = function(name) return singeGetDataPath() .. "Cfg/" .. name end, cfgWritePath = function(name) return singeGetDataPath() .. "Cfg/" .. name end,
singeGetScriptPath = function() return q.MYDIR .. "/" .. (q.snapshot.script or "game.singe") end, singeGetScriptPath = function() return q.MYDIR .. "/" .. (q.snapshot.script or "game.singe") end,
dofile = function(name) dofile = function(name)
-- A map-mode game's files are its own data and helpers: loaded into the shim. -- A map-mode game's files are its own data and helpers: loaded into the shim. The
-- engine's own Framework.singe is not: the shim stands in for it, and loading it here
-- would wrap the shim's sprite calls a second time for a game that asks for the old
-- argument order, turning its y into a sprite handle.
if (type(name) == "string") and name:lower():match("^singe[/\\]framework%.singe$") then
return
end
if q.loadsFiles then if q.loadsFiles then
Q.runScript(q, Q.gameFile(q, name)) Q.runScript(q, Q.gameFile(q, name))
end end
@ -7804,6 +7856,12 @@ AUTHOR.behaviours.qte = {
step = function(instance) step = function(instance)
local q = instance.q local q = instance.q
Q.dropDraws(q)
-- What the game's own Lua queued to draw last frame: this loop has no look that draws it
-- (FORGE.md 15.4), and a queue nothing empties grows by every call, every frame.
Q.dropDraws(q)
q.currentFrame = discGetFrame() q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then if q.gameflow == "vldp" then
-- The framework's start: the title clip once through, a frame of init, the attract loop. -- The framework's start: the title clip once through, a frame of init, the attract loop.
@ -7822,6 +7880,7 @@ AUTHOR.behaviours.qte = {
if q.gameflow == "init" then if q.gameflow == "init" then
q.gameflow = "running" q.gameflow = "running"
q.lvlState = QTE.lvlSetup q.lvlState = QTE.lvlSetup
Q.setLang(q) -- initJob's readConfig
return return
end end
if q.currentLevel == QTE.levelIntro then if q.currentLevel == QTE.levelIntro then
@ -10587,6 +10646,8 @@ AUTHOR.behaviours.kimmy = {
step = function(instance) step = function(instance)
local q = instance.q local q = instance.q
Q.dropDraws(q)
q.currentFrame = discGetFrame() q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then if q.gameflow == "vldp" then
if q.lvlState == QTE.lvlSetup then if q.lvlState == QTE.lvlSetup then
@ -10607,6 +10668,7 @@ AUTHOR.behaviours.kimmy = {
q.lvlState = QTE.lvlSetup q.lvlState = QTE.lvlSetup
q.iCoins, q.iScore, q.iScoreTemp, q.iBonus, q.iScene = 0, 0, 0, 0, 0 q.iCoins, q.iScore, q.iScoreTemp, q.iBonus, q.iScene = 0, 0, 0, 0, 0
q.bShowCredits = true q.bShowCredits = true
Q.setLang(q) -- initJob's readConfig
return return
end end
local level = q.currentLevel local level = q.currentLevel
@ -11733,6 +11795,8 @@ AUTHOR.behaviours.rdg = {
step = function(instance) step = function(instance)
local q = instance.q local q = instance.q
Q.dropDraws(q)
q.currentFrame = discGetFrame() q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then if q.gameflow == "vldp" then
-- The ancestor's start: the title clip with the inputs paused, then the init. -- The ancestor's start: the title clip with the inputs paused, then the init.
@ -12532,6 +12596,8 @@ AUTHOR.behaviours.timegal = {
step = function(instance) step = function(instance)
local q = instance.q local q = instance.q
Q.dropDraws(q)
q.currentFrame = discGetFrame() q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then if q.gameflow == "vldp" then
-- The ancestor's start: the title clip, then the init. -- The ancestor's start: the title clip, then the init.
@ -13664,6 +13730,8 @@ AUTHOR.behaviours.sdq = {
step = function(instance) step = function(instance)
local q = instance.q local q = instance.q
Q.dropDraws(q)
q.currentFrame = discGetFrame() q.currentFrame = discGetFrame()
if q.gameflow == "vldp" then if q.gameflow == "vldp" then
if q.lvlState == q.lvlSetup then if q.lvlState == q.lvlSetup then

View file

@ -4162,8 +4162,6 @@ function forgeKey(keysym, scancode)
forgeBuild(singeGetDataPath() .. "preview.singe") forgeBuild(singeGetDataPath() .. "preview.singe")
elseif scancode == SCANCODE.X.value then elseif scancode == SCANCODE.X.value then
forgeExport(forgeReleaseFolder()) forgeExport(forgeReleaseFolder())
elseif scancode == SCANCODE.P.value then
forgePublish()
elseif scancode == SCANCODE.U.value then elseif scancode == SCANCODE.U.value then
forgeUndo() forgeUndo()
elseif scancode == SCANCODE.R.value then elseif scancode == SCANCODE.R.value then
@ -5094,7 +5092,12 @@ if not FORGE_LIBRARY then
elseif (scancode == SCANCODE.ESCAPE.value) and (FORGE.editing == nil) and (FORGE.picker == nil) then elseif (scancode == SCANCODE.ESCAPE.value) and (FORGE.editing == nil) and (FORGE.picker == nil) then
editorEscape() editorEscape()
elseif (scancode == SCANCODE.P.value) and (FORGE.editing == nil) and (FORGE.picker == nil) then elseif (scancode == SCANCODE.P.value) and (FORGE.editing == nil) and (FORGE.picker == nil) then
forgePlay() -- P plays; Shift+P publishes, one step past X's export.
if (keyboardGetModifiers() & MODIFIER.SHIFT.value) ~= 0 then
forgePublish()
else
forgePlay()
end
else else
forgeKey(keysym, scancode) forgeKey(keysym, scancode)
end end

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@
], ],
"head": ["min", "mid", "mid"], "head": ["min", "mid", "mid"],
"jaw": ["min", "max", "mid"],
"neck": ["mid", "min", "mid"], "neck": ["mid", "min", "mid"],
"tail": ["max", "mid", "mid"], "tail": ["max", "mid", "mid"],

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
media/music/eliza_music-neons-in-napoli-539381.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -66,6 +66,9 @@ typedef struct GuiContextS {
int32_t height; int32_t height;
bool inputEnabled; bool inputEnabled;
bool used; bool used;
bool mustDraw; // Draw next frame whatever the signature says: new, or guiInvalidate
uint64_t signature; // The last frame drawn, as GuiRenderT signs it
uint64_t generation; // What the render interface had made by then
char name[GUI_NAME_MAX]; char name[GUI_NAME_MAX];
} GuiContextT; } GuiContextT;
@ -470,24 +473,23 @@ static void _releaseContext(GuiContextT *context) {
} }
// Clears the context's texture to transparent black, then lets RmlUi draw the context over it. // Lets RmlUi draw the context into its texture, from transparent black. RmlUi describes the whole
// picture every frame, but a HUD or a menu is the same picture most of them: when this frame's
// drawing signs the same as the last one drawn and nothing new was made since, the texture already
// holds it and the GPU is spared the work. Anything that changes the picture behind RmlUi's back
// calls guiInvalidate.
static void _renderContext(GuiContextT *context, SDL_GPUCommandBuffer *commands) { static void _renderContext(GuiContextT *context, SDL_GPUCommandBuffer *commands) {
SDL_GPUColorTargetInfo target = {}; bool draw = false;
SDL_GPURenderPass *pass = nullptr;
target.texture = context->target;
target.load_op = SDL_GPU_LOADOP_CLEAR;
target.store_op = SDL_GPU_STOREOP_STORE;
target.clear_color = { 0.0f, 0.0f, 0.0f, 0.0f };
pass = rgpuBeginRenderPass(commands, &target, 1, nullptr);
if (pass == nullptr) {
utilTrace("Gui: %s", SDL_GetError());
return;
}
rgpuEndRenderPass(pass);
_render->BeginFrame(commands, context->target, context->stencil, (uint32_t)context->width, (uint32_t)context->height); _render->BeginFrame(commands, context->target, context->stencil, (uint32_t)context->width, (uint32_t)context->height);
context->context->Render(); context->context->Render();
_render->EndFrame(); draw = context->mustDraw || (_render->GetSignature() != context->signature) || (_render->GetGeneration() != context->generation);
if (draw) {
context->signature = _render->GetSignature();
context->generation = _render->GetGeneration();
context->mustDraw = false;
}
_render->EndFrame(draw);
} }
@ -697,6 +699,20 @@ bool guiInit(SDL_GPUDevice *device, SDL_Renderer *renderer, SDL_Window *window)
} }
// The GUI draws next frame even if RmlUi describes the same picture: for anything that changes what
// it shows without RmlUi knowing. False for a GUI that does not exist.
bool guiInvalidate(int32_t gui) {
GuiContextT *context = _contextOf(gui);
if (context == nullptr) {
return false;
}
context->mustDraw = true;
return true;
}
bool guiKey(int32_t gui, SDL_Keycode key, SDL_Keymod modifiers, bool down) { bool guiKey(int32_t gui, SDL_Keycode key, SDL_Keymod modifiers, bool down) {
GuiContextT *context = _inputContextOf(gui); GuiContextT *context = _inputContextOf(gui);
Rml::Input::KeyIdentifier identifier = _keyOf(key); Rml::Input::KeyIdentifier identifier = _keyOf(key);
@ -870,6 +886,7 @@ int32_t guiNew(int32_t width, int32_t height) {
context->width = width; context->width = width;
context->height = height; context->height = height;
context->inputEnabled = false; context->inputEnabled = false;
context->mustDraw = true;
info.type = SDL_GPU_TEXTURETYPE_2D; info.type = SDL_GPU_TEXTURETYPE_2D;
info.format = _targetFormat; info.format = _targetFormat;
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;

View file

@ -73,6 +73,7 @@ bool guiDelete(int32_t gui);
int32_t guiGetHeight(int32_t gui); int32_t guiGetHeight(int32_t gui);
void guiGetStats(GuiStatsT *stats); // The last completed frame's rendering counters void guiGetStats(GuiStatsT *stats); // The last completed frame's rendering counters
int32_t guiGetWidth(int32_t gui); int32_t guiGetWidth(int32_t gui);
bool guiInvalidate(int32_t gui); // Draw next frame even when RmlUi describes the same picture
int32_t guiNew(int32_t width, int32_t height); // GUI_NO_HANDLE when full or without a GPU int32_t guiNew(int32_t width, int32_t height); // GUI_NO_HANDLE when full or without a GPU
SDL_GPUTexture *guiTexture(int32_t gui); // For materials; NULL when invalid SDL_GPUTexture *guiTexture(int32_t gui); // For materials; NULL when invalid
SDL_Texture *guiTexture2D(int32_t gui); // The same as the 2D renderer sees it (premultiplied alpha) SDL_Texture *guiTexture2D(int32_t gui); // The same as the 2D renderer sees it (premultiplied alpha)

View file

@ -93,6 +93,8 @@ extern "C" {
#define MAX_LAYERS 8 // Layers stacked over a context's target at once #define MAX_LAYERS 8 // Layers stacked over a context's target at once
#define FILTER_BUFFERS 2 // The ping-pong pair a filter chain runs through #define FILTER_BUFFERS 2 // The ping-pong pair a filter chain runs through
#define BLUR_WEIGHTS 4 // The centre tap and the three beyond it each side (BLUR_TAPS = 7, in gui.hlsl) #define BLUR_WEIGHTS 4 // The centre tap and the three beyond it each side (BLUR_TAPS = 7, in gui.hlsl)
#define FNV_OFFSET 14695981039346656037ULL // FNV-1a, which the frame's signature is
#define FNV_PRIME 1099511628211ULL
#define BLUR_MAX_LEVELS 10 // Halvings before a blur, at most #define BLUR_MAX_LEVELS 10 // Halvings before a blur, at most
#define BLUR_MAX_SIGMA 3.0f // The widest sigma one pass of BLUR_TAPS taps handles #define BLUR_MAX_SIGMA 3.0f // The widest sigma one pass of BLUR_TAPS taps handles
#define BLUR_MIN_SIGMA 0.1f // Below this the blur is the identity #define BLUR_MIN_SIGMA 0.1f // Below this the blur is the identity
@ -104,6 +106,23 @@ extern "C" {
#define LUMA_BLUE 0.0722f #define LUMA_BLUE 0.0722f
#define TWO_PI_SQRT 2.5066283f #define TWO_PI_SQRT 2.5066283f
// What each drawing command adds to the frame's signature ahead of its values, so two different
// commands with the same values never sign alike.
enum GuiCommandE : uint8_t {
COMMAND_CLIP_MASK,
COMMAND_COMPOSITE,
COMMAND_GEOMETRY,
COMMAND_POP_LAYER,
COMMAND_PUSH_LAYER,
COMMAND_SAVE_MASK,
COMMAND_SAVE_TEXTURE,
COMMAND_SCISSOR,
COMMAND_SCISSOR_ENABLE,
COMMAND_SHADER,
COMMAND_TO_CLIP_MASK,
COMMAND_TRANSFORM
};
// The fragment shaders, as the pipeline table names them. // The fragment shaders, as the pipeline table names them.
typedef enum GuiFragmentE { typedef enum GuiFragmentE {
@ -464,6 +483,8 @@ void GuiRenderT::BeginFrame(SDL_GPUCommandBuffer *frameCommands, SDL_GPUTexture
recordDepth = 0; recordDepth = 0;
refusedDepth = 0; refusedDepth = 0;
recordScissor = { 0, 0, (int)width, (int)height }; recordScissor = { 0, 0, (int)width, (int)height };
signature = FNV_OFFSET;
signatureBytes = 0;
proj = Rml::Matrix4f::ProjectOrtho(0.0f, (float)width, (float)height, 0.0f, ORTHO_NEAR, ORTHO_FAR); proj = Rml::Matrix4f::ProjectOrtho(0.0f, (float)width, (float)height, 0.0f, ORTHO_NEAR, ORTHO_FAR);
if (screenQuad == nullptr) { if (screenQuad == nullptr) {
screenQuad = uploadGeometry(_screenQuadVertices, sizeof(_screenQuadVertices), _screenQuadIndices, sizeof(_screenQuadIndices)); screenQuad = uploadGeometry(_screenQuadVertices, sizeof(_screenQuadVertices), _screenQuadIndices, sizeof(_screenQuadIndices));
@ -486,6 +507,7 @@ Rml::CompiledFilterHandle GuiRenderT::CompileFilter(const Rml::String &name, con
Rml::Vector3f mix; Rml::Vector3f mix;
Rml::Vector3f green; Rml::Vector3f green;
Rml::Vector3f blue; Rml::Vector3f blue;
generation++;
filter->type = GUI_FILTER_OPACITY; filter->type = GUI_FILTER_OPACITY;
filter->factor = 1.0f; filter->factor = 1.0f;
@ -560,6 +582,7 @@ Rml::CompiledFilterHandle GuiRenderT::CompileFilter(const Rml::String &name, con
Rml::CompiledGeometryHandle GuiRenderT::CompileGeometry(Rml::Span<const Rml::Vertex> vertices, Rml::Span<const int> indices) { Rml::CompiledGeometryHandle GuiRenderT::CompileGeometry(Rml::Span<const Rml::Vertex> vertices, Rml::Span<const int> indices) {
GeometryT *geometry = uploadGeometry(vertices.data(), (uint32_t)(vertices.size() * sizeof(Rml::Vertex)), indices.data(), (uint32_t)(indices.size() * sizeof(int))); GeometryT *geometry = uploadGeometry(vertices.data(), (uint32_t)(vertices.size() * sizeof(Rml::Vertex)), indices.data(), (uint32_t)(indices.size() * sizeof(int)));
generation++;
return reinterpret_cast<Rml::CompiledGeometryHandle>(geometry); return reinterpret_cast<Rml::CompiledGeometryHandle>(geometry);
} }
@ -578,6 +601,7 @@ Rml::CompiledShaderHandle GuiRenderT::CompileShader(const Rml::String &name, con
float angle = 0.0f; float angle = 0.0f;
int32_t kind = GUI_GRADIENT_LINEAR; int32_t kind = GUI_GRADIENT_LINEAR;
int32_t i = 0; int32_t i = 0;
generation++;
if (name == "linear-gradient") { if (name == "linear-gradient") {
kind = GUI_GRADIENT_LINEAR; kind = GUI_GRADIENT_LINEAR;
@ -628,11 +652,20 @@ Rml::CompiledShaderHandle GuiRenderT::CompileShader(const Rml::String &name, con
void GuiRenderT::CompositeLayers(Rml::LayerHandle source, Rml::LayerHandle destination, Rml::BlendMode blendMode, Rml::Span<const Rml::CompiledFilterHandle> filters) { void GuiRenderT::CompositeLayers(Rml::LayerHandle source, Rml::LayerHandle destination, Rml::BlendMode blendMode, Rml::Span<const Rml::CompiledFilterHandle> filters) {
sign(COMMAND_COMPOSITE);
sign(source);
sign(destination);
sign(blendMode);
for (Rml::CompiledFilterHandle filter : filters) {
sign(filter);
}
commands.push_back(Rml::MakeUnique<CompositeLayersCommandT>(source, destination, blendMode, filters)); commands.push_back(Rml::MakeUnique<CompositeLayersCommandT>(source, destination, blendMode, filters));
} }
void GuiRenderT::EnableClipMask(bool enable) { void GuiRenderT::EnableClipMask(bool enable) {
sign(COMMAND_CLIP_MASK);
sign(enable);
commands.push_back(Rml::MakeUnique<EnableClipMaskCommandT>(enable)); commands.push_back(Rml::MakeUnique<EnableClipMaskCommandT>(enable));
} }
@ -641,13 +674,26 @@ void GuiRenderT::EnableScissorRegion(bool enable) {
if (!enable) { if (!enable) {
recordScissor = { 0, 0, (int)targetWidth, (int)targetHeight }; recordScissor = { 0, 0, (int)targetWidth, (int)targetHeight };
} }
sign(COMMAND_SCISSOR_ENABLE);
sign(enable);
commands.push_back(Rml::MakeUnique<EnableScissorRegionCommandT>(enable)); commands.push_back(Rml::MakeUnique<EnableScissorRegionCommandT>(enable));
} }
void GuiRenderT::EndFrame() { void GuiRenderT::EndFrame(bool draw) {
// Drawing starts from a cleared target, in a pass of its own that also clears the stencil.
if (draw) {
target = baseTarget;
targetClear = true;
if (beginRenderPass()) {
rgpuEndRenderPass(renderPass);
renderPass = nullptr;
}
}
for (Rml::UniquePtr<CommandT> &command : commands) { for (Rml::UniquePtr<CommandT> &command : commands) {
command->Run(*this); if (draw || command->Releases()) {
command->Run(*this);
}
} }
commands.clear(); commands.clear();
if (copyPass != nullptr) { if (copyPass != nullptr) {
@ -678,6 +724,7 @@ Rml::TextureHandle GuiRenderT::GenerateTexture(Rml::Span<const Rml::byte> source
SDL_GPUCopyPass *pass = nullptr; SDL_GPUCopyPass *pass = nullptr;
uint32_t size = (uint32_t)(sourceDimensions.x * sourceDimensions.y * BYTES_PER_PIXEL); uint32_t size = (uint32_t)(sourceDimensions.x * sourceDimensions.y * BYTES_PER_PIXEL);
void *destination = nullptr; void *destination = nullptr;
generation++;
transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
transferInfo.size = size; transferInfo.size = size;
@ -745,6 +792,18 @@ void GuiRenderT::GetStats(GuiStatsT *outStats) const {
} }
uint64_t GuiRenderT::GetGeneration() const {
return generation;
}
// The byte count goes in too, so a frame that is the last one's with something left off the end
// still signs differently.
uint64_t GuiRenderT::GetSignature() const {
return signature ^ (signatureBytes * FNV_PRIME);
}
GuiRenderT::GuiRenderT(SDL_GPUDevice *gpuDevice, SDL_Window *gpuWindow) { GuiRenderT::GuiRenderT(SDL_GPUDevice *gpuDevice, SDL_Window *gpuWindow) {
SDL_GPUSamplerCreateInfo info = {}; SDL_GPUSamplerCreateInfo info = {};
size_t i = 0; size_t i = 0;
@ -828,6 +887,7 @@ Rml::TextureHandle GuiRenderT::LoadTexture(Rml::Vector2i &textureDimensions, con
size_t dot = source.rfind('.'); size_t dot = source.rfind('.');
Rml::String extension = (dot == Rml::String::npos) ? Rml::String() : source.substr(dot + 1); Rml::String extension = (dot == Rml::String::npos) ? Rml::String() : source.substr(dot + 1);
Rml::TextureHandle handle = 0; Rml::TextureHandle handle = 0;
generation++;
if (!file) { if (!file) {
return 0; return 0;
@ -897,6 +957,7 @@ void GuiRenderT::PopLayer() {
if (recordDepth > 0) { if (recordDepth > 0) {
recordDepth--; recordDepth--;
} }
sign(COMMAND_POP_LAYER);
commands.push_back(Rml::MakeUnique<PopLayerCommandT>()); commands.push_back(Rml::MakeUnique<PopLayerCommandT>());
} }
@ -913,6 +974,7 @@ Rml::LayerHandle GuiRenderT::PushLayer() {
return (Rml::LayerHandle)recordDepth; return (Rml::LayerHandle)recordDepth;
} }
recordDepth++; recordDepth++;
sign(COMMAND_PUSH_LAYER);
commands.push_back(Rml::MakeUnique<PushLayerCommandT>()); commands.push_back(Rml::MakeUnique<PushLayerCommandT>());
return (Rml::LayerHandle)recordDepth; return (Rml::LayerHandle)recordDepth;
} }
@ -963,6 +1025,10 @@ void GuiRenderT::ReleaseTexture(Rml::TextureHandle texture) {
void GuiRenderT::RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture) { void GuiRenderT::RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture) {
sign(COMMAND_GEOMETRY);
sign(geometry);
sign(translation);
sign(texture);
commands.push_back(Rml::MakeUnique<RenderGeometryCommandT>(geometry, translation, texture)); commands.push_back(Rml::MakeUnique<RenderGeometryCommandT>(geometry, translation, texture));
} }
@ -973,11 +1039,19 @@ void GuiRenderT::RenderShader(Rml::CompiledShaderHandle shader, Rml::CompiledGeo
if ((shader == 0) || (geometry == 0)) { if ((shader == 0) || (geometry == 0)) {
return; return;
} }
sign(COMMAND_SHADER);
sign(shader);
sign(geometry);
sign(translation);
commands.push_back(Rml::MakeUnique<RenderShaderCommandT>(reinterpret_cast<ShaderT *>(shader), geometry, translation)); commands.push_back(Rml::MakeUnique<RenderShaderCommandT>(reinterpret_cast<ShaderT *>(shader), geometry, translation));
} }
void GuiRenderT::RenderToClipMask(Rml::ClipMaskOperation operation, Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation) { void GuiRenderT::RenderToClipMask(Rml::ClipMaskOperation operation, Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation) {
sign(COMMAND_TO_CLIP_MASK);
sign(operation);
sign(geometry);
sign(translation);
commands.push_back(Rml::MakeUnique<RenderToClipMaskCommandT>(operation, geometry, translation)); commands.push_back(Rml::MakeUnique<RenderToClipMaskCommandT>(operation, geometry, translation));
} }
@ -997,6 +1071,9 @@ Rml::CompiledFilterHandle GuiRenderT::SaveLayerAsMaskImage() {
filter->matrix = Rml::Matrix4f::Identity(); filter->matrix = Rml::Matrix4f::Identity();
filter->mask = nullptr; filter->mask = nullptr;
memset(filter->colour, 0, sizeof(filter->colour)); memset(filter->colour, 0, sizeof(filter->colour));
generation++;
sign(COMMAND_SAVE_MASK);
sign(filter);
commands.push_back(Rml::MakeUnique<SaveLayerAsMaskImageCommandT>(filter)); commands.push_back(Rml::MakeUnique<SaveLayerAsMaskImageCommandT>(filter));
return reinterpret_cast<Rml::CompiledFilterHandle>(filter); return reinterpret_cast<Rml::CompiledFilterHandle>(filter);
} }
@ -1024,6 +1101,10 @@ Rml::TextureHandle GuiRenderT::SaveLayerAsTexture() {
Rml::Log::Message(Rml::Log::LT_ERROR, "failed to create layer texture: %s", SDL_GetError()); Rml::Log::Message(Rml::Log::LT_ERROR, "failed to create layer texture: %s", SDL_GetError());
return 0; return 0;
} }
generation++;
sign(COMMAND_SAVE_TEXTURE);
sign(texture);
sign(region);
commands.push_back(Rml::MakeUnique<SaveLayerAsTextureCommandT>(texture, region)); commands.push_back(Rml::MakeUnique<SaveLayerAsTextureCommandT>(texture, region));
return reinterpret_cast<Rml::TextureHandle>(texture); return reinterpret_cast<Rml::TextureHandle>(texture);
} }
@ -1034,11 +1115,17 @@ void GuiRenderT::SetScissorRegion(Rml::Rectanglei region) {
recordScissor.y = region.Top(); recordScissor.y = region.Top();
recordScissor.w = region.Width(); recordScissor.w = region.Width();
recordScissor.h = region.Height(); recordScissor.h = region.Height();
sign(COMMAND_SCISSOR);
sign(region);
commands.push_back(Rml::MakeUnique<SetScissorRegionCommandT>(region)); commands.push_back(Rml::MakeUnique<SetScissorRegionCommandT>(region));
} }
void GuiRenderT::SetTransform(const Rml::Matrix4f *newTransform) { void GuiRenderT::SetTransform(const Rml::Matrix4f *newTransform) {
sign(COMMAND_TRANSFORM);
if (newTransform != nullptr) {
sign(*newTransform);
}
commands.push_back(Rml::MakeUnique<SetTransformCommandT>(newTransform)); commands.push_back(Rml::MakeUnique<SetTransformCommandT>(newTransform));
} }
@ -1555,6 +1642,18 @@ void GuiRenderT::setTarget(SDL_GPUTexture *texture, bool clear) {
} }
// Adds bytes to the frame's signature (FNV-1a).
void GuiRenderT::sign(const void *bytes, size_t count) {
const uint8_t *in = (const uint8_t *)bytes;
size_t x = 0;
for (x = 0; x < count; x++) {
signature = (signature ^ in[x]) * FNV_PRIME;
}
signatureBytes += count;
}
// Vertices and indices into pooled buffers through the frame's copy pass. // Vertices and indices into pooled buffers through the frame's copy pass.
GuiRenderT::GeometryT *GuiRenderT::uploadGeometry(const void *vertexData, uint32_t vertexSize, const void *indexData, uint32_t indexSize) { GuiRenderT::GeometryT *GuiRenderT::uploadGeometry(const void *vertexData, uint32_t vertexSize, const void *indexData, uint32_t indexSize) {
SDL_GPUTransferBufferLocation location = {}; SDL_GPUTransferBufferLocation location = {};
@ -1587,6 +1686,9 @@ GuiRenderT::GeometryT *GuiRenderT::uploadGeometry(const void *vertexData, uint32
delete geometry; delete geometry;
return nullptr; return nullptr;
} }
// Cycled: a pooled buffer came back from geometry that is gone, so nothing in it is wanted, while
// the last frame's draws may still be reading it. Writing over it in place makes a tiler wait for
// them or copy the buffer first; cycling hands the upload fresh storage instead.
memcpy(vertexStage, vertexData, vertexSize); memcpy(vertexStage, vertexData, vertexSize);
memcpy(indexStage, indexData, indexSize); memcpy(indexStage, indexData, indexSize);
rgpuUnmapTransferBuffer(device, geometry->vertices->transfer); rgpuUnmapTransferBuffer(device, geometry->vertices->transfer);
@ -1594,11 +1696,11 @@ GuiRenderT::GeometryT *GuiRenderT::uploadGeometry(const void *vertexData, uint32
location.transfer_buffer = geometry->vertices->transfer; location.transfer_buffer = geometry->vertices->transfer;
region.buffer = geometry->vertices->buffer; region.buffer = geometry->vertices->buffer;
region.size = vertexSize; region.size = vertexSize;
rgpuUploadToBuffer(copyPass, &location, &region, false); rgpuUploadToBuffer(copyPass, &location, &region, true);
location.transfer_buffer = geometry->indices->transfer; location.transfer_buffer = geometry->indices->transfer;
region.buffer = geometry->indices->buffer; region.buffer = geometry->indices->buffer;
region.size = indexSize; region.size = indexSize;
rgpuUploadToBuffer(copyPass, &location, &region, false); rgpuUploadToBuffer(copyPass, &location, &region, true);
geometry->indexCount = (int32_t)(indexSize / sizeof(int32_t)); geometry->indexCount = (int32_t)(indexSize / sizeof(int32_t));
geometry->vertices->inUse = true; geometry->vertices->inUse = true;
geometry->indices->inUse = true; geometry->indices->inUse = true;

Some files were not shown because too many files have changed in this diff Show more