Many memory related bugs fixed. Some render optimizations.

This commit is contained in:
Scott Duensing 2026-09-07 00:42:34 -05:00
parent 3064a099cb
commit 259c1d5d55
32 changed files with 4729 additions and 3558 deletions

View file

@ -326,6 +326,30 @@ API Changes
read the measured device queue with singeGetAudioLatency(), and now
have singeGetTicks() for a wall clock in milliseconds (os.clock() is
processor time and drifts).
- Inside a packed game a relative name with a ".." component resolves
nowhere: io.open returns nil and "NAME reaches outside the game",
io.lines, io.input and io.output raise it, lfs.mkdir and lfs.rmdir
return nil with it, and asset loads report the file as missing. The
packer already refused such names; the lookup now enforces the rule at
run time. lfs.dir on a packed directory lists its entries sorted
without regard to case.
- One rule for the data directory on every launch (command line,
games.dat, scriptPush and scriptExecute): with --datadir, or for a
packed game, a directory named for the game under the base; otherwise
the game's own directory. scriptPush of a loose game without --datadir
used to write under the current directory.
- Color components handed to lightSetColor, materialSetColor (alpha
too), materialSetEmissive, lineDraw and navDraw are clamped to 0..255,
as colorForeground and colorBackground always were; they used to wrap.
- materialDelete on the private material of a sprite or text node is an
error ("belongs to a sprite node; clear the sprite instead") instead
of freeing a slot the node still used.
- lightSetCone raises the outer angle to at least the inner angle.
Fixes
@ -440,6 +464,69 @@ Fixes
64-bit Raspberry Pi OS (bullseye or newer) binary and a macOS 13 or
newer universal (Apple silicon and Intel) binary. The toolchains repository is no longer
needed. See INSTALL.
- On a plain video disc, discSearch, discSkipToFrame, discSkipForward,
discSkipBackward, discStepForward and discStepBackward clamp at the
first and last frame instead of wrapping around the disc.
discStepForward and discStepBackward are ignored while the disc is
stopped, like the skips; they used to move the frame behind the blue
screen. On a framefile the skips apply to the frame number
discGetFrame reports, so a skip in any segment after the first lands
where it should.
- Every collision and trigger event of a frame reaches onCollision and
onTrigger; only the first 64 used to, the rest were dropped.
onTrigger fires once per body and trigger pair even when a mesh or
compound shape touches in several places. playerJump is granted
whenever the player is on the ground.
- playerSetEnabled(node, false) removes the player's body from the
world and reports it leaving every trigger it stood in; enabling puts
it back. navPath ends a partial path at the last reachable point.
navLoad rejects data that is not a Detour mesh. The world holds at
most 4096 bodies; bodyNew raises an error when it is full.
- modelLoad refuses a skin with more than 128 joints or a skinned mesh
naming a joint its skin lacks (the shader used to read past the
matrices). 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, so Blender shape keys are no longer silently
zero. Alpha MASK materials, textures on TEXCOORD_1 and up, and
KHR_texture_transform are traced as unsupported.
- Video index files are named <name>-<hash>.index, the hash from the
video's full name, so two videos with one base name in a game stop
rebuilding each other's index. Old <name>.index files are ignored and
can be deleted.
- The packer reports every forbidden file in a directory rather than
stopping at the first. --unpack refuses a database whose stored names
would land outside the target directory. Launching a .game database
from the command line no longer crashes.
- --stretch hands the script mouse positions in the game's coordinates
at any window size. --scalefactor with --sindengun shrinks the picture
inside the Sinden border instead of being cancelled by it. A negative
--audio track number is ignored. With no --datadir the controls.cfg
beside the script is read once, not twice.
- A reload (F5, --reload, singeReload) ignores keys and buttons held
across it until they are released, resets the effect channels and their
pending onSoundCompleted queue, the held switches and the 2D particle
textures, 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.
- Error messages: an invalid view, navigation mesh or navigation agent
handle reports "No <thing> N." like every other handle; navAgentNew,
vehicleSetGears and viewNew state their limits from the engine's
constants; fontLoad and soundLoad of a missing file say "Unable to open
NAME" instead of the library's "invalid parameter"; navDraw reports its
argument count, and running out of memory in it is an error rather than
a silent blank; scriptExecute and scriptPush that cannot create the new
data directory abort the script with a message instead of exiting.
materialNew, sceneGetSize and sceneGetStats no longer reject extra
arguments.
SINGE 2.10

View file

@ -658,7 +658,7 @@ set(shaderHeader ${CMAKE_BINARY_DIR}/generated/shaders/sceneShaders.h)
add_custom_command(
OUTPUT ${shaderHeader}
COMMAND ${CMAKE_COMMAND} -DSHADERCROSS=${SINGE_SHADERCROSS} -DSOURCE=${CMAKE_SOURCE_DIR}/src/shaders/scene.hlsl -DOUTPUT=${shaderHeader} -P ${CMAKE_SOURCE_DIR}/cmake/shaderHeader.cmake
DEPENDS ${CMAKE_SOURCE_DIR}/src/shaders/scene.hlsl ${CMAKE_SOURCE_DIR}/cmake/shaderHeader.cmake
DEPENDS ${CMAKE_SOURCE_DIR}/src/shaders/scene.hlsl ${CMAKE_SOURCE_DIR}/src/sceneShared.h ${CMAKE_SOURCE_DIR}/cmake/shaderHeader.cmake
COMMENT "Compiling the scene shaders"
)
target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${shaderHeader})

View file

@ -1,4 +1,4 @@
# Compiles src/shaders/scene.hlsl into a C header of SPIR-V, DXIL and MSL blobs with
# Compiles src/shaders/scene.hlsl (which includes src/sceneShared.h) into a C header of SPIR-V, DXIL and MSL blobs with
# SDL_shadercross. Run by the build as
# cmake -DSHADERCROSS=<tool> -DSOURCE=<scene.hlsl> -DOUTPUT=<sceneShaders.h> -P shaderHeader.cmake
# so the header is generated into the build tree like the icon and the other embedded files.
@ -6,6 +6,7 @@
set(entries vertexStatic:vertex vertexSkinned:vertex fragmentMain:fragment depthMain:fragment particleVertex:vertex particleFragment:fragment lineVertex:vertex lineFragment:fragment postVertex:vertex postFragment:fragment skyFragment:fragment bloomDown:fragment bloomUp:fragment)
set(formats SPIRV DXIL MSL)
get_filename_component(sourceDir ${SOURCE} DIRECTORY)
get_filename_component(includeDir ${sourceDir} DIRECTORY)
get_filename_component(outputDir ${OUTPUT} DIRECTORY)
set(work ${outputDir}/work)
file(MAKE_DIRECTORY ${work})
@ -21,7 +22,7 @@ foreach(entry IN LISTS entries)
foreach(format IN LISTS formats)
string(TOLOWER ${format} extension)
set(blob ${work}/${name}.${extension})
execute_process(COMMAND ${SHADERCROSS} ${SOURCE} -s HLSL -d ${format} -t ${stage} -e ${name} -o ${blob} RESULT_VARIABLE code OUTPUT_VARIABLE out ERROR_VARIABLE err)
execute_process(COMMAND ${SHADERCROSS} ${SOURCE} -s HLSL -d ${format} -t ${stage} -e ${name} -I ${includeDir} -o ${blob} RESULT_VARIABLE code OUTPUT_VARIABLE out ERROR_VARIABLE err)
if(NOT code EQUAL 0)
message(FATAL_ERROR "Shader ${name} (${format}) failed to compile:\n${out}\n${err}")
endif()

View file

@ -137,7 +137,7 @@ name and any extension FFmpeg can demux, then for a `.txt` framefile.
| `-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`.
| `-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. 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.
| `-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.
| `-w`, `--fullscreen_window` | Borderless window covering the desktop.
@ -281,8 +281,9 @@ sounds, fonts, videos, the 3D scene, physics, emitters, the overlay back to
its default size), the engine stays (the window, the GPU, the disc keeps
playing from where it was, the controllers), and the script runs afresh, so a
52 MB model is back on screen in the time it takes to load it rather than the
time it takes to start Singe. `F5` reloads on demand, and a script can ask for
it with `singeReload()`. Packed games have nothing to watch; the option is for
time it takes to start Singe. A key or button held through the reload is
ignored until it is released, as at a fresh start. `F5` reloads on demand,
and a script can ask for it with `singeReload()`. Packed games have nothing to watch; the option is for
a game in a directory. A script error on reload is printed and the game sits
empty until the next reload fixes it. Because the reload runs the same
teardown that quitting does, anything a script leaks shows up here first.
@ -366,7 +367,12 @@ 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`,
`Cfg/game.cfg`, and `DIR .. "Cfg/game.cfg"` all find the same file. Names
starting with `Singe/` or with the data directory stay on the filesystem;
`Singe/Framework.singe` and `singeGetDataPath()` work as always.
`Singe/Framework.singe` and `singeGetDataPath()` work as always. A relative
name with a `..` component resolves nowhere inside a packed game: `io.open`
returns `nil` with a message saying the name reaches outside the game,
`lfs.attributes` returns `nil`, and `spriteLoad` and its kind report the
file as missing. `lfs.dir` on a packed directory lists the union of the
three places, sorted without regard to case.
`require("name")` finds `name.lua`, `name/init.lua`, or `name.singe` under the
script's directory, then under the game root.
@ -375,8 +381,10 @@ The packer refuses a directory that has no `games.dat`, that contains
`bat`, or `cmd`, or an extensionless file whose name starts with `singe`,
that has a top level entry named like the directory itself (that would make the own-directory
rule ambiguous), or whose scripts and data files reach outside the game
with `..`; it reports the file and line. Two files whose names differ only
by case cannot both be packed. Stale `.index` files are skipped. A game
with `..`; it reports every such file, with the line. Two files whose
names differ only by case cannot both be packed. Stale `.index` files are
skipped. `--unpack` refuses a database whose stored names would land outside the target
directory (an absolute path, a drive letter, or a `..` component). A game
that references another game's directory (a shared framework beside it)
must copy that directory inside first. Before the database is committed,
its `games.dat` is checked: every entry needs a `SCRIPT`, and each
@ -394,7 +402,7 @@ may also carry a `removed(path)` table naming files to delete. A `.patch`
can be unpacked like a game, but it cannot be run and the menu ignores it.
A packed game keeps its data directory exactly where the loose game would
have it (`data/DLe/` for `DLe/DLe.singe`), so saves and settings are shared
have it (`DLe/` for `DLe/DLe.singe`, or `data/DLe/` under `--datadir=data`), so saves and settings are shared
between a loose install and a packed one. The video index goes there too.
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.
@ -1091,7 +1099,12 @@ takes, and a packed `.game` carries it along. The file must be
self-contained: meshes, skins, animations and textures all inside the one
file, which is Blender's default export. A model that refers to a `.bin` or
an image beside it is refused with a message naming the file, for the same
reason a packed game has to be self-contained. Loading is the expensive
reason a packed game has to be self-contained. Materials use the
metallic-roughness model with `KHR_materials_emissive_strength` and
`KHR_texture_basisu`; an alpha `MASK` material draws opaque, a texture
addressed through a second set of texture coordinates reads the first set,
and `KHR_texture_transform` is ignored, each with a line in the program
trace. A skin may have up to `128` joints. Loading is the expensive
part; `modelInstance` then places a copy of the model's node tree, as many
times as you like, and every instance shares the file's meshes and
materials. `modelDelete` frees the model; instances keep their nodes, bare.
@ -2045,7 +2058,8 @@ audio file next to them. Every audio track in the file is available to
format, channel count, and rate.
The first time a video is opened, Singe indexes it and stores the index next
to the game's other data (`<name>.index`). Indexing is one pass over the file
to the game's other data (`<name>-<hash>.index`, the hash made from the
video's full name so two videos with one base name keep separate indexes). Indexing is one pass over the file
without decoding, a few seconds even for a feature length disc, and happens
again if the video changes. When a video is loaded, Singe
reports its keyframe spacing in the program trace (`--program`), with a
@ -2420,7 +2434,7 @@ bodyNew(node, type, shape, a, b)
bodyNew(node, type, shape, a, b, c)
----
Gives the node a body of the given type, placed where the node is in the world right now (its parents included), with a collision shape sized by `a`, `b` and `c` in world units and scaled by the node's world scale. A second call on the same node replaces the first, and a node that had a player loses it. A dynamic body's mass comes from its shape's volume at the density of water, so set `bodySetMass` on anything meant to feel light. Changing the node's scale later does not resize the body. Raises an error for an unknown type or shape, when physics is not available on this machine, or when the shape cannot be built (a dynamic `SHAPE_MESH`, or a hull with no mesh under the node).
Gives the node a body of the given type, placed where the node is in the world right now (its parents included), with a collision shape sized by `a`, `b` and `c` in world units and scaled by the node's world scale. A second call on the same node replaces the first, and a node that had a player loses it. A dynamic body's mass comes from its shape's volume at the density of water, so set `bodySetMass` on anything meant to feel light. Changing the node's scale later does not resize the body. Raises an error for an unknown type or shape, when physics is not available on this machine, when the world already holds `4096` bodies, or when the shape cannot be built (a dynamic `SHAPE_MESH`, or a hull with no mesh under the node).
*Parameters:*
@ -3210,7 +3224,7 @@ Draws a straight line between two world-space points over the scene for the curr
* `x0, y0, z0` -- the start point, in world units.
* `x1, y1, z1` -- the end point, in world units.
* `r, g, b` -- the line color, `0` to `255` per channel; white when omitted.
* `r, g, b` -- the line color, `0` to `255` per channel, clamped; white when omitted.
*Since:* 3.00.
*See also:* <<physicssetdebug,physicsSetDebug>>, <<navdraw,navDraw>>, <<physicsraycast,physicsRaycast>>
@ -3579,7 +3593,7 @@ end
discSearch(frame)
----
Seeks the disc to `frame`, shows that frame and pauses on it, clearing the stopped state if the disc was stopped. The disc holds the frame until `discPlay`, `discSkipToFrame` or another transport call. For a single video file the frame number wraps modulo the frame count, so a negative or past-the-end frame lands somewhere inside the video rather than failing; with a framefile the segment containing the frame is selected and the position is clamped inside it. Does nothing without a disc.
Seeks the disc to `frame`, shows that frame and pauses on it, clearing the stopped state if the disc was stopped. The disc holds the frame until `discPlay`, `discSkipToFrame` or another transport call. A negative frame clamps to the first frame and one past the end clamps to the last; with a framefile the segment containing the frame is selected and the position is clamped inside it. Does nothing without a disc.
*Parameters:*
@ -3689,7 +3703,7 @@ discPlay()
discSkipBackward(frames)
----
Seeks backward by `frames` from the current frame without changing the play or pause state: a playing disc keeps playing from the new position, a paused disc stays paused on it. The call is ignored while the disc is stopped and without a disc. The delta is not validated; a negative value skips forward, and a delta larger than the current frame wraps to the end of a single video file or clamps at the start of a framefile segment.
Seeks backward by `frames` from the current frame without changing the play or pause state: a playing disc keeps playing from the new position, a paused disc stays paused on it. The call is ignored while the disc is stopped and without a disc. The delta is not validated; a negative value skips forward, and a delta larger than the current frame clamps at frame `0`. With a framefile the delta applies to the frame number `discGetFrame` reports, so a skip may cross into an earlier segment.
*Parameters:*
@ -3743,7 +3757,7 @@ discSearch(TITLE_FRAME)
discSkipForward(frames)
----
Seeks forward by `frames` from the current frame without changing the play or pause state. Mirror of `discSkipBackward`: ignored while stopped and without a disc, the delta is not validated, and a target past the last frame wraps to the start of a single video file or clamps at the end of a framefile segment.
Seeks forward by `frames` from the current frame without changing the play or pause state. Mirror of `discSkipBackward`: ignored while stopped and without a disc, the delta is not validated, and a target past the last frame clamps at the last frame. With a framefile the delta applies to the frame number `discGetFrame` reports, so a skip may cross into a later segment.
*Parameters:*
@ -3772,7 +3786,7 @@ end
discSkipToFrame(frame)
----
Seeks to `frame` and starts playing from it no matter the disc's state, clearing the stopped state if necessary. Contrast with `discSearch`, which seeks and pauses. Frame numbers wrap modulo the frame count for a single video file and are clamped within the segment for a framefile. Does nothing without a disc, apart from clearing the stopped flag.
Seeks to `frame` and starts playing from it no matter the disc's state, clearing the stopped state if necessary. Contrast with `discSearch`, which seeks and pauses. A frame before the start or past the end clamps to the first or last frame; with a framefile the segment containing the frame is selected and the position is clamped inside it. Does nothing without a disc, apart from clearing the stopped flag.
*Parameters:*
@ -3802,7 +3816,7 @@ end
discStepBackward()
----
Moves the disc back exactly one frame and pauses on it, whatever the previous play state. Stepping at frame `0` stays on frame `0`. The stopped state is not cleared, so after `discStop` the step happens behind the blue screen. Intended for frame-accurate debugging and service screens. Does nothing without a disc.
Moves the disc back exactly one frame and pauses on it, whatever the previous play state. Stepping at frame `0` stays on frame `0`. Ignored while the disc is stopped, like `discSkipBackward`. Intended for frame-accurate debugging and service screens. Does nothing without a disc.
*Since:* 1.x
*See also:* <<discstepforward,discStepForward>>, <<discskipbackward,discSkipBackward>>, <<discgetframe,discGetFrame>>
@ -3828,7 +3842,7 @@ end
discStepForward()
----
Moves the disc forward exactly one frame and pauses on it, whatever the previous play state. Stepping past the last frame wraps to frame `0` on a single video file and clamps at the end of the segment with a framefile. Like `discStepBackward` it does not clear the stopped state. Does nothing without a disc.
Moves the disc forward exactly one frame and pauses on it, whatever the previous play state. Stepping on the last frame stays on the last frame. Like `discStepBackward` it is ignored while the disc is stopped. Does nothing without a disc.
*Since:* 1.x
*See also:* <<discstepbackward,discStepBackward>>, <<discskipforward,discSkipForward>>, <<discgetframe,discGetFrame>>
@ -3855,7 +3869,7 @@ end
discStop()
----
Stops the disc. Playback pauses, the video is replaced by the classic blue screen, `discGetState` reports `DISC_STOPPED` and `discGetFrame` returns `0` until the disc is started again. While stopped, `discPause`, `discSkipForward` and `discSkipBackward` are ignored. Any of `discPlay`, `discSearch` or `discSkipToFrame` clears the stopped state. A second `discStop` is ignored.
Stops the disc. Playback pauses, the video is replaced by the classic blue screen, `discGetState` reports `DISC_STOPPED` and `discGetFrame` returns `0` until the disc is started again. While stopped, `discPause`, `discSkipForward`, `discSkipBackward`, `discStepForward` and `discStepBackward` are ignored. Any of `discPlay`, `discSearch` or `discSkipToFrame` clears the stopped state. A second `discStop` is ignored.
*Since:* 1.x
*See also:* <<discplay,discPlay>>, <<discpause,discPause>>, <<discgetstate,discGetState>>
@ -4707,7 +4721,7 @@ Singe has two text renderers. The built-in console font is drawn by <<overlaypri
result = fontLoad(filename, pointSize)
----
Opens a TrueType font at one point size and makes it the selected font. The size is fixed for the life of the handle, so load the same file once per size you need. The name goes through the virtual file system like any other asset; prepend `DIR` for files shipped with your game, or use `Singe/FreeSansBold.ttf` for the bundled face. A file that cannot be opened ends the script with the font library's error message.
Opens a TrueType font at one point size and makes it the selected font. The size is fixed for the life of the handle, so load the same file once per size you need. The name goes through the virtual file system like any other asset; prepend `DIR` for files shipped with your game, or use `Singe/FreeSansBold.ttf` for the bundled face. A file that cannot be opened ends the script with `Unable to open` and the name.
*Parameters:*
@ -5313,7 +5327,7 @@ lightSetIntensity(headlight, 30)
lightSetColor(node, r, g, b)
----
The light's color, `0` to `255` per channel, treated as sRGB; white by default. Brightness belongs in `lightSetIntensity`, so keep the color for the hue (a warm `255, 230, 190` bulb, a cool `150, 190, 255` moon) and leave at least one channel near `255`. Raises an error when the node carries no light.
The light's color, `0` to `255` per channel (out-of-range values are clamped), treated as sRGB; white by default. Brightness belongs in `lightSetIntensity`, so keep the color for the hue (a warm `255, 230, 190` bulb, a cool `150, 190, 255` moon) and leave at least one channel near `255`. Raises an error when the node carries no light.
*Since:* 3.00.
*See also:* <<lightsetintensity,lightSetIntensity>>, <<lightnew,lightNew>>
@ -5349,7 +5363,7 @@ The shape of a spot light's cone, both angles measured from the axis (the node's
* `node` -- the light's node.
* `innerDegrees` -- half angle of full brightness, in degrees from the axis.
* `outerDegrees` -- half angle where the light reaches nothing, in degrees from the axis; larger than `innerDegrees`.
* `outerDegrees` -- half angle where the light reaches nothing, in degrees from the axis; raised to `innerDegrees` when smaller.
*Since:* 3.00.
*See also:* <<lightnew,lightNew>>, <<lightsetrange,lightSetRange>>, <<nodelookat,nodeLookAt>>
@ -5499,7 +5513,7 @@ nodeSetMesh(trophy, meshCone(0.3, 0.8, 24), gold)
materialDelete(material)
----
Frees the material and any texture it uploaded. Every node that was drawn with it falls back to the default look (white, half rough), so reassign those nodes with `nodeSetMaterial` first if they should keep a look. The handle is invalid afterwards and raises an error if used again. Materials are cheap; deleting them matters mostly for ones that carried large textures.
Frees the material and any texture it uploaded. Every node that was drawn with it falls back to the default look (white, half rough), so reassign those nodes with `nodeSetMaterial` first if they should keep a look. The handle is invalid afterwards and raises an error if used again. The private material a node makes for itself under `nodeSetSprite` or `nodeSetText` belongs to the node and cannot be deleted; the call raises an error, and clearing the sprite or text frees it. Materials are cheap; deleting them matters mostly for ones that carried large textures.
*Since:* 3.00.
*See also:* <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>
@ -5557,7 +5571,7 @@ materialSetColor(material, r, g, b)
materialSetColor(material, r, g, b, a)
----
Sets the base color, `0` to `255` per channel, with `a` defaulting to `255`. The color is treated as sRGB and lit in linear light. When the material has a texture, video or view, the color multiplies it, so white leaves the picture as is and a darker color tints it. The alpha only shows through with `materialSetBlend`.
Sets the base color, `0` to `255` per channel (out-of-range values are clamped), with `a` defaulting to `255`. The color is treated as sRGB and lit in linear light. When the material has a texture, video or view, the color multiplies it, so white leaves the picture as is and a darker color tints it. The alpha only shows through with `materialSetBlend`.
*Parameters:*
@ -5618,7 +5632,7 @@ nodeSetRotation(banner, 90, 0, 0)
materialSetEmissive(material, r, g, b)
----
The light the surface gives off on its own, `0` to `255` per channel, added on top of the lighting: screens, lamp bulbs, neon, instrument panels. The default is black, no glow. It lights nothing else and casts no light; put a light node at the same place for that. With `materialSetEmissiveMap` the map says where on the surface this color applies. Emissive surfaces bright enough to pass the threshold of `sceneSetBloom` glow.
The light the surface gives off on its own, `0` to `255` per channel (out-of-range values are clamped), added on top of the lighting: screens, lamp bulbs, neon, instrument panels. The default is black, no glow. It lights nothing else and casts no light; put a light node at the same place for that. With `materialSetEmissiveMap` the map says where on the surface this color applies. Emissive surfaces bright enough to pass the threshold of `sceneSetBloom` glow.
*Notes:* An unlit material (`materialSetUnlit`) shows its base color only; the emissive color is ignored on it.
@ -6378,7 +6392,7 @@ root node scales the whole instance. See <<scenes3d,3D Scenes>>.
model = modelLoad(name)
----
Reads a `.glb` through the same lookup as every other asset (game directory, data directory, packed database), uploads its meshes and materials to the GPU once, and keeps its node tree, skins and animations for instancing. Call it once at startup and instance the handle as many times as needed; loading the same file twice makes two copies on the GPU. It needs the 3D scene, so on a machine without a suitable GPU it fails like any other 3D call. A bad file ends the script with a message naming the problem: the file could not be read, is not glTF, refers to an external buffer or image, fails validation, or holds a primitive that could not be uploaded.
Reads a `.glb` through the same lookup as every other asset (game directory, data directory, packed database), uploads its meshes and materials to the GPU once, and keeps its node tree, skins and animations for instancing. Call it once at startup and instance the handle as many times as needed; loading the same file twice makes two copies on the GPU. It needs the 3D scene, so on a machine without a suitable GPU it fails like any other 3D call. A bad file ends the script with a message naming the problem: the file could not be read, is not glTF, refers to an external buffer or image, fails validation, holds a primitive that could not be uploaded, has a skin with more than `128` joints, or has a skinned mesh naming a joint its skin does not have. A texture that cannot be decoded, or a missing KTX2 image, does not fail the load; the material goes without that map and the program trace (`--program`) says which one.
*Returns:* The model handle, an integer.
@ -7143,7 +7157,7 @@ navBuild(nav)
points = navPath(nav, x0, y0, z0, x1, y1, z1)
----
The corners of the shortest walk from one point to another, both snapped onto the mesh first, as a table of `{ x, y, z }` tables with the first at the start and the last at the end; up to 256 corners. `nil` when either end is nowhere near the mesh or no path is found. For drawing a route, measuring how far something is on foot, or moving something along it yourself; agents do this on their own.
The corners of the shortest walk from one point to another, both snapped onto the mesh first, as a table of `{ x, y, z }` tables with the first at the start and the last at the end; up to 256 corners. When the end cannot be reached the walk stops at the nearest reachable point on the way, so the last corner is always somewhere an agent can stand. `nil` when either end is nowhere near the mesh or no path is found. For drawing a route, measuring how far something is on foot, or moving something along it yourself; agents do this on their own.
*Parameters:*
@ -9241,7 +9255,7 @@ end
playerSetEnabled(node, enabled)
----
With `false` the simulation skips the player: it does not move, does not fall, does not drive its node, reports no trigger entries or exits, and is left out of the debug drawing. `true` picks it up again from wherever it is. Disable a player while a ragdoll, a cutscene or a vehicle owns the character.
With `false` the player leaves the simulation: its body is removed from the world, so it does not move, fall, drive its node or block anything, `onTrigger` reports it leaving every trigger it stood in, and it is left out of the debug drawing. `true` puts the body back and picks it up again from wherever the node is. Disable a player while a ragdoll, a cutscene or a vehicle owns the character.
*Since:* 3.00.
*See also:* <<playerdelete,playerDelete>>, <<ragdollnew,ragdollNew>>, <<bodysetenabled,bodySetEnabled>>
@ -10075,7 +10089,7 @@ end
WARNING: These functions let a Singe script hand control to another Singe script: the mechanism behind the built-in `Menu.singe` launcher. Game developers should not reach for these; build your game as a single script and let the menu system handle chaining. They are documented here for completeness and for anyone maintaining the menu itself.
Both calls take a table shaped like a `GAMES[]` entry in `games.dat` and queue it for the engine's script runner. The new configuration starts from the running one, so command line options such as the data directory and tracing carry over, but the disc, video and container never do: each entry brings its own. The current script ends at the end of the frame, `onShutdown` fires, and the queued script starts with a fresh Lua state (see <<enginecallbacks,Engine Callbacks>>).
Both calls take a table shaped like a `GAMES[]` entry in `games.dat` and queue it for the engine's script runner. The new configuration starts from the running one, so command line options such as the data directory and tracing carry over, but the disc, video and container never do: each entry brings its own. The current script ends at the end of the frame, `onShutdown` fires, and the queued script starts with a fresh Lua state (see <<enginecallbacks,Engine Callbacks>>). The new script's data directory follows the same rule as a command line launch (see `singeGetDataPath`); when it cannot be created the calling script is aborted.
[#scriptexecute]
==== scriptExecute
@ -10477,7 +10491,7 @@ end
singeReload()
----
Runs the game again from its script at the end of this frame, as `F5` and a changed file do under `--reload`; this call works without the option. Every sound stops, the Lua state is discarded without calling `onShutdown`, everything the script loaded is freed, the scene, physics, particles and navigation are reset, the overlay returns to its default resolution, and the keyboard mode, pause key, sound listener and pause flag return to their defaults. The disc and the engine itself stay as they are. It suits a debug menu's restart item or a level editor reloading what it just saved.
Runs the game again from its script at the end of this frame, as `F5` and a changed file do under `--reload`; this call works without the option. Every sound stops, the Lua state is discarded without calling `onShutdown`, everything the script loaded is freed, the scene, physics, particles and navigation are reset, the overlay returns to its default resolution, and the keyboard mode, pause key, sound listener and pause flag return to their defaults. Keys and buttons held through the reload are ignored until released, as at a fresh start. The disc and the engine itself stay as they are. It suits a debug menu's restart item or a level editor reloading what it just saved.
*Since:* 3.00.
*See also:* <<singequit,singeQuit>>
@ -12527,7 +12541,7 @@ end
[#view]
=== View
A view is a second camera rendered to a texture every frame, for a monitor, a mirror or a portal in the scene: `viewNew` makes one and returns an integer handle, `viewSetCamera` points it at a node, and `materialSetView` shows it on a material. Up to four exist at once; each renders the whole scene again at its own size, in overlay-independent pixels, with the main camera's projection, the frame's shadows and no bloom, so keep them few and small. A bad handle raises an error. See <<scenes3d,3D Scenes>>.
A view is a second camera rendered to a texture every frame, for a monitor, a mirror or a portal in the scene: `viewNew` makes one and returns an integer handle, `viewSetCamera` points it at a node, and `materialSetView` shows it on a material. Up to four exist at once; each renders the whole scene again at its own size, in overlay-independent pixels, with the main camera's projection, the frame's shadows and no bloom, so keep them few and small. Billboards (`nodeSetBillboard`) are turned to face the window's camera and blended draws are sorted back to front from it, once for every view, so a view whose camera looks from elsewhere sees billboards side on and may see transparent objects overlap in the wrong order. A bad handle raises an error. See <<scenes3d,3D Scenes>>.
[#viewdelete]
==== viewDelete
@ -13604,7 +13618,7 @@ function onTrigger(trigger, other, entered)
end
----
Called when a body enters or leaves a trigger made with `bodySetTrigger`, once on the way in and once on the way out. Triggers never push anything, so this is the way to notice a body crossing a doorway, reaching a checkpoint or falling into a kill volume.
Called when a body enters or leaves a trigger made with `bodySetTrigger`, once on the way in and once on the way out, whatever the shapes involved (a mesh or compound shape touching in several places still counts as one body). Triggers never push anything, so this is the way to notice a body crossing a doorway, reaching a checkpoint or falling into a kill volume.
*Parameters:*

View file

@ -156,6 +156,7 @@ int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer
char *temp = NULL;
char *frameLine = NULL;
char *space = NULL;
char *end = NULL;
char *endptr = NULL;
const char *name = NULL;
const char *offset = NULL;
@ -202,11 +203,16 @@ int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer
frame = strtoll(frameLine, &endptr, 10);
space = strchr(frameLine, ' ');
if ((space != NULL) && (endptr == space)) {
// Got an integer. Point at filename.
// Got an integer. Point at filename, without the spaces around it.
name = space;
while (*name == ' ') {
name++;
}
end = frameLine + strlen(frameLine);
while ((end > name) && (end[-1] == ' ')) {
end--;
}
*end = 0;
// Copy frame number and filename into array
newFiles = realloc(files, sizeof(FrameLineT) * (size_t)(count + 1));
if (!newFiles) {
@ -215,11 +221,11 @@ int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer
files = newFiles;
files[count].frame = frame;
files[count].filename = utilCreateString("%s%s", path, name);
// Is this an old m2v/ogg pair?
// Is this an old m2v/ogg pair? The audio may be packed like the video, so ask the vfs.
audio = NULL;
if (utilStricmp(utilGetFileExtension(files[count].filename), "m2v") == 0) {
audio = utilCreateString("%.*s.ogg", (int32_t)(strlen(files[count].filename) - strlen("m2v") - 1), files[count].filename);
if (!utilFileExists(audio)) {
if (!vfsExists(audio)) {
free(audio);
audio = NULL;
}
@ -270,7 +276,7 @@ void frameFileQuit(void) {
// Daphne-like framefile searching: the segment is the last one starting at or before the frame.
bool frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame) {
void frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame) {
FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileSeek");
int32_t i = 0;
int32_t found = 0;
@ -292,8 +298,6 @@ bool frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHan
*actualFrame = last;
}
_selectSegment(f, found, *actualFrame, videoHandle);
return true;
}

View file

@ -33,7 +33,7 @@
int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle);
int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer *renderer, bool showCalculated);
void frameFileQuit(void);
bool frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame);
void frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame);
void frameFileUnload(int32_t frameFileHandle);
void frameFileUpdate(int32_t frameFileHandle, int32_t *videoHandle);

View file

@ -34,8 +34,10 @@
#define RGBE_RLE_MIN_WIDTH 8 // Scanlines this wide and narrower are never run-length coded
#define RGBE_RLE_MAX_WIDTH 32767
#define RGBE_HEADER_MAX 4096 // Bytes to search for the header's end
#define RGBE_DIMENSION_MAX 16384 // Pixels a side; larger claims are corrupt or hostile
#define SRGB_LINEAR_CUTOFF 0.04045f
#define COLOUR_MAX 255.0f
#define COLOUR_LEVELS 256
static bool _isRadiance(const uint8_t *bytes, size_t size);
@ -53,14 +55,21 @@ static bool _isRadiance(const uint8_t *bytes, size_t size) {
}
// An sRGB byte as linear light.
// An sRGB byte as linear light, from a table built on first use.
static float _linear(uint8_t value) {
float v = value / COLOUR_MAX;
static float table[COLOUR_LEVELS];
static bool ready = false;
int32_t x;
if (v <= SRGB_LINEAR_CUTOFF) {
return v / 12.92f;
if (!ready) {
for (x = 0; x < COLOUR_LEVELS; x++) {
float v = (float)x / COLOUR_MAX;
table[x] = (v <= SRGB_LINEAR_CUTOFF) ? v / 12.92f : SDL_powf((v + 0.055f) / 1.055f, 2.4f);
}
return SDL_powf((v + 0.055f) / 1.055f, 2.4f);
ready = true;
}
return table[value];
}
@ -96,7 +105,7 @@ static float *_loadRadiance(const uint8_t *bytes, size_t size, int32_t *width, i
}
memcpy(line, bytes + offset, lineEnd - offset);
line[lineEnd - offset] = '\0';
if ((SDL_sscanf(line, "-Y %d +X %d", &h, &w) != 2) || (w <= 0) || (h <= 0)) {
if ((SDL_sscanf(line, "-Y %d +X %d", &h, &w) != 2) || (w <= 0) || (h <= 0) || (w > RGBE_DIMENSION_MAX) || (h > RGBE_DIMENSION_MAX)) {
SDL_SetError("Unsupported Radiance orientation or size: %s", line);
return NULL;
}

View file

@ -22,6 +22,7 @@
// Singe - KTX2 textures on the Basis Universal transcoder, behind the C interface in ktx2.h.
#include <stdint.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <basisu_transcoder.h>
@ -85,7 +86,7 @@ bool ktx2Transcode(const void *bytes, size_t size, Ktx2FormatE wanted, Ktx2Image
basist::basisu_transcoder_init();
_ready = true;
}
if (!transcoder.init(bytes, (uint32_t)size) || !transcoder.start_transcoding()) {
if ((size > UINT32_MAX) || !transcoder.init(bytes, (uint32_t)size) || !transcoder.start_transcoding()) {
SDL_SetError("Not a Basis Universal KTX2 file.");
return false;
}
@ -107,7 +108,7 @@ bool ktx2Transcode(const void *bytes, size_t size, Ktx2FormatE wanted, Ktx2Image
for (level = 0; level < levels; level++) {
basist::ktx2_image_level_info info;
Ktx2LevelT *dst = &out->levels[level];
uint32_t count;
size_t count;
uint32_t pitch = 0;
if (!transcoder.get_image_level_info(info, level, 0, 0)) {
@ -118,12 +119,17 @@ bool ktx2Transcode(const void *bytes, size_t size, Ktx2FormatE wanted, Ktx2Image
dst->width = (int32_t)info.m_orig_width;
dst->height = (int32_t)info.m_orig_height;
if (basist::basis_transcoder_format_is_uncompressed(format)) {
count = info.m_orig_width * info.m_orig_height;
dst->size = (size_t)count * basist::basis_get_uncompressed_bytes_per_pixel(format);
count = (size_t)info.m_orig_width * (size_t)info.m_orig_height;
dst->size = count * basist::basis_get_uncompressed_bytes_per_pixel(format);
pitch = info.m_orig_width;
} else {
count = info.m_total_blocks;
dst->size = (size_t)count * basist::basis_get_bytes_per_block_or_pixel(format);
dst->size = count * basist::basis_get_bytes_per_block_or_pixel(format);
}
if (count > UINT32_MAX) {
ktx2Free(out);
SDL_SetError("KTX2 level %u is too large.", level);
return false;
}
dst->data = (uint8_t *)SDL_malloc(dst->size);
if (dst->data == NULL) {
@ -131,7 +137,7 @@ bool ktx2Transcode(const void *bytes, size_t size, Ktx2FormatE wanted, Ktx2Image
SDL_SetError("Out of memory transcoding a texture.");
return false;
}
if (!transcoder.transcode_image_level(level, 0, 0, dst->data, count, format, 0, pitch, 0)) {
if (!transcoder.transcode_image_level(level, 0, 0, dst->data, (uint32_t)count, format, 0, pitch, 0)) {
ktx2Free(out);
SDL_SetError("KTX2 level %u would not transcode.", level);
return false;

View file

@ -22,6 +22,7 @@
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
@ -59,7 +60,6 @@
#include "embedded.h"
#define SUPPORT_DIR "Singe"
#define MENU_OPTIONS "-k -w -d data -v"
#define PRIMARY_DISPLAY 0
#define MIXER_FREQUENCY 44100
@ -189,6 +189,7 @@ static bool _runTool(const ConfigT *conf);
static bool _modeMatchesRatio(int32_t index, int32_t ratioIndex);
static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]);
static bool _parseInteger(const char *text, int32_t *value);
static void _requireRange(const char *exeName, int32_t value, int32_t min, int32_t max, const char *what, const char *unit);
static void _resolveFiles(const char *exeName, ConfigT *conf);
static void _showHeader(void);
static void _showUsage(const char *name, const char *message) __attribute__((noreturn));
@ -213,8 +214,9 @@ static bool _extractFile(const char *filename, const uint8_t *data, size_t lengt
size_t bytes = 0;
bool written = false;
bool same = false;
bool existed = utilFileExists(filename);
if (utilFileExists(filename)) {
if (existed) {
existing = utilReadFile(filename, &bytes);
same = (existing != NULL) && (bytes == length) && (memcmp(existing, data, length) == 0);
free(existing);
@ -235,7 +237,7 @@ static bool _extractFile(const char *filename, const uint8_t *data, size_t lengt
unlink(filename);
utilDie("Unable to write %s", filename);
}
utilSay(">>> %s File: %s", existing ? "Updated" : "Created", filename);
utilSay(">>> %s File: %s", existed ? "Updated" : "Created", filename);
return true;
}
@ -325,9 +327,6 @@ static void _launcher(const char *exeName, ConfigT *conf) {
}
}
}
if (conf->bestRatioIndex < 0) {
_showUsage(exeName, "Unknown aspect ratio.");
}
_mainTrace(conf, "Aspect ratio is %d:%d", _modes[conf->bestRatioIndex].ratio.aspectNum, _modes[conf->bestRatioIndex].ratio.aspectDom);
// Were both resolutions not specified?
if ((conf->xResolution <= 0) && (conf->yResolution <= 0)) {
@ -733,23 +732,11 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
_showUsage(exeName, "Full Screen or Full Screen Windowed. Pick one.");
}
// Sane volume values?
if ((conf->volumeVldp < VOLUME_MIN) || (conf->volumeVldp > VOLUME_MAX)) {
_showUsage(exeName, "Laserdisc volume must be between 0 and 100 percent.");
}
if ((conf->volumeNonVldp < VOLUME_MIN) || (conf->volumeNonVldp > VOLUME_MAX)) {
_showUsage(exeName, "Effects volume must be between 0 and 100 percent.");
}
// Sane audio delay?
if ((conf->audioDelayMs < -VIDEO_AUDIO_DELAY_MAX) || (conf->audioDelayMs > VIDEO_AUDIO_DELAY_MAX)) {
_showUsage(exeName, "Audio delay must be between -1000 and 1000 milliseconds.");
}
// Sane scale factor?
if ((conf->scaleFactor < SCALE_FACTOR_MIN) || (conf->scaleFactor > SCALE_FACTOR_MAX)) {
_showUsage(exeName, "Display scale must be between 50 and 100 percent.");
}
// Sane volume, delay and scale values?
_requireRange(exeName, conf->volumeVldp, VOLUME_MIN, VOLUME_MAX, "Laserdisc volume", "percent");
_requireRange(exeName, conf->volumeNonVldp, VOLUME_MIN, VOLUME_MAX, "Effects volume", "percent");
_requireRange(exeName, conf->audioDelayMs, -VIDEO_AUDIO_DELAY_MAX, VIDEO_AUDIO_DELAY_MAX, "Audio delay", "milliseconds");
_requireRange(exeName, conf->scaleFactor, SCALE_FACTOR_MIN, SCALE_FACTOR_MAX, "Display scale", "percent");
// Sinden light gun?
if (sindenString) {
@ -810,8 +797,9 @@ static bool _parseInteger(const char *text, int32_t *value) {
if ((text == NULL) || (*text == 0)) {
return false;
}
errno = 0;
parsed = strtol(text, &end, 10);
if (*end != 0) {
if ((*end != 0) || (errno == ERANGE) || (parsed < INT32_MIN) || (parsed > INT32_MAX)) {
return false;
}
*value = (int32_t)parsed;
@ -820,6 +808,14 @@ static bool _parseInteger(const char *text, int32_t *value) {
}
// Usage error naming the limits, so the message cannot drift from the constants.
static void _requireRange(const char *exeName, int32_t value, int32_t min, int32_t max, const char *what, const char *unit) {
if ((value < min) || (value > max)) {
_showUsage(exeName, utilCreateString("%s must be between %d and %d %s.", what, min, max, unit));
}
}
static void _resolveFiles(const char *exeName, ConfigT *conf) {
size_t length = 0;
const char *extension = NULL;
@ -883,13 +879,8 @@ static void _resolveFiles(const char *exeName, ConfigT *conf) {
}
conf->isFrameFile = conf->disc && isFrameFileName(conf->videoFile);
if (conf->dataDirGiven || conf->container) {
// Under the base, in a directory named for the game.
conf->dataDir = createDataDirFor(conf);
} else {
// No data directory specified. Use the game folder.
conf->dataDir = utilGetUpToLastPathComponent(conf->scriptFile);
}
free(conf->dataDir);
conf->dataDir = resolveDataDir(conf);
if (!conf->dataDir) {
_showUsage(exeName, "Unable to create data directory.");
}
@ -1033,12 +1024,12 @@ static void _unpackData(const char *name) {
bool created = false;
// Extract missing or outdated support files. We do this here so they are not generated if launched from a front end.
if (!utilMkDirP(SUPPORT_DIR, DIRECTORY_MODE)) {
utilDie("Unable to create %s directory.", SUPPORT_DIR);
if (!utilMkDirP(VFS_ENGINE_DIRECTORY, DIRECTORY_MODE)) {
utilDie("Unable to create %s directory.", VFS_ENGINE_DIRECTORY);
}
for (x = 0; x < (int32_t)(sizeof(files) / sizeof(files[0])); x++) {
temp = utilCreateString("%s%c%s", SUPPORT_DIR, utilGetPathSeparator(), files[x].name);
temp = utilCreateString("%s%c%s", VFS_ENGINE_DIRECTORY, utilGetPathSeparator(), files[x].name);
created |= _extractFile(temp, files[x].data, files[x].length);
free(temp);
}
@ -1047,14 +1038,14 @@ static void _unpackData(const char *name) {
if (utilGetPathSeparator() == '/') {
// Unix-ish
temp = strdup("Menu.sh");
data = utilCreateString("#!/bin/sh\n\ncd \"$(dirname \"$0\")\"\n./%s %s %s/menuBackground.mkv %s/Menu.singe\n", utilGetLastPathComponent(name), MENU_OPTIONS, SUPPORT_DIR, SUPPORT_DIR);
data = utilCreateString("#!/bin/sh\n\ncd \"$(dirname \"$0\")\"\n./%s %s %s/menuBackground.mkv %s/Menu.singe\n", utilGetLastPathComponent(name), MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY);
} else {
// Winders
temp = strdup("Menu.bat");
data = utilCreateString("@start %s %s %s\\menuBackground.mkv %s\\Menu.singe\n", utilGetLastPathComponent(name), MENU_OPTIONS, SUPPORT_DIR, SUPPORT_DIR);
data = utilCreateString("@start %s %s %s\\menuBackground.mkv %s\\Menu.singe\n", utilGetLastPathComponent(name), MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY);
}
created |= _extractFile(temp, (const uint8_t *)data, strlen(data));
utilChMod(temp, DIRECTORY_MODE);
utilChMod(temp, SCRIPT_MODE);
free(data);
free(temp);
@ -1122,15 +1113,16 @@ char *createDataDir(const char *dataDirBase, const char *filename) {
// The data directory follows the script's directory, or the database's name when the script sits at its root.
char *createDataDirFor(const ConfigT *conf) {
const char *base = NULL;
char *stem = NULL;
char *name = NULL;
char *path = NULL;
if ((conf->container != NULL) && (strchr(conf->scriptFile, '/') == NULL) && (strchr(conf->scriptFile, '\\') == NULL)) {
base = utilGetLastPathComponent(conf->container);
name = utilCreateString("%.*s%c%s", (int)(strlen(base) - strlen(VFS_DATABASE_EXTENSION)), base, utilGetPathSeparator(), conf->scriptFile);
stem = vfsDatabaseStem(utilGetLastPathComponent(conf->container));
name = utilCreateString("%s%c%s", stem, utilGetPathSeparator(), conf->scriptFile);
path = createDataDir(conf->dataDirBase, name);
free(name);
free(stem);
return path;
}
@ -1215,6 +1207,18 @@ void queueScript(const ConfigT *conf) {
}
// Where a game writes: under the -d base (or, for a packed game, the default base) in a directory
// named for the game; without either, the game's own folder. A new string, or NULL when it cannot
// be created. The one rule for command line, games.dat and scriptPush launches alike.
char *resolveDataDir(const ConfigT *conf) {
if (conf->dataDirGiven || (conf->container != NULL)) {
return createDataDirFor(conf);
}
return utilGetUpToLastPathComponent(conf->scriptFile);
}
int main(int argc, char *argv[]) {
const char *exeName = argv[0];
char *temp = NULL;

View file

@ -33,6 +33,7 @@
#define SCALE_FACTOR_MIN 50
#define SCALE_FACTOR_MAX 100
#define DIRECTORY_MODE 0777
#define SCRIPT_MODE 0755 // Menu.sh: chmod ignores the umask, so no world write
#define CANVAS_DEFAULT_WIDTH 720
#define CANVAS_DEFAULT_HEIGHT 480
@ -44,6 +45,7 @@ void destroyConf(ConfigT **confPointer);
bool isFrameFileName(const char *filename);
bool parseSindenString(const char *sindenString, ConfigT *conf);
void queueScript(const ConfigT *conf);
char *resolveDataDir(const ConfigT *conf);
#endif // MAIN_H

View file

@ -28,9 +28,6 @@
#include "math3d.h"
#define EPSILON 1e-6f
// Translation * rotation * scale, the glTF node transform.
Mat4T mat4Compose(Vec3T translation, QuatT rotation, Vec3T scale) {
Mat4T out;
@ -105,7 +102,9 @@ Mat4T mat4Identity(void) {
}
// General 4x4 inverse by cofactors. Returns false for a singular matrix (out untouched).
// General 4x4 inverse by cofactors. Returns false for a singular matrix (out untouched): one
// whose determinant is zero or too small to take a finite reciprocal of, so a node scaled down by
// any ordinary amount still inverts.
bool mat4Invert(Mat4T a, Mat4T *out) {
float inv[16];
float det;
@ -129,7 +128,7 @@ bool mat4Invert(Mat4T a, Mat4T *out) {
inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5];
inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5];
det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
if (fabsf(det) < EPSILON) {
if ((det == 0.0f) || !isfinite(1.0f / det)) {
return false;
}
det = 1.0f / det;
@ -188,6 +187,37 @@ Mat4T mat4Multiply(Mat4T a, Mat4T b) {
}
// The matrix that carries normals under a's rotation and scale: the inverse transpose of its
// upper 3x3, taken as the cofactor matrix over the determinant (no 4x4 inverse needed). Under a
// singular matrix (a zero scale) the normals keep their direction.
Mat4T mat4NormalMatrix(Mat4T a) {
Mat4T out = mat4Identity();
const float *m = a.m;
float c[9];
float det;
int32_t x;
c[0] = m[5] * m[10] - m[6] * m[9];
c[1] = m[6] * m[8] - m[4] * m[10];
c[2] = m[4] * m[9] - m[5] * m[8];
c[3] = m[2] * m[9] - m[1] * m[10];
c[4] = m[0] * m[10] - m[2] * m[8];
c[5] = m[1] * m[8] - m[0] * m[9];
c[6] = m[1] * m[6] - m[2] * m[5];
c[7] = m[2] * m[4] - m[0] * m[6];
c[8] = m[0] * m[5] - m[1] * m[4];
det = m[0] * c[0] + m[1] * c[1] + m[2] * c[2];
if ((det == 0.0f) || !isfinite(1.0f / det)) {
return out;
}
det = 1.0f / det;
for (x = 0; x < 9; x++) {
out.m[(x / 3) * 4 + (x % 3)] = c[x] * det;
}
return out;
}
// Depth maps to 0..1 (what SDL_GPU expects on every backend).
Mat4T mat4Orthographic(float width, float height, float near, float far) {
Mat4T out;
@ -232,22 +262,31 @@ Mat4T mat4Perspective(float fovDegrees, float aspect, float near, float far) {
}
Vec3T mat4TransformPoint(Mat4T a, Vec3T p) {
// A point through a matrix, divided by the clip w it comes out with (which is handed back for
// callers that need to know which side of the camera the point is on).
Vec3T mat4Project(Mat4T a, Vec3T p, float *w) {
Vec3T out;
float w = a.m[3] * p.x + a.m[7] * p.y + a.m[11] * p.z + a.m[15];
*w = a.m[3] * p.x + a.m[7] * p.y + a.m[11] * p.z + a.m[15];
out.x = a.m[0] * p.x + a.m[4] * p.y + a.m[8] * p.z + a.m[12];
out.y = a.m[1] * p.x + a.m[5] * p.y + a.m[9] * p.z + a.m[13];
out.z = a.m[2] * p.x + a.m[6] * p.y + a.m[10] * p.z + a.m[14];
if (fabsf(w) > EPSILON) {
out.x /= w;
out.y /= w;
out.z /= w;
if (fabsf(*w) > MATH_EPSILON) {
out.x /= *w;
out.y /= *w;
out.z /= *w;
}
return out;
}
Vec3T mat4TransformPoint(Mat4T a, Vec3T p) {
float w;
return mat4Project(a, p, &w);
}
// Directions ignore translation.
Vec3T mat4TransformVector(Mat4T a, Vec3T v) {
Vec3T out;
@ -340,6 +379,17 @@ QuatT quatIdentity(void) {
}
// The inverse rotation: normalised, then conjugated.
QuatT quatInverse(QuatT q) {
QuatT out = quatNormalize(q);
out.x = -out.x;
out.y = -out.y;
out.z = -out.z;
return out;
}
// The rotation that points -Z along forward with +Y near up.
QuatT quatLookRotation(Vec3T forward, Vec3T up) {
Mat4T m;
@ -347,7 +397,7 @@ QuatT quatLookRotation(Vec3T forward, Vec3T up) {
Vec3T s;
Vec3T u;
if (vec3Length(vec3Cross(f, up)) < EPSILON) {
if (vec3Length(vec3Cross(f, up)) < MATH_EPSILON) {
// Looking straight along up: pick any perpendicular.
up = (fabsf(f.y) < 0.9f) ? vec3(0.0f, 1.0f, 0.0f) : vec3(0.0f, 0.0f, 1.0f);
}
@ -382,7 +432,7 @@ QuatT quatMultiply(QuatT a, QuatT b) {
QuatT quatNormalize(QuatT q) {
float length = sqrtf(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
if (length < EPSILON) {
if (length < MATH_EPSILON) {
return quatIdentity();
}
q.x /= length;
@ -417,7 +467,7 @@ QuatT quatSlerp(QuatT a, QuatT b, float t) {
b.w = -b.w;
cosTheta = -cosTheta;
}
if (cosTheta > 1.0f - EPSILON) {
if (cosTheta > 1.0f - MATH_EPSILON) {
// Nearly parallel: lerp is accurate and avoids the division.
out.x = a.x + (b.x - a.x) * t;
out.y = a.y + (b.y - a.y) * t;
@ -449,7 +499,7 @@ void quatToEuler(QuatT q, float *xDegrees, float *yDegrees, float *zDegrees) {
sinX = -1.0f;
}
*xDegrees = RADIANS_TO_DEGREES(asinf(sinX));
if (fabsf(sinX) < 1.0f - EPSILON) {
if (fabsf(sinX) < 1.0f - MATH_EPSILON) {
*yDegrees = RADIANS_TO_DEGREES(atan2f(m.m[8], m.m[10]));
*zDegrees = RADIANS_TO_DEGREES(atan2f(m.m[1], m.m[5]));
} else {
@ -495,7 +545,7 @@ Vec3T vec3Lerp(Vec3T a, Vec3T b, float t) {
Vec3T vec3Normalize(Vec3T a) {
float length = vec3Length(a);
if (length < EPSILON) {
if (length < MATH_EPSILON) {
return vec3(0.0f, 0.0f, 0.0f);
}
return vec3Scale(a, 1.0f / length);

View file

@ -29,6 +29,7 @@
#define PI 3.14159265358979323846f
#define DEGREES_TO_RADIANS(d) ((d) * (PI / 180.0f))
#define RADIANS_TO_DEGREES(r) ((r) * (180.0f / PI))
#define MATH_EPSILON 1e-6f // Below this a length is zero and a direction has none
// Right-handed, +Y up, -Z forward (glTF's convention). Matrices are column major, as the GPU
@ -65,9 +66,11 @@ Mat4T mat4Identity(void);
bool mat4Invert(Mat4T a, Mat4T *out);
Mat4T mat4LookAt(Vec3T eye, Vec3T target, Vec3T up);
Mat4T mat4Multiply(Mat4T a, Mat4T b);
Mat4T mat4NormalMatrix(Mat4T a);
Mat4T mat4Orthographic(float width, float height, float near, float far);
Mat4T mat4OrthographicBounds(float left, float right, float bottom, float top, float near, float far);
Mat4T mat4Perspective(float fovDegrees, float aspect, float near, float far);
Vec3T mat4Project(Mat4T a, Vec3T p, float *w);
Vec3T mat4TransformPoint(Mat4T a, Vec3T p);
Vec3T mat4TransformVector(Mat4T a, Vec3T v);
Mat4T mat4Transpose(Mat4T a);
@ -75,6 +78,7 @@ QuatT quatFromAxisAngle(Vec3T axis, float degrees);
QuatT quatFromEuler(float xDegrees, float yDegrees, float zDegrees);
QuatT quatFromMat4(Mat4T a);
QuatT quatIdentity(void);
QuatT quatInverse(QuatT q);
QuatT quatLookRotation(Vec3T forward, Vec3T up);
QuatT quatMultiply(QuatT a, QuatT b);
QuatT quatNormalize(QuatT q);

File diff suppressed because it is too large Load diff

View file

@ -32,7 +32,14 @@
#include <stdint.h>
#include "math3d.h"
#ifdef __cplusplus
extern "C" {
#endif
#define NAV_NO_HANDLE -1
#define NAV_MAX_PATH 256 // Corners navPath can hand back, and polygons a query may cross
#define NAV_ARRIVAL_QUEUE 64 // Arrivals held for navPollArrived between frames
#define NAV_MAX_CROWD_AGENTS 128 // Agents one mesh's crowd can steer
void navInit(void);
void navQuit(void);
@ -62,4 +69,8 @@ bool navAgentSetPlayer(int32_t agent, bool player); // Steer a pla
bool navAgentValid(int32_t agent);
int32_t navAgentGetNode(int32_t agent);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -43,11 +43,12 @@ extern "C" {
#define MAX_NAVS 8
#define MAX_AGENTS 256
#define MAX_CROWD_AGENTS 128
#define MAX_PATH_POLYS 256
#define MAX_QUERY_NODES 2048
#define MAX_STEP_SECONDS 0.1
#define MIN_AGENT_SIZE 0.01f // Radius and height are at least this
#define MAX_SLOPE_DEGREES 89.0f
#define CELLS_PER_RADIUS 3.0f // Voxel size as a fraction of the agent radius
#define CELL_HEIGHT_RATIO 0.5f // Voxel height as a fraction of its width
#define MAX_EDGE_CELLS 12
#define MAX_SIMPLIFICATION 1.3f
#define MIN_REGION_CELLS 8
@ -57,15 +58,26 @@ extern "C" {
#define DETAIL_MAX_ERROR 1.0f
#define POLY_FLAG_WALK 1
#define ARRIVE_RADII 1.5f // Within this many radii of the target counts as arrived
#define ARRIVE_SPEED 0.05f // ... when slower than this
#define ARRIVE_SPEED 0.05f // ... when slower than this ...
#define ARRIVE_SPEED_PER_RADIUS 0.5f // ... plus this much per unit of agent radius
#define AGENT_ACCELERATION 8.0f
#define AGENT_QUERY_RADII 12.0f
#define AGENT_OPTIMIZE_RADII 30.0f
#define AGENT_SEPARATION 2.0f
#define AGENT_AVOIDANCE 3 // The most careful of the four presets
#define ARRIVAL_QUEUE 64
#define CROWD_RADIUS_RATIO 2.0f // The largest agent radius a crowd plans for, in mesh agent radii
#define AVOIDANCE_PRESETS 4 // Obstacle avoidance presets, from quick to careful ...
#define AGENT_AVOIDANCE (AVOIDANCE_PRESETS - 1) // ... agents use the most careful
#define AVOID_VEL_BIAS 0.5f // The presets, as the Recast demo sets them: bias toward the wanted velocity ...
#define AVOID_ADAPTIVE_DIVS 5 // ... samples per ring ...
#define AVOID_BASE_RINGS 2 // ... rings and depth, one more of each per preset
#define AVOID_BASE_DEPTH 1
#define NEAREST_EXTENT_XZ 2.0f // Half-size of the box a nearest-polygon search covers
#define NEAREST_EXTENT_Y 4.0f
#define MAX_CELLS (4096 * 4096) // Heightfield cells a bake may cover
static_assert(AVOIDANCE_PRESETS <= DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS, "more avoidance presets than the crowd holds");
static_assert(sizeof(Vec3T) == 3 * sizeof(float), "Vec3T must be three packed floats for Detour to write into");
typedef struct NavRecordS {
dtNavMesh *mesh;
@ -95,23 +107,25 @@ typedef struct AgentRecordS {
} AgentRecordT;
static AgentRecordT *_agentOf(int32_t agent);
static bool _buildTile(NavRecordT *nav);
static void _freeNav(NavRecordT *nav);
static bool _gather(NavRecordT *nav, int32_t node, Vec3T parentPosition, QuatT parentRotation, Vec3T parentScale);
static bool _gather(NavRecordT *nav, int32_t node);
static bool _initMesh(NavRecordT *nav, unsigned char *data, int32_t size);
static void _placeAgent(AgentRecordT *agent, const dtCrowdAgent *crowdAgent, float dt);
static dtPolyRef _nearestPoly(NavRecordT *nav, Vec3T point, float *out);
static NavRecordT *_navOf(int32_t nav);
static AgentRecordT *_agentOf(int32_t agent);
static NavRecordT *_navRecord(int32_t nav);
static dtPolyRef _nearestPoly(NavRecordT *nav, Vec3T point, float *out);
static void _placeAgent(AgentRecordT *agent, const dtCrowdAgent *crowdAgent);
static void _releaseDetour(NavRecordT *nav);
static NavRecordT _navs[MAX_NAVS];
static AgentRecordT _agents[MAX_AGENTS];
static int32_t _arrivals[ARRIVAL_QUEUE];
static int32_t _arrivals[NAV_ARRIVAL_QUEUE];
static int32_t _arrivalCount = 0;
static uint64_t _lastTick = 0;
static dtQueryFilter _filter;
static const float _extents[3] = { 2.0f, 4.0f, 2.0f };
static const float _extents[3] = { NEAREST_EXTENT_XZ, NEAREST_EXTENT_Y, NEAREST_EXTENT_XZ };
// ===== Internal helpers =====
@ -146,12 +160,12 @@ static bool _buildTile(NavRecordT *nav) {
}
memset(&config, 0, sizeof(config));
config.cs = nav->radius / CELLS_PER_RADIUS;
config.ch = config.cs * 0.5f;
config.ch = config.cs * CELL_HEIGHT_RATIO;
config.walkableSlopeAngle = nav->slope;
config.walkableHeight = (int)ceilf(nav->height / config.ch);
config.walkableClimb = (int)floorf(nav->step / config.ch);
config.walkableRadius = (int)ceilf(nav->radius / config.cs);
config.maxEdgeLen = (int)(MAX_EDGE_CELLS / config.cs * config.cs) > 0 ? MAX_EDGE_CELLS : MAX_EDGE_CELLS;
config.walkableHeight = (int32_t)ceilf(nav->height / config.ch);
config.walkableClimb = (int32_t)floorf(nav->step / config.ch);
config.walkableRadius = (int32_t)ceilf(nav->radius / config.cs);
config.maxEdgeLen = MAX_EDGE_CELLS;
config.maxSimplificationError = MAX_SIMPLIFICATION;
config.minRegionArea = MIN_REGION_CELLS * MIN_REGION_CELLS;
config.mergeRegionArea = MERGE_REGION_CELLS * MERGE_REGION_CELLS;
@ -228,9 +242,6 @@ static bool _buildTile(NavRecordT *nav) {
goto done;
}
ok = _initMesh(nav, navData, navDataSize);
if (!ok) {
dtFree(navData);
}
done:
rcFreePolyMeshDetail(detail);
rcFreePolyMesh(polyMesh);
@ -250,15 +261,7 @@ static void _freeNav(NavRecordT *nav) {
memset(&_agents[x], 0, sizeof(_agents[x]));
}
}
if (nav->crowd != nullptr) {
dtFreeCrowd(nav->crowd);
}
if (nav->query != nullptr) {
dtFreeNavMeshQuery(nav->query);
}
if (nav->mesh != nullptr) {
dtFreeNavMesh(nav->mesh);
}
_releaseDetour(nav);
SDL_free(nav->vertices);
SDL_free(nav->triangles);
memset(nav, 0, sizeof(*nav));
@ -266,7 +269,7 @@ static void _freeNav(NavRecordT *nav) {
// A node's mesh in world space, and its children's, appended to the bake.
static bool _gather(NavRecordT *nav, int32_t node, Vec3T parentPosition, QuatT parentRotation, Vec3T parentScale) {
static bool _gather(NavRecordT *nav, int32_t node) {
const float *positions;
const uint32_t *indices;
int32_t vertexCount;
@ -277,9 +280,6 @@ static bool _gather(NavRecordT *nav, int32_t node, Vec3T parentPosition, QuatT p
QuatT rotation;
Vec3T scale;
(void)parentPosition;
(void)parentRotation;
(void)parentScale;
if (!nodeGetWorldTransform(node, &position, &rotation, &scale)) {
return false;
}
@ -308,40 +308,48 @@ static bool _gather(NavRecordT *nav, int32_t node, Vec3T parentPosition, QuatT p
nav->vertexCount += vertexCount;
}
for (x = 0; x < nodeGetChildCount(node); x++) {
_gather(nav, nodeGetChild(node, x), position, rotation, scale);
_gather(nav, nodeGetChild(node, x));
}
return true;
}
// Detour and the crowd over baked tile data (which the mesh then owns).
// Detour and the crowd over baked tile data. The data is taken either way: the mesh owns it on
// success (DT_TILE_FREE_DATA) and it is freed on failure, along with whatever was set up so far, so
// the record is left as it was found.
static bool _initMesh(NavRecordT *nav, unsigned char *data, int32_t size) {
int32_t x;
nav->mesh = dtAllocNavMesh();
if ((nav->mesh == nullptr) || dtStatusFailed(nav->mesh->init(data, size, DT_TILE_FREE_DATA))) {
// init only hands the data to the mesh once its tile is in place, so on failure it is
// still ours.
utilTrace("Nav: mesh init failed.");
dtFree(data);
_releaseDetour(nav);
return false;
}
nav->query = dtAllocNavMeshQuery();
if ((nav->query == nullptr) || dtStatusFailed(nav->query->init(nav->mesh, MAX_QUERY_NODES))) {
utilTrace("Nav: query init failed.");
_releaseDetour(nav);
return false;
}
nav->crowd = dtAllocCrowd();
if ((nav->crowd == nullptr) || !nav->crowd->init(MAX_CROWD_AGENTS, nav->radius * 2.0f, nav->mesh)) {
if ((nav->crowd == nullptr) || !nav->crowd->init(NAV_MAX_CROWD_AGENTS, nav->radius * CROWD_RADIUS_RATIO, nav->mesh)) {
utilTrace("Nav: crowd init failed.");
_releaseDetour(nav);
return false;
}
// The obstacle avoidance presets, from quick to careful, as the Recast demo sets them.
for (x = 0; x < 4; x++) {
for (x = 0; x < AVOIDANCE_PRESETS; x++) {
dtObstacleAvoidanceParams params;
memcpy(&params, nav->crowd->getObstacleAvoidanceParams(0), sizeof(params));
params.velBias = 0.5f;
params.adaptiveDivs = 5;
params.adaptiveRings = 2 + x;
params.adaptiveDepth = 1 + x;
params.velBias = AVOID_VEL_BIAS;
params.adaptiveDivs = AVOID_ADAPTIVE_DIVS;
params.adaptiveRings = (unsigned char)(AVOID_BASE_RINGS + x);
params.adaptiveDepth = (unsigned char)(AVOID_BASE_DEPTH + x);
nav->crowd->setObstacleAvoidanceParams(x, &params);
}
nav->built = true;
@ -349,8 +357,17 @@ static bool _initMesh(NavRecordT *nav, unsigned char *data, int32_t size) {
}
// A baked mesh's record, or NULL.
static NavRecordT *_navOf(int32_t nav) {
if ((nav < 0) || (nav >= MAX_NAVS) || !_navs[nav].used || !_navs[nav].built) {
NavRecordT *record = _navRecord(nav);
return ((record != nullptr) && record->built) ? record : nullptr;
}
// A mesh's record, baked or not, or NULL.
static NavRecordT *_navRecord(int32_t nav) {
if ((nav < 0) || (nav >= MAX_NAVS) || !_navs[nav].used) {
return nullptr;
}
return &_navs[nav];
@ -371,12 +388,11 @@ static dtPolyRef _nearestPoly(NavRecordT *nav, Vec3T point, float *out) {
// Where the crowd put an agent goes to its node (in its parent's frame, turned to face its way),
// or, for a player, becomes the velocity the controller walks with and the controller's position
// comes back into the crowd.
static void _placeAgent(AgentRecordT *agent, const dtCrowdAgent *crowdAgent, float dt) {
static void _placeAgent(AgentRecordT *agent, const dtCrowdAgent *crowdAgent) {
Vec3T velocity = vec3(crowdAgent->vel[0], crowdAgent->vel[1], crowdAgent->vel[2]);
Vec3T world = vec3(crowdAgent->npos[0], crowdAgent->npos[1], crowdAgent->npos[2]);
float speed = sqrtf(velocity.x * velocity.x + velocity.z * velocity.z);
(void)dt;
if (!nodeValid(agent->node)) {
return;
}
@ -390,12 +406,7 @@ static void _placeAgent(AgentRecordT *agent, const dtCrowdAgent *crowdAgent, flo
Vec3T parentScale;
if ((parent >= 0) && nodeGetWorldTransform(parent, &parentPosition, &parentRotation, &parentScale)) {
QuatT inverse = parentRotation;
inverse.x = -inverse.x;
inverse.y = -inverse.y;
inverse.z = -inverse.z;
local = quatRotate(inverse, vec3Subtract(world, parentPosition));
local = quatRotate(quatInverse(parentRotation), vec3Subtract(world, parentPosition));
local = vec3((parentScale.x != 0.0f) ? local.x / parentScale.x : local.x, (parentScale.y != 0.0f) ? local.y / parentScale.y : local.y, (parentScale.z != 0.0f) ? local.z / parentScale.z : local.z);
}
nodeSetPosition(agent->node, local);
@ -407,17 +418,29 @@ static void _placeAgent(AgentRecordT *agent, const dtCrowdAgent *crowdAgent, flo
}
// Detour's objects, in the reverse of the order they were made (all three tolerate NULL).
static void _releaseDetour(NavRecordT *nav) {
dtFreeCrowd(nav->crowd);
dtFreeNavMeshQuery(nav->query);
dtFreeNavMesh(nav->mesh);
nav->crowd = nullptr;
nav->query = nullptr;
nav->mesh = nullptr;
nav->built = false;
}
// ===== Public =====
bool navAddNode(int32_t nav, int32_t node) {
NavRecordT *record = ((nav >= 0) && (nav < MAX_NAVS) && _navs[nav].used) ? &_navs[nav] : nullptr;
NavRecordT *record = _navRecord(nav);
if ((record == nullptr) || record->built || !nodeValid(node)) {
return false;
}
// Nodes placed this frame have no world matrix until the scene walks them.
sceneUpdateTransforms();
return _gather(record, node, vec3(0.0f, 0.0f, 0.0f), quatIdentity(), vec3(1.0f, 1.0f, 1.0f));
return _gather(record, node);
}
@ -575,7 +598,7 @@ bool navAgentValid(int32_t agent) {
bool navBuild(int32_t nav) {
NavRecordT *record = ((nav >= 0) && (nav < MAX_NAVS) && _navs[nav].used) ? &_navs[nav] : nullptr;
NavRecordT *record = _navRecord(nav);
uint64_t start = SDL_GetTicksNS();
bool ok;
@ -597,10 +620,12 @@ bool navBuild(int32_t nav) {
bool navDelete(int32_t nav) {
if ((nav < 0) || (nav >= MAX_NAVS) || !_navs[nav].used) {
NavRecordT *record = _navRecord(nav);
if (record == nullptr) {
return false;
}
_freeNav(&_navs[nav]);
_freeNav(record);
return true;
}
@ -608,6 +633,7 @@ bool navDelete(int32_t nav) {
// The baked mesh's polygons as triangle fans, for drawing.
int32_t navGetPolygons(int32_t nav, Vec3T *vertices, int32_t max) {
NavRecordT *record = _navOf(nav);
const dtNavMesh *mesh;
int32_t count = 0;
int32_t t;
int32_t p;
@ -616,8 +642,7 @@ int32_t navGetPolygons(int32_t nav, Vec3T *vertices, int32_t max) {
if (record == nullptr) {
return 0;
}
const dtNavMesh *mesh = record->mesh;
mesh = record->mesh;
for (t = 0; t < mesh->getMaxTiles(); t++) {
const dtMeshTile *tile = mesh->getTile(t);
@ -658,20 +683,27 @@ void navInit(void) {
}
// A baked mesh from navSave's bytes (copied).
// A baked mesh from navSave's bytes (copied). Detour reads the tile header before it checks
// anything, so the bytes are checked for a whole header with its magic and version first.
int32_t navLoad(const void *data, size_t size, float agentRadius, float agentHeight) {
int32_t x;
unsigned char *copy;
dtMeshHeader header;
for (x = 0; x < MAX_NAVS; x++) {
if (!_navs[x].used) {
break;
}
}
if ((x == MAX_NAVS) || (data == nullptr) || (size == 0)) {
if ((x == MAX_NAVS) || (data == nullptr) || (size < sizeof(header))) {
return NAV_NO_HANDLE;
}
copy = (unsigned char *)dtAlloc((size_t)size, DT_ALLOC_PERM);
memcpy(&header, data, sizeof(header));
if ((header.magic != DT_NAVMESH_MAGIC) || (header.version != DT_NAVMESH_VERSION)) {
utilTrace("Nav: not a navigation mesh file.");
return NAV_NO_HANDLE;
}
copy = (unsigned char *)dtAlloc(size, DT_ALLOC_PERM);
if (copy == nullptr) {
return NAV_NO_HANDLE;
}
@ -681,7 +713,6 @@ int32_t navLoad(const void *data, size_t size, float agentRadius, float agentHei
_navs[x].height = agentHeight;
_navs[x].used = true;
if (!_initMesh(&_navs[x], copy, (int32_t)size)) {
dtFree(copy);
_freeNav(&_navs[x]);
return NAV_NO_HANDLE;
}
@ -713,27 +744,27 @@ int32_t navNew(float agentRadius, float agentHeight, float maxSlopeDegrees, floa
return NAV_NO_HANDLE;
}
memset(&_navs[x], 0, sizeof(_navs[x]));
_navs[x].radius = SDL_max(agentRadius, 0.01f);
_navs[x].height = SDL_max(agentHeight, 0.01f);
_navs[x].slope = SDL_clamp(maxSlopeDegrees, 0.0f, 89.0f);
_navs[x].radius = SDL_max(agentRadius, MIN_AGENT_SIZE);
_navs[x].height = SDL_max(agentHeight, MIN_AGENT_SIZE);
_navs[x].slope = SDL_clamp(maxSlopeDegrees, 0.0f, MAX_SLOPE_DEGREES);
_navs[x].step = SDL_max(maxStep, 0.0f);
_navs[x].used = true;
return x;
}
// The corners of the shortest path, from (or the nearest walkable point) to to.
// The corners of the shortest path, from (or the nearest walkable point) to to. When to cannot
// be reached the path ends at the nearest point on the last polygon it did reach.
int32_t navPath(int32_t nav, Vec3T from, Vec3T to, Vec3T *points, int32_t max) {
NavRecordT *record = _navOf(nav);
float start[3];
float end[3];
dtPolyRef startRef;
dtPolyRef endRef;
dtPolyRef polys[MAX_PATH_POLYS];
dtPolyRef polys[NAV_MAX_PATH];
int32_t polyCount = 0;
float *straight;
int32_t straightCount = 0;
int32_t x;
dtStatus status;
if ((record == nullptr) || (max <= 0)) {
return -1;
@ -743,21 +774,21 @@ int32_t navPath(int32_t nav, Vec3T from, Vec3T to, Vec3T *points, int32_t max) {
if ((startRef == 0) || (endRef == 0)) {
return -1;
}
if (dtStatusFailed(record->query->findPath(startRef, endRef, start, end, &_filter, polys, &polyCount, MAX_PATH_POLYS)) || (polyCount == 0)) {
status = record->query->findPath(startRef, endRef, start, end, &_filter, polys, &polyCount, NAV_MAX_PATH);
if (dtStatusFailed(status) || (polyCount == 0)) {
return -1;
}
straight = (float *)SDL_malloc(sizeof(float) * 3 * (size_t)max);
if (straight == nullptr) {
utilDie("Out of memory finding a path.");
if (dtStatusDetail(status, DT_PARTIAL_RESULT)) {
float reachable[3];
if (dtStatusSucceed(record->query->closestPointOnPoly(polys[polyCount - 1], end, reachable, nullptr))) {
rcVcopy(end, reachable);
}
if (dtStatusFailed(record->query->findStraightPath(start, end, polys, polyCount, straight, nullptr, nullptr, &straightCount, max))) {
SDL_free(straight);
}
// Vec3T is three packed floats, so Detour writes the corners straight into the caller's array.
if (dtStatusFailed(record->query->findStraightPath(start, end, polys, polyCount, reinterpret_cast<float *>(points), nullptr, nullptr, &straightCount, max))) {
return -1;
}
for (x = 0; x < straightCount; x++) {
points[x] = vec3(straight[x * 3], straight[x * 3 + 1], straight[x * 3 + 2]);
}
SDL_free(straight);
return straightCount;
}
@ -803,7 +834,7 @@ bool navRaycast(int32_t nav, Vec3T from, Vec3T to, Vec3T *hit) {
float end[3] = { to.x, to.y, to.z };
float t = 0.0f;
float normal[3];
dtPolyRef polys[MAX_PATH_POLYS];
dtPolyRef polys[NAV_MAX_PATH];
int32_t polyCount = 0;
dtPolyRef startRef;
@ -811,7 +842,7 @@ bool navRaycast(int32_t nav, Vec3T from, Vec3T to, Vec3T *hit) {
return false;
}
startRef = _nearestPoly(record, from, start);
if ((startRef == 0) || dtStatusFailed(record->query->raycast(startRef, start, end, &_filter, &t, normal, polys, &polyCount, MAX_PATH_POLYS))) {
if ((startRef == 0) || dtStatusFailed(record->query->raycast(startRef, start, end, &_filter, &t, normal, polys, &polyCount, NAV_MAX_PATH))) {
return false;
}
if (t >= 1.0f) {
@ -893,16 +924,16 @@ void navUpdate(bool advance) {
if ((crowdAgent == nullptr) || !crowdAgent->active) {
continue;
}
_placeAgent(agent, crowdAgent, (float)dt);
_placeAgent(agent, crowdAgent);
if (agent->moving && (crowdAgent->targetState == DT_CROWDAGENT_TARGET_VALID)) {
float dx = crowdAgent->targetPos[0] - crowdAgent->npos[0];
float dz = crowdAgent->targetPos[2] - crowdAgent->npos[2];
float speed = sqrtf(crowdAgent->vel[0] * crowdAgent->vel[0] + crowdAgent->vel[2] * crowdAgent->vel[2]);
if ((sqrtf(dx * dx + dz * dz) < agent->radius * ARRIVE_RADII) && (speed < agent->radius * ARRIVE_SPEED * 10.0f + ARRIVE_SPEED)) {
if ((sqrtf(dx * dx + dz * dz) < agent->radius * ARRIVE_RADII) && (speed < agent->radius * ARRIVE_SPEED_PER_RADIUS + ARRIVE_SPEED)) {
agent->moving = false;
agent->arrived = true;
if (_arrivalCount < ARRIVAL_QUEUE) {
if (_arrivalCount < NAV_ARRIVAL_QUEUE) {
_arrivals[_arrivalCount++] = x;
}
}
@ -912,5 +943,5 @@ void navUpdate(bool advance) {
bool navValid(int32_t nav) {
return (nav >= 0) && (nav < MAX_NAVS) && _navs[nav].used;
return _navRecord(nav) != nullptr;
}

View file

@ -20,9 +20,8 @@
*
*/
/*
* Packer, unpacker, and patcher for single-file games. The database layout is described in vfs.c.
*/
// Packer, unpacker, and patcher for single-file games. The database layout is described in vfs.c;
// the format constants it shares with the engine live in vfs.h.
#include <ctype.h>
#include <dirent.h>
@ -44,10 +43,10 @@
#include "vfs.h"
#define ENGINE_PREFIX "singe/"
#define GAMES_DAT "games.dat"
#define MAIN_SCHEMA "main"
#define PAGE_SIZE 4096
#define PATCH_EXTENSION "patch"
#define PATCH_SCHEMA "patch"
typedef struct EntryS {
@ -75,16 +74,21 @@ static const char *const _textExtensions[] = { "singe", "lua", "dat", "cfg", "tx
static bool _applyDatabase(WriterT *writer, const char *source);
static bool _applyDirectory(WriterT *writer, const char *directory, bool checkGame);
static bool _attachPatch(sqlite3 *db, const char *source);
static bool _collect(const char *root, const char *relative, EntryT **list);
static bool _createTables(sqlite3 *db);
static void _entriesFree(EntryT *list);
static bool _exec(sqlite3 *db, const char *sql);
static bool _insertFile(WriterT *writer, const EntryT *entry);
static bool _isDirectory(const char *path);
static bool _isEngineKey(const char *key);
static bool _isPatchName(const char *database);
static bool _isSafeName(const char *name);
static char *_keyFor(const char *relative);
static int64_t _readMetaInt(sqlite3 *db, const char *schema, const char *key, int64_t fallback);
static bool _scanEscapes(const EntryT *list);
static bool _validateGamesDat(WriterT *writer);
static bool _writeBlob(FILE *file, const void *data, size_t bytes);
static bool _writeMeta(sqlite3 *db, const char *key, const char *value);
static bool _writerClose(WriterT *writer, bool commit);
static bool _writerOpen(WriterT *writer, const char *database, bool create);
@ -93,21 +97,13 @@ static bool _writerOpen(WriterT *writer, const char *database, bool create);
// Copies every asset row of a patch database into the open game, replacing what it names.
static bool _applyDatabase(WriterT *writer, const char *source) {
sqlite3_stmt *stmt = NULL;
char *sql = utilCreateString("ATTACH DATABASE '%s' AS patch", source);
int64_t chunk = 0;
bool ok = true;
if (!_exec(writer->db, sql)) {
free(sql);
if (!_attachPatch(writer->db, source)) {
return false;
}
free(sql);
if (sqlite3_prepare_v2(writer->db, "SELECT value FROM patch.meta WHERE key = 'chunk'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
chunk = sqlite3_column_int64(stmt, 0);
}
sqlite3_finalize(stmt);
}
chunk = _readMetaInt(writer->db, PATCH_SCHEMA, VFS_META_CHUNK, 0);
if (chunk != writer->chunkBytes) {
utilSay("!!! %s uses a different chunk size than the game.", source);
return false;
@ -115,13 +111,6 @@ static bool _applyDatabase(WriterT *writer, const char *source) {
ok = ok && _exec(writer->db, "DELETE FROM main.chunks WHERE path IN (SELECT path FROM patch.assets)");
ok = ok && _exec(writer->db, "INSERT OR REPLACE INTO main.assets (path, name, size, data) SELECT path, name, size, data FROM patch.assets");
ok = ok && _exec(writer->db, "INSERT OR REPLACE INTO main.chunks (path, chunk, data) SELECT path, chunk, data FROM patch.chunks");
if (ok && (sqlite3_prepare_v2(writer->db, "SELECT count(*) FROM patch.sqlite_master WHERE name = 'removed'", -1, &stmt, NULL) == SQLITE_OK)) {
if ((sqlite3_step(stmt) == SQLITE_ROW) && (sqlite3_column_int64(stmt, 0) > 0)) {
ok = ok && _exec(writer->db, "DELETE FROM main.chunks WHERE path IN (SELECT path FROM patch.removed)");
ok = ok && _exec(writer->db, "DELETE FROM main.assets WHERE path IN (SELECT path FROM patch.removed)");
}
sqlite3_finalize(stmt);
}
if (ok && (sqlite3_prepare_v2(writer->db, "SELECT count(*), coalesce(sum(size), 0) FROM patch.assets", -1, &stmt, NULL) == SQLITE_OK)) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
writer->files = sqlite3_column_int64(stmt, 0);
@ -135,7 +124,7 @@ static bool _applyDatabase(WriterT *writer, const char *source) {
// Inserts every file below directory. checkGame enforces the rules a whole game must meet; the
// forbidden-file list and the escape scan apply to patches too.
// forbidden-file list and the escape scan apply to patches too. Every offending file is reported.
static bool _applyDirectory(WriterT *writer, const char *directory, bool checkGame) {
EntryT *list = NULL;
EntryT *entry = NULL;
@ -144,12 +133,13 @@ static bool _applyDirectory(WriterT *writer, const char *directory, bool checkGa
const char *slash = NULL;
size_t length = 0;
bool ok = true;
bool clash = false;
if (!_collect(directory, "", &list)) {
return false;
}
gameDir = strdup(utilGetLastPathComponent(directory));
for (entry = list; ok && (entry != NULL); entry = entry->next) {
for (entry = list; entry != NULL; entry = entry->next) {
reason = packForbiddenReason(entry->relative, !checkGame);
if (reason != NULL) {
utilSay("!!! %s has %s: %s", directory, reason, entry->relative);
@ -157,11 +147,12 @@ static bool _applyDirectory(WriterT *writer, const char *directory, bool checkGa
ok = false;
}
// A top level entry named like the game directory would make the own-directory prefix ambiguous.
if (ok && checkGame) {
if (checkGame && !clash) {
slash = strchr(entry->relative, '/');
length = slash ? (size_t)(slash - entry->relative) : strlen(entry->relative);
if ((length == strlen(gameDir)) && (strncasecmp(entry->relative, gameDir, length) == 0)) {
utilSay("!!! %s contains an entry named like the game directory (%s); rename one of them.", directory, gameDir);
clash = true;
ok = false;
}
}
@ -179,6 +170,24 @@ static bool _applyDirectory(WriterT *writer, const char *directory, bool checkGa
}
// ATTACH takes the file name as a bound parameter, so a quote in the path is just a quote.
static bool _attachPatch(sqlite3 *db, const char *source) {
sqlite3_stmt *stmt = NULL;
bool ok = false;
if (sqlite3_prepare_v2(db, "ATTACH DATABASE ? AS " PATCH_SCHEMA, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, source, -1, SQLITE_STATIC);
ok = (sqlite3_step(stmt) == SQLITE_DONE);
}
if (!ok) {
utilSay("!!! SQLite: %s", sqlite3_errmsg(db));
}
sqlite3_finalize(stmt);
return ok;
}
// Recursive directory walk producing relative names with forward slashes. Stale index files are skipped.
static bool _collect(const char *root, const char *relative, EntryT **list) {
DIR *dir = NULL;
@ -213,6 +222,9 @@ static bool _collect(const char *root, const char *relative, EntryT **list) {
utilSay(">>> Skipping stale index %s", child);
} else {
entry = (EntryT *)calloc(1, sizeof(EntryT));
if (entry == NULL) {
utilDie("Out of memory collecting %s.", full);
}
entry->relative = strdup(child);
entry->full = strdup(full);
entry->size = (int64_t)info.st_size;
@ -266,12 +278,14 @@ static bool _exec(sqlite3 *db, const char *sql) {
}
// One file becomes one assets row, plus chunk rows when it is larger than the chunk size.
// One file becomes one assets row, plus chunk rows when it is larger than the chunk size. The size
// recorded is the size stored, so a file that changed under the packer is refused, not misdescribed.
static bool _insertFile(WriterT *writer, const EntryT *entry) {
FILE *file = NULL;
uint8_t *buffer = NULL;
char *key = _keyFor(entry->relative);
size_t got = 0;
int64_t stored = 0;
int64_t index = 0;
bool ok = true;
@ -282,6 +296,9 @@ static bool _insertFile(WriterT *writer, const EntryT *entry) {
return false;
}
buffer = (uint8_t *)malloc((size_t)writer->chunkBytes);
if (buffer == NULL) {
utilDie("Out of memory packing %s.", entry->full);
}
sqlite3_reset(writer->deleteChunkStmt);
sqlite3_bind_text(writer->deleteChunkStmt, 1, key, -1, SQLITE_STATIC);
sqlite3_step(writer->deleteChunkStmt);
@ -291,6 +308,7 @@ static bool _insertFile(WriterT *writer, const EntryT *entry) {
sqlite3_bind_int64(writer->assetStmt, 3, entry->size);
if (entry->size <= writer->chunkBytes) {
got = fread(buffer, 1, (size_t)entry->size, file);
stored = (int64_t)got;
sqlite3_bind_blob(writer->assetStmt, 4, buffer, (int)got, SQLITE_STATIC);
} else {
sqlite3_bind_null(writer->assetStmt, 4);
@ -305,11 +323,15 @@ static bool _insertFile(WriterT *writer, const EntryT *entry) {
sqlite3_bind_blob(writer->chunkStmt, 3, buffer, (int)got, SQLITE_STATIC);
ok = (sqlite3_step(writer->chunkStmt) == SQLITE_DONE);
sqlite3_reset(writer->chunkStmt);
stored += (int64_t)got;
index++;
}
}
if (!ok) {
utilSay("!!! SQLite: %s", sqlite3_errmsg(writer->db));
} else if (ferror(file) || (stored != entry->size)) {
utilSay("!!! %s changed while it was being packed (%" PRId64 " of %" PRId64 " bytes read).", entry->full, stored, entry->size);
ok = false;
}
writer->files++;
writer->bytes += entry->size;
@ -328,17 +350,52 @@ static bool _isDirectory(const char *path) {
}
// A key below the engine's own directory, which a game never carries.
static bool _isEngineKey(const char *key) {
size_t length = strlen(VFS_ENGINE_DIRECTORY);
return (strncasecmp(key, VFS_ENGINE_DIRECTORY, length) == 0) && (key[length] == '/');
}
// Patch databases are .patch files: packed like a game but without a games.dat, and never run.
static bool _isPatchName(const char *database) {
return utilStricmp(utilGetFileExtension(database), PATCH_EXTENSION) == 0;
}
// A name from a database may only land below the unpack directory: relative, no drive letter,
// no ".." component, no empty component.
static bool _isSafeName(const char *name) {
const char *p = name;
size_t piece = 0;
if ((name[0] == 0) || (name[0] == '/') || (name[0] == '\\') || (isalpha((unsigned char)name[0]) && (name[1] == ':'))) {
return false;
}
while (*p != 0) {
piece = strcspn(p, "/\\");
if ((piece == 0) || ((piece == 2) && (p[0] == '.') && (p[1] == '.'))) {
return false;
}
p += piece;
if (*p != 0) {
p++;
}
}
return true;
}
// The lookup key: lower case, forward slashes.
static char *_keyFor(const char *relative) {
char *key = strdup(relative);
char *p = NULL;
if (key == NULL) {
utilDie("Out of memory packing %s.", relative);
}
for (p = key; *p != 0; p++) {
*p = (*p == '\\') ? '/' : (char)tolower((unsigned char)*p);
}
@ -347,13 +404,27 @@ static char *_keyFor(const char *relative) {
}
// An integer meta value, or fallback when the key is absent.
static int64_t _readMetaInt(sqlite3 *db, const char *schema, const char *key, int64_t fallback) {
char *value = vfsReadMeta(db, schema, key);
int64_t result = fallback;
if (value != NULL) {
result = strtoll(value, NULL, 10);
free(value);
}
return result;
}
// A packed game may only reach its own contents: ".." in a script or data file is refused, by line.
static bool _scanEscapes(const EntryT *list) {
const EntryT *entry = NULL;
const char *ext = NULL;
const char *offset = NULL;
const char *hit = NULL;
const char *p = NULL;
char *data = NULL;
char *line = NULL;
size_t bytes = 0;
int32_t number = 0;
int32_t x = 0;
@ -373,15 +444,20 @@ static bool _scanEscapes(const EntryT *list) {
if (data == NULL) {
continue;
}
offset = data;
number = 0;
while ((line = utilReadLine(data, bytes, &offset)) != NULL) {
// One search over the whole file; the line number is only counted up when there is a hit.
number = 1;
p = data;
for (hit = strstr(p, ".."); hit != NULL; hit = strstr(p, "..")) {
if ((hit[2] == '/') || (hit[2] == '\\')) {
for (; p < hit; p++) {
if (*p == '\n') {
number++;
if ((strstr(line, "../") != NULL) || (strstr(line, "..\\") != NULL)) {
}
}
utilSay("!!! %s:%d reaches outside the game with \"..\"", entry->relative, number);
ok = false;
}
free(line);
p = hit + 2;
}
free(data);
}
@ -397,6 +473,7 @@ static bool _validateGamesDat(WriterT *writer) {
sqlite3_stmt *stmt = NULL;
lua_State *L = NULL;
char *gameDir = NULL;
char *meta = NULL;
char *key = NULL;
const char *value = NULL;
const char *title = NULL;
@ -409,26 +486,25 @@ static bool _validateGamesDat(WriterT *writer) {
if ((sqlite3_prepare_v2(writer->db, "SELECT data FROM assets WHERE path = ?", -1, &stmt, NULL) != SQLITE_OK)) {
return false;
}
sqlite3_bind_text(stmt, 1, GAMES_DAT, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 1, VFS_GAMES_DAT, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) {
sqlite3_finalize(stmt);
utilSay("!!! No %s at the root of the game.", GAMES_DAT);
utilSay("!!! No %s at the root of the game.", VFS_GAMES_DAT);
return false;
}
L = luaL_newstate();
luaL_openlibs(L);
if ((luaL_loadbuffer(L, sqlite3_column_blob(stmt, 0), (size_t)sqlite3_column_bytes(stmt, 0), GAMES_DAT) != LUA_OK) || (lua_pcall(L, 0, 0, 0) != LUA_OK)) {
if ((luaL_loadbuffer(L, sqlite3_column_blob(stmt, 0), (size_t)sqlite3_column_bytes(stmt, 0), VFS_GAMES_DAT) != LUA_OK) || (lua_pcall(L, 0, 0, 0) != LUA_OK)) {
utilSay("!!! %s", lua_tostring(L, -1));
sqlite3_finalize(stmt);
lua_close(L);
return false;
}
sqlite3_finalize(stmt);
if (sqlite3_prepare_v2(writer->db, "SELECT value FROM meta WHERE key = 'gamedir'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
gameDir = _keyFor((const char *)sqlite3_column_text(stmt, 0));
}
sqlite3_finalize(stmt);
meta = vfsReadMeta(writer->db, MAIN_SCHEMA, VFS_META_GAMEDIR);
if (meta != NULL) {
gameDir = _keyFor(meta);
free(meta);
}
if (sqlite3_prepare_v2(writer->db, "SELECT count(*) FROM assets WHERE path = ?", -1, &stmt, NULL) != SQLITE_OK) {
free(gameDir);
@ -438,7 +514,7 @@ static bool _validateGamesDat(WriterT *writer) {
lua_getglobal(L, "GAMES");
if (!lua_istable(L, -1)) {
utilSay("!!! %s defines no GAMES table.", GAMES_DAT);
utilSay("!!! %s defines no GAMES table.", VFS_GAMES_DAT);
ok = false;
}
lua_pushnil(L);
@ -458,17 +534,17 @@ static bool _validateGamesDat(WriterT *writer) {
inner += length + 1;
}
}
if (strncmp(key, ENGINE_PREFIX, strlen(ENGINE_PREFIX)) != 0) {
if (!_isEngineKey(key)) {
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, inner, -1, SQLITE_TRANSIENT);
if ((sqlite3_step(stmt) != SQLITE_ROW) || (sqlite3_column_int64(stmt, 0) == 0)) {
utilSay("!!! %s: \"%s\" names %s = \"%s\", which is not in the game.", GAMES_DAT, title, keys[x], value);
utilSay("!!! %s: \"%s\" names %s = \"%s\", which is not in the game.", VFS_GAMES_DAT, title, keys[x], value);
ok = false;
}
}
free(key);
} else if (strcmp(keys[x], "SCRIPT") == 0) {
utilSay("!!! %s: \"%s\" has no SCRIPT.", GAMES_DAT, title);
utilSay("!!! %s: \"%s\" has no SCRIPT.", VFS_GAMES_DAT, title);
ok = false;
}
lua_pop(L, 1);
@ -485,6 +561,11 @@ static bool _validateGamesDat(WriterT *writer) {
}
static bool _writeBlob(FILE *file, const void *data, size_t bytes) {
return fwrite(data, 1, bytes, file) == bytes;
}
static bool _writeMeta(sqlite3 *db, const char *key, const char *value) {
sqlite3_stmt *stmt = NULL;
bool ok = false;
@ -519,7 +600,6 @@ static bool _writerClose(WriterT *writer, bool commit) {
// Opens the database for writing inside one transaction; create replaces any existing file.
static bool _writerOpen(WriterT *writer, const char *database, bool create) {
sqlite3_stmt *stmt = NULL;
char *text = NULL;
bool ok = true;
@ -549,14 +629,15 @@ static bool _writerOpen(WriterT *writer, const char *database, bool create) {
ok = _createTables(writer->db);
}
if (ok && !create) {
// An existing game must be one of ours, and its chunk size rules.
if ((sqlite3_prepare_v2(writer->db, "SELECT value FROM meta WHERE key = 'chunk'", -1, &stmt, NULL) != SQLITE_OK) || (sqlite3_step(stmt) != SQLITE_ROW)) {
// An existing game must be one of ours, and its chunk size rules, but only when it is believable.
writer->chunkBytes = _readMetaInt(writer->db, MAIN_SCHEMA, VFS_META_CHUNK, 0);
if (writer->chunkBytes == 0) {
utilSay("!!! %s is not a Singe game database.", database);
ok = false;
} else {
writer->chunkBytes = sqlite3_column_int64(stmt, 0);
} else if ((writer->chunkBytes < 0) || (writer->chunkBytes > VFS_CHUNK_BYTES_MAX)) {
utilSay("!!! %s has an unusable chunk size (%" PRId64 ").", database, writer->chunkBytes);
ok = false;
}
sqlite3_finalize(stmt);
}
ok = ok && _exec(writer->db, "BEGIN");
ok = ok && (sqlite3_prepare_v2(writer->db, "INSERT OR REPLACE INTO assets (path, name, size, data) VALUES (?, ?, ?, ?)", -1, &writer->assetStmt, NULL) == SQLITE_OK);
@ -606,6 +687,7 @@ bool packGame(const char *directory, const char *database) {
char *root = strdup(directory);
char *gamesDat = NULL;
char *chunk = NULL;
char *version = NULL;
size_t length = strlen(root);
bool isPatch = _isPatchName(database);
bool ok = true;
@ -618,9 +700,9 @@ bool packGame(const char *directory, const char *database) {
free(root);
return false;
}
gamesDat = utilCreateString("%s/games.dat", root);
gamesDat = utilCreateString("%s/%s", root, VFS_GAMES_DAT);
if (!isPatch && !utilFileExists(gamesDat)) {
utilSay("!!! %s has no games.dat; a game database needs one at its root.", root);
utilSay("!!! %s has no %s; a game database needs one at its root.", root, VFS_GAMES_DAT);
free(gamesDat);
free(root);
return false;
@ -630,16 +712,18 @@ bool packGame(const char *directory, const char *database) {
free(root);
return false;
}
chunk = utilCreateString("%lld", (long long)writer.chunkBytes);
ok = ok && _writeMeta(writer.db, "version", "1");
ok = ok && _writeMeta(writer.db, "gamedir", utilGetLastPathComponent(root));
ok = ok && _writeMeta(writer.db, "chunk", chunk);
ok = ok && _writeMeta(writer.db, "packer", VERSION_STRING);
chunk = utilCreateString("%" PRId64, writer.chunkBytes);
version = utilCreateString("%d", VFS_FORMAT_VERSION);
ok = ok && _writeMeta(writer.db, VFS_META_VERSION, version);
ok = ok && _writeMeta(writer.db, VFS_META_GAMEDIR, utilGetLastPathComponent(root));
ok = ok && _writeMeta(writer.db, VFS_META_CHUNK, chunk);
ok = ok && _writeMeta(writer.db, VFS_META_PACKER, VERSION_STRING);
free(chunk);
free(version);
ok = ok && _applyDirectory(&writer, root, !isPatch);
ok = ok && (isPatch || _validateGamesDat(&writer));
if (ok) {
utilSay(">>> Packed %lld files (%lld bytes) from %s into %s", (long long)writer.files, (long long)writer.bytes, root, database);
utilSay(">>> Packed %" PRId64 " files (%" PRId64 " bytes) from %s into %s", writer.files, writer.bytes, root, database);
}
if (!_writerClose(&writer, ok)) {
ok = false;
@ -656,17 +740,16 @@ bool packGame(const char *directory, const char *database) {
// A database with our meta table, whatever its extension.
bool packIsDatabase(const char *database) {
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
char *value = NULL;
bool found = false;
if (!utilFileExists(database) || (sqlite3_open_v2(database, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK)) {
sqlite3_close(db);
return false;
}
if (sqlite3_prepare_v2(db, "SELECT value FROM meta WHERE key = 'version'", -1, &stmt, NULL) == SQLITE_OK) {
found = (sqlite3_step(stmt) == SQLITE_ROW);
}
sqlite3_finalize(stmt);
value = vfsReadMeta(db, MAIN_SCHEMA, VFS_META_VERSION);
found = (value != NULL);
free(value);
sqlite3_close(db);
return found;
@ -691,7 +774,7 @@ bool packPatch(const char *database, const char *source) {
}
ok = ok && _validateGamesDat(&writer);
if (ok) {
utilSay(">>> Patched %s with %lld files (%lld bytes) from %s", database, (long long)writer.files, (long long)writer.bytes, source);
utilSay(">>> Patched %s with %" PRId64 " files (%" PRId64 " bytes) from %s", database, writer.files, writer.bytes, source);
}
if (!_writerClose(&writer, ok)) {
ok = false;
@ -701,7 +784,8 @@ bool packPatch(const char *database, const char *source) {
}
// Writes every asset back out as files, in the author's spelling, under directory.
// Writes every asset back out as files, in the author's spelling, under directory. A name that
// would land anywhere else is refused; a short write fails the unpack.
bool packUnpack(const char *database, const char *directory) {
sqlite3 *db = NULL;
sqlite3_stmt *assets = NULL;
@ -711,6 +795,7 @@ bool packUnpack(const char *database, const char *directory) {
char *parent = NULL;
const char *name = NULL;
int64_t files = 0;
bool written = false;
bool ok = true;
if (!packIsDatabase(database)) {
@ -726,7 +811,15 @@ bool packUnpack(const char *database, const char *directory) {
ok = ok && (sqlite3_prepare_v2(db, "SELECT data FROM chunks WHERE path = ? ORDER BY chunk", -1, &chunks, NULL) == SQLITE_OK);
while (ok && (sqlite3_step(assets) == SQLITE_ROW)) {
name = (const char *)sqlite3_column_text(assets, 1);
path = utilCreateString("%s/%s", directory, name ? name : (const char *)sqlite3_column_text(assets, 0));
if (name == NULL) {
name = (const char *)sqlite3_column_text(assets, 0);
}
if ((name == NULL) || !_isSafeName(name)) {
utilSay("!!! %s holds a name that would land outside %s: %s", database, directory, name ? name : "(null)");
ok = false;
break;
}
path = utilCreateString("%s/%s", directory, name);
utilFixPathSeparators(&path, false);
parent = utilGetUpToLastPathComponent(path);
if (!utilMkDirP(parent, 0755)) {
@ -738,19 +831,24 @@ bool packUnpack(const char *database, const char *directory) {
utilSay("!!! Unable to write %s", path);
ok = false;
} else {
written = true;
if (sqlite3_column_type(assets, 2) != SQLITE_NULL) {
fwrite(sqlite3_column_blob(assets, 2), 1, (size_t)sqlite3_column_bytes(assets, 2), file);
written = _writeBlob(file, sqlite3_column_blob(assets, 2), (size_t)sqlite3_column_bytes(assets, 2));
} else {
sqlite3_reset(chunks);
sqlite3_bind_text(chunks, 1, (const char *)sqlite3_column_text(assets, 0), -1, SQLITE_TRANSIENT);
while (sqlite3_step(chunks) == SQLITE_ROW) {
fwrite(sqlite3_column_blob(chunks, 0), 1, (size_t)sqlite3_column_bytes(chunks, 0), file);
while (written && (sqlite3_step(chunks) == SQLITE_ROW)) {
written = _writeBlob(file, sqlite3_column_blob(chunks, 0), (size_t)sqlite3_column_bytes(chunks, 0));
}
}
fclose(file);
if ((fclose(file) != 0) || !written) {
utilSay("!!! Unable to write %s", path);
ok = false;
} else {
files++;
}
}
}
free(parent);
free(path);
}
@ -758,7 +856,7 @@ bool packUnpack(const char *database, const char *directory) {
sqlite3_finalize(chunks);
sqlite3_close(db);
if (ok) {
utilSay(">>> Unpacked %lld files from %s into %s", (long long)files, database, directory);
utilSay(">>> Unpacked %" PRId64 " files from %s into %s", files, database, directory);
}
return ok;

View file

@ -20,9 +20,7 @@
*
*/
/*
* Packing games into, and out of, single-file SQLite databases.
*/
// Packing games into, and out of, single-file SQLite databases.
#ifndef PACK_H
#define PACK_H

View file

@ -37,10 +37,10 @@
#define DEFAULT_MAX 1000
#define MAX_STEP_SECONDS 0.1
#define DISC_SIZE 64
#define QUEUE_MAX 256
#define MAX_TRAIL 16 // Points kept behind a particle
#define SCENE_CASTS_MAX 200 // Ray casts per emitter per step for COLLIDE_SCENE
#define CONTACT_LIFT 0.002f // A particle rests this far off what it hit
#define COLOUR_MAX 255.0f
typedef struct EmitterS {
int32_t id;
@ -77,7 +77,6 @@ typedef struct EmitterS {
float floor;
int32_t trailLength;
float trailWidth;
int32_t castNext; // Where the capped scene casts resume next step
SDL_Surface **frames;
int32_t frameCount;
uint32_t textureVersion;
@ -85,6 +84,7 @@ typedef struct EmitterS {
bool emitting;
float pending; // Fractional particles owed
Vec3T position; // 2D position, or the last known 3D origin
int32_t castNext; // Where the capped scene casts resume next step
int32_t count;
float *px;
float *py;
@ -111,13 +111,15 @@ static void _collide(EmitterT *emitter, float dt);
static EmitterT *_find(int32_t emitter);
static void _freeFrames(EmitterT *emitter);
static void _freePools(EmitterT *emitter);
static void _freeTrails(EmitterT *emitter);
static void _kill(EmitterT *emitter, int32_t index);
static Vec3T _origin(EmitterT *emitter);
static float _random(float min, float max);
static Vec3T _randomDirection(EmitterT *emitter);
static Vec3T _randomOffset(EmitterT *emitter);
static void _spawn(EmitterT *emitter, int32_t count);
static void _recordTrails(EmitterT *emitter);
static void _shiftTrails(EmitterT *emitter, Vec3T offset);
static void _spawn(EmitterT *emitter, int32_t count);
static void _step(EmitterT *emitter, float dt);
static void _view(EmitterT *emitter, EmitterViewT *view);
@ -125,7 +127,7 @@ static EmitterT *_emitters = NULL;
static int32_t _nextId = 1;
static uint64_t _lastTick = 0;
static SDL_Surface *_disc = NULL;
static int32_t _queue[QUEUE_MAX];
static int32_t _queue[PARTICLES_QUEUE_MAX];
static int32_t _queueCount = 0;
@ -156,10 +158,7 @@ static void _allocatePools(EmitterT *emitter, int32_t max) {
// Trail storage for the pool, sized to the trail length (none when there are no trails).
static void _allocateTrails(EmitterT *emitter) {
SDL_free(emitter->trail);
SDL_free(emitter->trailCount);
emitter->trail = NULL;
emitter->trailCount = NULL;
_freeTrails(emitter);
if (emitter->trailLength > 0) {
emitter->trail = SDL_calloc((size_t)emitter->max * (size_t)emitter->trailLength * 3, sizeof(float));
emitter->trailCount = SDL_calloc((size_t)emitter->max, sizeof(int32_t));
@ -232,7 +231,7 @@ static void _collide(EmitterT *emitter, float dt) {
emitter->vy[i] = velocity.y;
emitter->vz[i] = velocity.z;
}
emitter->castNext = (emitter->castNext + casts) % SDL_max(emitter->count, 1);
emitter->castNext = (emitter->castNext + n) % emitter->count;
}
@ -271,10 +270,6 @@ static void _freePools(EmitterT *emitter) {
SDL_free(emitter->spin);
SDL_free(emitter->frame);
SDL_free(emitter->views);
SDL_free(emitter->trail);
SDL_free(emitter->trailCount);
emitter->trail = NULL;
emitter->trailCount = NULL;
emitter->px = NULL;
emitter->py = NULL;
emitter->pz = NULL;
@ -293,6 +288,14 @@ static void _freePools(EmitterT *emitter) {
}
static void _freeTrails(EmitterT *emitter) {
SDL_free(emitter->trail);
SDL_free(emitter->trailCount);
emitter->trail = NULL;
emitter->trailCount = NULL;
}
// Removes particle index by moving the last live one into its slot.
static void _kill(EmitterT *emitter, int32_t index) {
int32_t last = emitter->count - 1;
@ -384,6 +387,49 @@ static Vec3T _randomOffset(EmitterT *emitter) {
}
// Each particle's position joins the tail of its trail (the oldest point drops off the front).
static void _recordTrails(EmitterT *emitter) {
int32_t i;
if (emitter->trail == NULL) {
return;
}
for (i = 0; i < emitter->count; i++) {
float *points = emitter->trail + (size_t)i * (size_t)emitter->trailLength * 3;
int32_t count = emitter->trailCount[i];
if (count == emitter->trailLength) {
memmove(points, points + 3, sizeof(float) * 3 * (size_t)(count - 1));
count--;
}
points[count * 3] = emitter->px[i];
points[count * 3 + 1] = emitter->py[i];
points[count * 3 + 2] = emitter->pz[i];
emitter->trailCount[i] = count + 1;
}
}
// Moves every trail point by an offset, when the particles themselves move frames of reference.
static void _shiftTrails(EmitterT *emitter, Vec3T offset) {
int32_t i;
int32_t j;
if (emitter->trail == NULL) {
return;
}
for (i = 0; i < emitter->count; i++) {
float *points = emitter->trail + (size_t)i * (size_t)emitter->trailLength * 3;
for (j = 0; j < emitter->trailCount[i]; j++) {
points[j * 3] += offset.x;
points[j * 3 + 1] += offset.y;
points[j * 3 + 2] += offset.z;
}
}
}
static void _spawn(EmitterT *emitter, int32_t count) {
Vec3T origin = _origin(emitter);
Vec3T base = emitter->local ? vec3(0.0f, 0.0f, 0.0f) : origin;
@ -409,7 +455,7 @@ static void _spawn(EmitterT *emitter, int32_t count) {
emitter->size1[i] = emitter->sizeEnd * variation;
emitter->angle[i] = _random(0.0f, 360.0f);
emitter->spin[i] = _random(emitter->spinMin, emitter->spinMax);
emitter->frame[i] = (emitter->frameCount > 1) ? (int32_t)_random((float)emitter->frameFirst, (float)emitter->frameLast + 0.999f) : 0;
emitter->frame[i] = (emitter->frameCount > 1) ? emitter->frameFirst + SDL_rand(emitter->frameLast - emitter->frameFirst + 1) : 0;
if (emitter->trailCount != NULL) {
emitter->trailCount[i] = 0;
}
@ -418,29 +464,6 @@ static void _spawn(EmitterT *emitter, int32_t count) {
}
// Each particle's position joins the tail of its trail (the oldest point drops off the front).
static void _recordTrails(EmitterT *emitter) {
int32_t i;
if (emitter->trail == NULL) {
return;
}
for (i = 0; i < emitter->count; i++) {
float *points = emitter->trail + (size_t)i * (size_t)emitter->trailLength * 3;
int32_t count = emitter->trailCount[i];
if (count == emitter->trailLength) {
memmove(points, points + 3, sizeof(float) * 3 * (size_t)(count - 1));
count--;
}
points[count * 3] = emitter->px[i];
points[count * 3 + 1] = emitter->py[i];
points[count * 3 + 2] = emitter->pz[i];
emitter->trailCount[i] = count + 1;
}
}
static void _step(EmitterT *emitter, float dt) {
float keep = SDL_max(0.0f, 1.0f - emitter->drag * dt);
int32_t i = 0;
@ -532,6 +555,7 @@ void emitterDelete(int32_t emitter) {
if (found != NULL) {
HASH_DEL(_emitters, found);
_freePools(found);
_freeTrails(found);
_freeFrames(found);
SDL_free(found);
}
@ -693,15 +717,15 @@ void emitterSetLocal(int32_t emitter, bool local) {
if ((found != NULL) && (found->local != local)) {
// Keep the live particles where they are by moving them between frames of reference.
Vec3T origin = _origin(found);
float sign = local ? -1.0f : 1.0f;
Vec3T offset = vec3Scale(_origin(found), local ? -1.0f : 1.0f);
int32_t i;
for (i = 0; i < found->count; i++) {
found->px[i] += sign * origin.x;
found->py[i] += sign * origin.y;
found->pz[i] += sign * origin.z;
found->px[i] += offset.x;
found->py[i] += offset.y;
found->pz[i] += offset.z;
}
_shiftTrails(found, offset);
found->local = local;
}
}
@ -864,11 +888,6 @@ void particlesClearQueue2D(void) {
}
int32_t particlesCount(void) {
return (int32_t)HASH_COUNT(_emitters);
}
// The built-in picture: a soft disc, opaque in the middle fading to nothing at the edge.
void particlesInit(void) {
int32_t x;
@ -876,6 +895,7 @@ void particlesInit(void) {
float dx;
float dy;
float d;
float fade;
uint8_t alpha;
uint32_t *pixels;
@ -889,7 +909,8 @@ void particlesInit(void) {
dx = ((float)x + 0.5f) / (DISC_SIZE / 2.0f) - 1.0f;
dy = ((float)y + 0.5f) / (DISC_SIZE / 2.0f) - 1.0f;
d = SDL_sqrtf(dx * dx + dy * dy);
alpha = (uint8_t)(255.0f * SDL_clamp(1.0f - d, 0.0f, 1.0f) * SDL_clamp(1.0f - d, 0.0f, 1.0f));
fade = SDL_clamp(1.0f - d, 0.0f, 1.0f);
alpha = (uint8_t)(COLOUR_MAX * fade * fade);
pixels[y * (_disc->pitch / 4) + x] = SDL_MapSurfaceRGBA(_disc, 255, 255, 255, alpha);
}
}
@ -898,7 +919,7 @@ void particlesInit(void) {
void particlesQueue2D(int32_t emitter) {
if ((_queueCount < QUEUE_MAX) && emitterValid(emitter)) {
if ((_queueCount < PARTICLES_QUEUE_MAX) && emitterValid(emitter)) {
_queue[_queueCount++] = emitter;
}
}
@ -960,19 +981,22 @@ void particlesUpdate(bool advance) {
}
bool particlesView(int32_t index, EmitterViewT *view) {
// Every 3D emitter with live particles, up to max of them, in one walk; how many were filled.
int32_t particlesView3D(EmitterViewT *views, int32_t max) {
EmitterT *emitter;
EmitterT *next;
int32_t n = 0;
HASH_ITER(hh, _emitters, emitter, next) {
if (n == index) {
_view(emitter, view);
return true;
if (n == max) {
break;
}
if ((emitter->node >= 0) && (emitter->count > 0)) {
_view(emitter, &views[n]);
n++;
}
return false;
}
return n;
}

View file

@ -30,6 +30,8 @@
#include <SDL3/SDL.h>
#include "math3d.h"
#define PARTICLES_QUEUE_MAX 256 // 2D emitters that may be queued for drawing in one frame
typedef enum ParticleBlendE {
PARTICLE_ALPHA = 0, // Normal alpha blending
PARTICLE_ADD = 1 // Additive: light on light
@ -78,8 +80,7 @@ typedef struct EmitterViewT {
void particlesInit(void);
void particlesQuit(void);
void particlesUpdate(bool advance);
int32_t particlesCount(void);
bool particlesView(int32_t index, EmitterViewT *view); // Emitter number index of particlesCount
int32_t particlesView3D(EmitterViewT *views, int32_t max); // Every 3D emitter with live particles, up to max; returns how many
bool particlesViewEmitter(int32_t emitter, EmitterViewT *view);
void particlesQueue2D(int32_t emitter); // Draw this emitter this frame
int32_t particlesQueued2D(ParticleLayerE layer, int32_t *emitters, int32_t max);

View file

@ -47,8 +47,8 @@ typedef enum ShapeTypeE {
SHAPE_SPHERE = 1, // radius
SHAPE_CAPSULE = 2, // radius, height (caps included), along Y
SHAPE_CYLINDER = 3, // radius, height, along Y
SHAPE_HULL = 4, // a convex hull of the node's mesh (stage 4)
SHAPE_MESH = 5 // the node's mesh triangles, static only (stage 4)
SHAPE_HULL = 4, // a convex hull of the node's mesh
SHAPE_MESH = 5 // the node's mesh triangles, static only
} ShapeTypeE;
@ -91,7 +91,7 @@ typedef enum PhysicsDebugE {
DEBUG_CONTACTS = 4, // A cross where bodies met this step
DEBUG_VELOCITIES = 8, // A line along each moving body's velocity
DEBUG_STATIC = 16, // Static bodies' shapes as well (a level mesh is a lot of lines)
DEBUG_ALL = 15 // Everything but the static shapes
DEBUG_ALL = DEBUG_SHAPES | DEBUG_CONSTRAINTS | DEBUG_CONTACTS | DEBUG_VELOCITIES // Everything but the static shapes
} PhysicsDebugE;
typedef enum VehicleKindE {
@ -102,6 +102,13 @@ typedef enum VehicleKindE {
} VehicleKindE;
// Limits the engine shares with the scripts' side of the API.
#define PHYSICS_MAX_EVENTS 512 // Events queued per frame; the rest of a busy step is dropped
#define VEHICLE_MAX_GEARS 8 // Forward gear ratios a vehicle may have
#define VEHICLE_DEFAULT_MIN_RPM 1000.0f
#define VEHICLE_DEFAULT_REVERSE_GEAR 2.9f // Jolt's own reverse ratio, as a positive number
bool bodyApplyForce(int32_t node, Vec3T force, const Vec3T *at);
bool bodyApplyImpulse(int32_t node, Vec3T impulse, const Vec3T *at);
bool bodyDelete(int32_t node);

File diff suppressed because it is too large Load diff

View file

@ -61,15 +61,26 @@ static void _sampleSmooth(const SDL_Surface *source, double sx, double sy, uint8
int32_t fx = (int32_t)((sx - 0.5 - (double)x0) * WEIGHT_ONE);
int32_t fy = (int32_t)((sy - 0.5 - (double)y0) * WEIGHT_ONE);
int32_t weight[4];
uint8_t corner[4][CHANNELS];
const uint8_t *corner[4];
int32_t sum = 0;
int32_t c = 0;
int32_t i = 0;
uint8_t edge[4][CHANNELS];
_sampleNearest(source, (double)x0, (double)y0, corner[0]);
_sampleNearest(source, (double)x0 + 1, (double)y0, corner[1]);
_sampleNearest(source, (double)x0, (double)y0 + 1, corner[2]);
_sampleNearest(source, (double)x0 + 1, (double)y0 + 1, corner[3]);
if ((x0 >= 0) && (y0 >= 0) && (x0 + 1 < source->w) && (y0 + 1 < source->h)) {
// All four inside: straight from the pixels.
const uint8_t *row = (const uint8_t *)source->pixels + (size_t)y0 * (size_t)source->pitch + (size_t)x0 * CHANNELS;
corner[0] = row;
corner[1] = row + CHANNELS;
corner[2] = row + source->pitch;
corner[3] = row + source->pitch + CHANNELS;
} else {
for (i = 0; i < 4; i++) {
_sampleNearest(source, (double)(x0 + (i & 1)), (double)(y0 + (i >> 1)), edge[i]);
corner[i] = edge[i];
}
}
weight[0] = (WEIGHT_ONE - fx) * (WEIGHT_ONE - fy);
weight[1] = fx * (WEIGHT_ONE - fy);
weight[2] = (WEIGHT_ONE - fx) * fy;
@ -94,8 +105,6 @@ SDL_Surface *rotoZoomSurface(SDL_Surface *source, double angle, double zoomX, do
double halfH = 0.0;
double cornerX = 0.0;
double cornerY = 0.0;
double maxX = 0.0;
double maxY = 0.0;
double dx = 0.0;
double dy = 0.0;
double sx = 0.0;
@ -121,10 +130,8 @@ SDL_Surface *rotoZoomSurface(SDL_Surface *source, double angle, double zoomX, do
halfH = (double)rgba->h * zoomY / 2.0;
cornerX = fabs(halfW * cosine) + fabs(halfH * sine);
cornerY = fabs(halfW * sine) + fabs(halfH * cosine);
maxX = cornerX;
maxY = cornerY;
width = (int32_t)ceil(maxX * 2.0);
height = (int32_t)ceil(maxY * 2.0);
width = (int32_t)ceil(cornerX * 2.0);
height = (int32_t)ceil(cornerY * 2.0);
if (width < 1) {
width = 1;
}

File diff suppressed because it is too large Load diff

View file

@ -28,8 +28,10 @@
#include <SDL3/SDL.h>
#include "ktx2.h"
#include "math3d.h"
#include "sceneShared.h"
#define MAX_VIEWS 4 // Cameras rendered to textures besides the main one
#define SCENE_ROOT_NODE 0
// A vertex as the GPU sees it; model loaders fill these directly.
@ -45,10 +47,11 @@ typedef struct SceneVertexS {
// Hands back a video player's current texture (NULL when it has none), for materialSetVideo.
typedef SDL_Texture *(*SceneVideoSourceFn)(int32_t player);
// The codes are the shader's (sceneShared.h).
typedef enum LightTypeE {
LIGHT_DIRECTIONAL = 0,
LIGHT_POINT = 1,
LIGHT_SPOT = 2
LIGHT_DIRECTIONAL = LIGHT_TYPE_DIRECTIONAL,
LIGHT_POINT = LIGHT_TYPE_POINT,
LIGHT_SPOT = LIGHT_TYPE_SPOT
} LightTypeE;
// How a material's textures are sampled.
@ -73,19 +76,21 @@ typedef enum MaterialMapE {
MAP_EMISSIVE = 4
} MaterialMapE;
// The curve that brings the scene's linear light down to the display.
// The curve that brings the scene's linear light down to the display (codes from sceneShared.h).
typedef enum SceneTonemapE {
TONEMAP_NONE = 0,
TONEMAP_NEUTRAL = 1,
TONEMAP_ACES = 2
TONEMAP_NONE = TONEMAP_CURVE_NONE,
TONEMAP_NEUTRAL = TONEMAP_CURVE_NEUTRAL,
TONEMAP_ACES = TONEMAP_CURVE_ACES
} SceneTonemapE;
bool sceneAvailable(void);
Ktx2FormatE sceneCompressedFormat(void); // What KTX2 textures are transcoded to on this GPU
void sceneComputeNormals(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
void sceneComputeTangents(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
void sceneDrawLine(Vec3T from, Vec3T to, uint8_t r, uint8_t g, uint8_t b); // A world-space line for this frame only (sRGB colour)
bool sceneEnable(bool enabled);
void sceneGetSize(int32_t *width, int32_t *height);
Ktx2FormatE sceneCompressedFormat(void); // What KTX2 textures are transcoded to on this GPU
void sceneDrawLine(Vec3T from, Vec3T to, uint8_t r, uint8_t g, uint8_t b); // A world-space line for this frame only (sRGB colour)
void sceneGetStats(int32_t *total, int32_t *drawn, int32_t *batches, int64_t *textureBytes);
Mat4T sceneGetView(void);
bool sceneInit(SDL_GPUDevice *device, SDL_Renderer *renderer);
@ -94,10 +99,8 @@ bool sceneProject(Vec3T world, float *x, float *y, float *depth);
void sceneQuit(void);
SDL_Texture *sceneRender(void);
bool sceneResize(int32_t width, int32_t height);
void sceneSetAntialias(bool antialias);
void sceneSetAmbient(uint8_t r, uint8_t g, uint8_t b);
void sceneComputeNormals(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
void sceneComputeTangents(SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
void sceneSetAntialias(bool antialias);
void sceneSetBackground(uint8_t r, uint8_t g, uint8_t b, uint8_t a);
void sceneSetBloom(float threshold, float strength);
bool sceneSetEnvironment(bool lit);
@ -127,6 +130,7 @@ void cameraSetPerspective(float fovDegrees, float near, float far);
bool lightAttach(int32_t node, LightTypeE type);
int32_t lightNew(LightTypeE type, int32_t parent);
bool lightSetColor(int32_t node, uint8_t r, uint8_t g, uint8_t b);
bool lightSetColorLinear(int32_t node, float r, float g, float b);
bool lightSetCone(int32_t node, float innerDegrees, float outerDegrees);
bool lightSetIntensity(int32_t node, float intensity);
bool lightSetRange(int32_t node, float range);
@ -140,15 +144,11 @@ bool materialSetColorLinear(int32_t material, float r, float g, float b,
bool materialSetDoubleSided(int32_t material, bool doubleSided);
bool materialSetEmissive(int32_t material, uint8_t r, uint8_t g, uint8_t b);
bool materialSetEmissiveLinear(int32_t material, float r, float g, float b);
bool materialSetEmissiveMap(int32_t material, SDL_Surface *image);
bool materialSetFilter(int32_t material, MaterialFilterE filter);
bool materialSetMap(int32_t material, MaterialMapE map, const Ktx2ImageT *image, float strength); // A transcoded KTX2 image (NULL clears); strength for normal and occlusion
bool materialSetMapSurface(int32_t material, MaterialMapE map, SDL_Surface *image, float strength); // The same from a surface (NULL clears)
bool materialSetMetallic(int32_t material, float metallic);
bool materialSetMetallicRoughnessMap(int32_t material, SDL_Surface *image);
bool materialSetNormalMap(int32_t material, SDL_Surface *image, float strength);
bool materialSetOcclusionMap(int32_t material, SDL_Surface *image, float strength);
bool materialSetRoughness(int32_t material, float roughness);
bool materialSetTexture(int32_t material, SDL_Surface *image);
bool materialSetTiling(int32_t material, float u, float v);
bool materialSetUnlit(int32_t material, bool unlit);
bool materialSetVideo(int32_t material, int32_t player);
@ -159,21 +159,20 @@ int32_t meshBox(float width, float height, float depth);
int32_t meshCone(float radius, float height, int32_t segments);
int32_t meshCylinder(float radius, float height, int32_t segments);
bool meshDelete(int32_t mesh);
int32_t nodeGetMaterial(int32_t node);
bool meshSetPositions(int32_t mesh, const float *positions);
int32_t meshFindMorph(int32_t mesh, const char *name);
bool meshGetGeometry(int32_t mesh, const float **positions, int32_t *vertexCount, const uint32_t **indices, int32_t *indexCount);
bool meshGetHeights(int32_t mesh, const float **heights, int32_t *columns, int32_t *rows, float *sizeX, float *sizeY, float *sizeZ);
int32_t meshFindMorph(int32_t mesh, const char *name);
int32_t meshGetMorphCount(int32_t mesh);
const char *meshGetMorphName(int32_t mesh, int32_t target);
int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned);
int32_t meshGrid(float width, float depth, int32_t columns, int32_t rows);
int32_t meshHeightmap(const float *heights, int32_t columns, int32_t rows, float sizeX, float sizeY, float sizeZ); // heights 0..1, (columns + 1) x (rows + 1), top row first
int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned);
int32_t meshPlane(float width, float depth);
bool meshSetMorphTargets(int32_t mesh, const float *deltas, int32_t targetCount, const char **names);
bool meshSetPositions(int32_t mesh, const float *positions);
int32_t meshSphere(float radius, int32_t segments);
int32_t meshTorus(float radius, float tubeRadius, int32_t segments);
bool meshSetMorphTargets(int32_t mesh, const float *deltas, int32_t targetCount, const char **names);
bool meshValid(int32_t mesh);
bool nodeDelete(int32_t node);
@ -181,6 +180,7 @@ int32_t nodeFind(int32_t root, const char *name);
int32_t nodeGetChild(int32_t node, int32_t index);
int32_t nodeGetChildCount(int32_t node);
uint32_t nodeGetGeneration(int32_t node);
int32_t nodeGetMaterial(int32_t node);
int32_t nodeGetMesh(int32_t node);
int32_t nodeGetMorphCount(int32_t node);
float nodeGetMorphWeight(int32_t node, int32_t target);
@ -189,6 +189,7 @@ int32_t nodeGetParent(int32_t node);
Vec3T nodeGetPosition(int32_t node);
QuatT nodeGetRotation(int32_t node);
Vec3T nodeGetScale(int32_t node);
int32_t nodeGetSkinJoints(int32_t node, const int32_t **joints);
Vec3T nodeGetWorldPosition(int32_t node);
bool nodeGetWorldTransform(int32_t node, Vec3T *position, QuatT *rotation, Vec3T *scale);
bool nodeLookAt(int32_t node, Vec3T target);
@ -204,9 +205,8 @@ bool nodeSetParent(int32_t node, int32_t parent);
bool nodeSetPosition(int32_t node, Vec3T position);
bool nodeSetRotation(int32_t node, QuatT rotation);
bool nodeSetScale(int32_t node, Vec3T scale);
int32_t nodeGetSkinJoints(int32_t node, const int32_t **joints);
bool nodeSetSkin(int32_t node, const int32_t *joints, const Mat4T *inverseBind, int32_t count);
bool nodeSetShadow(int32_t node, bool casts);
bool nodeSetSkin(int32_t node, const int32_t *joints, const Mat4T *inverseBind, int32_t count);
bool nodeSetSprite(int32_t node, SDL_Surface **frames, int32_t count, float width, float height, bool lit); // NULL clears
bool nodeSetSpriteFrame(int32_t node, int32_t frame);
bool nodeSetVisible(int32_t node, bool visible);

View file

@ -22,40 +22,25 @@
// Singe 3 scene shaders: one vertex shader for static meshes, one for skinned meshes, one
// fragment shader for both, and an empty fragment shader for the depth-only shadow pass (which
// reuses the vertex shaders with the light's view-projection). Compiled offline by src/shaders/build.sh (SDL_shadercross) into the
// SPIR-V, DXIL and MSL blobs in sceneShaders.h; the engine build never compiles shaders.
// reuses the vertex shaders with the light's view-projection). Compiled by the build
// (cmake/shaderHeader.cmake, SDL_shadercross) into the SPIR-V, DXIL and MSL blobs of the generated
// sceneShaders.h; the engine never compiles shaders at run time.
//
// Resource bindings follow SDL_GPU's HLSL convention: vertex uniforms in space1, fragment
// textures and samplers in space2, fragment uniforms in space3.
//
// The array sizes and codes the uniform blocks are laid out with come from sceneShared.h, the
// same file the C side reads.
#define MAX_LIGHTS 8
#define MAX_JOINTS 128
#define MAX_SHADOWS 8
#define MAX_MORPHS 8 // Active morph targets per draw
#include "sceneShared.h"
#define SHADOW_NONE 0
#define SHADOW_MAP 1
#define SHADOW_CUBE 2
#define SHADOW_CASCADE 3
#define MAX_CASCADES 4
#define CASCADE_BLEND 0.1 // The fraction of each cascade over which the next blends in
#define LIGHT_DIRECTIONAL 0
#define LIGHT_POINT 1
#define LIGHT_SPOT 2
#define TONEMAP_NONE 0
#define TONEMAP_NEUTRAL 1
#define TONEMAP_ACES 2
#define NORMAL_OFFSET_TEXELS 8.0 // How far along the normal a shadow lookup steps, in depth bias units
#define CONE_EDGE_MIN 0.0001 // The narrowest spot cone edge (in cosine), keeping smoothstep defined
#define PI 3.14159265
#define DEGREES_TO_RADIANS 0.017453292
#define MIN_ROUGHNESS 0.045 // Below this the GGX lobe is narrower than a pixel and sparkles
// Textures: what material.w says about the base texture.
#define TEXTURE_NONE 0
#define TEXTURE_SRGB 1 // A texture the sampler decodes to linear
#define TEXTURE_FEED 2 // A video frame, sRGB, decoded here
// ----- Vertex -----
@ -66,6 +51,7 @@ cbuffer DrawUniforms : register(b0, space1) {
int4 morphInfo; // x = active count, y = vertices per target, z = the draw's first pair in instanceMatrices
};
// Morph target deltas: per target, per vertex, a position delta then a normal delta.
StructuredBuffer<float4> morphDeltas : register(t0, space0);
@ -77,6 +63,7 @@ cbuffer SkinUniforms : register(b1, space1) {
float4x4 joints[MAX_JOINTS];
};
struct VertexInput {
float3 position : TEXCOORD0;
float3 normal : TEXCOORD1;
@ -86,6 +73,7 @@ struct VertexInput {
float4 tangent : TEXCOORD5; // xyz along +u, w the bitangent's handedness
};
struct VertexOutput {
float4 position : SV_Position;
float3 worldPosition : TEXCOORD0;
@ -176,15 +164,12 @@ struct Light {
float4 cone; // x = cos(inner), y = cos(outer), z = shadow slot + 1 (0 = casts none)
};
// What every draw in a pass shares: the camera, the lights and their shadows, the sky and fog.
cbuffer FragmentUniforms : register(b0, space3) {
float4 cameraPosition;
float4 cameraForward;
float4 ambient;
float4 baseColor;
float4 emissive;
float4 material; // x = metallic, y = roughness, z = unlit (1/0), w = TEXTURE_*
float4 maps; // x = normal map strength (0 = none), y = occlusion strength (0 = none)
float4 tiling; // x, y = texture repeats across the surface
float4 counts; // x = light count
float4 shadowParams; // x = depth bias, y = 1 / map size, z = shadow count
float4x4 shadowMatrix[MAX_SHADOWS * MAX_CASCADES]; // Light view-projection per slot and cascade (maps)
@ -194,9 +179,20 @@ cbuffer FragmentUniforms : register(b0, space3) {
float4 fog; // rgb, w = on
float4 fogRange; // x = near, y = far
float4 environment; // x = lit by the sky, y = the sky's last mip level, z = sky intensity
float4 sh[9]; // The sky's diffuse light, second-order spherical harmonics
float4 sh[SH_COEFFICIENTS]; // The sky's diffuse light, second-order spherical harmonics
};
// What changes per material (pushed per draw batch).
cbuffer MaterialUniforms : register(b1, space3) {
float4 baseColor;
float4 emissive;
float4 material; // x = metallic, y = roughness, z = unlit (1/0), w = TEXTURE_*
float4 maps; // x = normal map strength (0 = none), y = occlusion strength (0 = none)
float4 tiling; // x, y = texture repeats across the surface
};
Texture2D<float4> baseTexture : register(t0, space2);
SamplerState baseSampler : register(s0, space2);
Texture2DArray<float> shadowMaps : register(t1, space2); // A layer per directional or spot shadow, six per point light
@ -216,7 +212,7 @@ SamplerState skyCubeSampler : register(s6, space2);
// How lit a point is by a directional or spot light's shadow map: its depth from the light against
// the map, averaged over a 3x3 block of texels so edges soften. Points outside the map are lit.
float shadowFactorMap(int slot, int cascade, float3 worldPosition, float3 normal, float3 toLight) {
float4 lightSpace = mul(shadowMatrix[slot * MAX_CASCADES + cascade], float4(worldPosition + normal * shadowParams.x * 8.0, 1.0));
float4 lightSpace = mul(shadowMatrix[slot * MAX_CASCADES + cascade], float4(worldPosition + normal * shadowParams.x * NORMAL_OFFSET_TEXELS, 1.0));
float layer = shadowInfo[slot].y + cascade;
float2 uv;
float depth;
@ -377,6 +373,35 @@ float3 linearToSrgb(float3 c) {
}
// Light x's direction to a point (toLight) and how much of it arrives there: the inverse square
// falloff, the fade to nothing at the range (so lights do not pop) and the spot cone. Shadows are
// the caller's. Shared by the mesh and particle shaders.
float lightTerm(int x, float3 worldPosition, out float3 toLight) {
float attenuation;
float distance;
if (lights[x].positionType.w == LIGHT_TYPE_DIRECTIONAL) {
toLight = normalize(-lights[x].directionRange.xyz);
return 1.0;
}
toLight = lights[x].positionType.xyz - worldPosition;
distance = length(toLight);
toLight = toLight / max(distance, 0.0001);
attenuation = 1.0 / (1.0 + distance * distance);
if (lights[x].directionRange.w > 0.0) {
attenuation *= saturate(1.0 - pow(distance / lights[x].directionRange.w, 4.0));
}
if (lights[x].positionType.w == LIGHT_TYPE_SPOT) {
// cone.x = cos(inner), cone.y = cos(outer); the edge width is kept above zero so a cone
// with equal angles is a hard edge rather than a division by zero.
float cosAngle = dot(-toLight, normalize(lights[x].directionRange.xyz));
attenuation *= smoothstep(0.0, 1.0, saturate((cosAngle - lights[x].cone.y) / max(lights[x].cone.x - lights[x].cone.y, CONE_EDGE_MIN)));
}
return attenuation;
}
// Named fragmentMain because "fragment" is a keyword in Metal and the MSL entry point keeps this name.
// Lighting is the glTF metallic-roughness model in linear space: Lambert diffuse and a GGX
// specular lobe with Schlick's Fresnel and Smith's height-correlated visibility. A light of
@ -449,8 +474,7 @@ float4 fragmentMain(VertexOutput input) : SV_Target {
lightCount = (int)counts.x;
for (x = 0; x < MAX_LIGHTS; x++) {
float3 toLight;
float attenuation = 1.0;
float distance;
float attenuation;
float diffuse;
float specular;
float3 halfway;
@ -459,22 +483,7 @@ float4 fragmentMain(VertexOutput input) : SV_Target {
if (x >= lightCount) {
continue;
}
if (lights[x].positionType.w == LIGHT_DIRECTIONAL) {
toLight = normalize(-lights[x].directionRange.xyz);
} else {
toLight = lights[x].positionType.xyz - input.worldPosition;
distance = length(toLight);
toLight = toLight / max(distance, 0.0001);
attenuation = 1.0 / (1.0 + distance * distance);
if (lights[x].directionRange.w > 0.0) {
// Fade to nothing at the range so lights do not pop.
attenuation *= saturate(1.0 - pow(distance / lights[x].directionRange.w, 4.0));
}
if (lights[x].positionType.w == LIGHT_SPOT) {
float cosAngle = dot(-toLight, normalize(lights[x].directionRange.xyz));
attenuation *= smoothstep(lights[x].cone.y, lights[x].cone.x, cosAngle);
}
}
attenuation = lightTerm(x, input.worldPosition, toLight);
if (lights[x].cone.z > 0.5) {
int slot = (int)lights[x].cone.z - 1;
@ -534,6 +543,7 @@ cbuffer SkyUniforms : register(b0, space3) {
float4 skyParams; // x = intensity
};
TextureCube<float4> skyTexture : register(t0, space2);
SamplerState skySampler : register(s0, space2);
@ -543,6 +553,7 @@ struct SkyInput {
float2 uv : TEXCOORD0;
};
float4 skyFragment(SkyInput input) : SV_Target {
float2 ndc = float2(input.uv.x * 2.0 - 1.0, 1.0 - input.uv.y * 2.0);
float4 far = mul(skyInverseViewProjection, float4(ndc, 1.0, 1.0));
@ -560,11 +571,13 @@ cbuffer PostUniforms : register(b0, space3) {
float4 postParams; // x = exposure scale, y = TONEMAP_*, z = bloom strength
};
struct PostOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
};
Texture2D<float4> postTexture : register(t0, space2);
SamplerState postSampler : register(s0, space2);
Texture2D<float4> bloomTexture : register(t1, space2); // The glow, or black
@ -580,6 +593,7 @@ PostOutput postVertex(uint id : SV_VertexID) {
return output;
}
// Khronos's PBR Neutral curve: keeps hues where glTF viewers keep them, compressing only the top.
float3 tonemapNeutral(float3 colour) {
float startCompression = 0.8 - 0.04;
@ -603,19 +617,21 @@ float3 tonemapNeutral(float3 colour) {
return lerp(colour, newPeak * float3(1.0, 1.0, 1.0), g);
}
// Narkowicz's fit of the ACES filmic curve: more contrast and a warmer roll-off than neutral.
float3 tonemapAces(float3 x) {
return saturate((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14));
}
float4 postFragment(PostOutput input) : SV_Target {
float4 source = postTexture.Sample(postSampler, input.uv);
float3 colour = (source.rgb + bloomTexture.Sample(bloomSampler, input.uv).rgb * postParams.z) * postParams.x;
int mode = (int)postParams.y;
if (mode == TONEMAP_NEUTRAL) {
if (mode == TONEMAP_CURVE_NEUTRAL) {
colour = tonemapNeutral(colour);
} else if (mode == TONEMAP_ACES) {
} else if (mode == TONEMAP_CURVE_ACES) {
colour = tonemapAces(colour);
}
return float4(linearToSrgb(saturate(colour)), source.a);
@ -628,6 +644,7 @@ cbuffer BloomUniforms : register(b0, space3) {
float4 bloomParams; // x, y = the source's texel size, z = threshold, w = first pass (1/0)
};
Texture2D<float4> bloomSource : register(t0, space2); // The level above (or the frame)
SamplerState bloomSourceSampler : register(s0, space2);
Texture2D<float4> bloomLower : register(t1, space2); // Upsample: the level below
@ -662,6 +679,7 @@ float4 bloomDown(PostOutput input) : SV_Target {
return float4(colour, 1.0);
}
// The level below, tent filtered over 3x3 texels, added to this level.
float4 bloomUp(PostOutput input) : SV_Target {
float2 t = bloomParams.xy;
@ -691,6 +709,7 @@ cbuffer ParticleUniforms : register(b0, space1) {
float4 cameraAhead;
};
// The fragment shares FragmentUniforms (lights, ambient, sky, fog) with the meshes, plus this.
cbuffer ParticleParams : register(b1, space3) {
float4 particleFlags; // x = additive run (fades to nothing in fog), y = lit, z = softness (0 = none)
@ -701,6 +720,7 @@ cbuffer ParticleParams : register(b1, space3) {
float4 particleAhead;
};
struct ParticleInput {
float3 centre : TEXCOORD0;
float2 corner : TEXCOORD1; // -1..1 across the quad
@ -709,6 +729,7 @@ struct ParticleInput {
float2 uv : TEXCOORD4;
};
struct ParticleOutput {
float4 position : SV_Position;
float4 colour : TEXCOORD0;
@ -717,6 +738,7 @@ struct ParticleOutput {
float viewDepth : TEXCOORD3; // Along the camera's forward, for soft fading against the scene
};
Texture2D<float4> particleTexture : register(t0, space2);
SamplerState particleSampler : register(s0, space2);
Texture2D<float> sceneDepth : register(t1, space2); // The camera's depth, for soft particles
@ -724,7 +746,7 @@ SamplerState sceneDepthSampler : register(s1, space2);
ParticleOutput particleVertex(ParticleInput input) {
ParticleOutput output;
float radians = input.sizeAngle.y * 0.017453292;
float radians = input.sizeAngle.y * DEGREES_TO_RADIANS;
float c = cos(radians);
float s = sin(radians);
float2 offset = float2(input.corner.x * c - input.corner.y * s, input.corner.x * s + input.corner.y * c) * input.sizeAngle.x * 0.5;
@ -747,6 +769,7 @@ float linearDepth(float d) {
return particleDepth.x + d * (particleDepth.y - particleDepth.x);
}
float4 particleFragment(ParticleOutput input) : SV_Target {
float4 colour = particleTexture.Sample(particleSampler, input.uv) * input.colour;
int lightCount;
@ -775,27 +798,12 @@ float4 particleFragment(ParticleOutput input) : SV_Target {
lightCount = (int)counts.x;
for (x = 0; x < MAX_LIGHTS; x++) {
float3 toLight;
float attenuation = 1.0;
float distance;
float attenuation;
if (x >= lightCount) {
continue;
}
if (lights[x].positionType.w == LIGHT_DIRECTIONAL) {
toLight = normalize(-lights[x].directionRange.xyz);
} else {
toLight = lights[x].positionType.xyz - input.worldPosition;
distance = length(toLight);
toLight = toLight / max(distance, 0.0001);
attenuation = 1.0 / (1.0 + distance * distance);
if (lights[x].directionRange.w > 0.0) {
attenuation *= saturate(1.0 - pow(distance / lights[x].directionRange.w, 4.0));
}
if (lights[x].positionType.w == LIGHT_SPOT) {
float cosAngle = dot(-toLight, normalize(lights[x].directionRange.xyz));
attenuation *= smoothstep(lights[x].cone.y, lights[x].cone.x, cosAngle);
}
}
attenuation = lightTerm(x, input.worldPosition, toLight);
light += lights[x].color.rgb * attenuation * (0.5 + 0.5 * dot(normal, toLight));
}
colour.rgb *= light;
@ -814,23 +822,25 @@ float4 particleFragment(ParticleOutput input) : SV_Target {
}
// ----- Lines: unlit world-space segments drawn for one frame (physics debug drawing, lineDraw) -----
cbuffer LineUniforms : register(b0, space1) {
float4x4 lineViewProjection;
};
struct LineInput {
float3 position : TEXCOORD0;
float4 colour : TEXCOORD1; // Linear
};
struct LineOutput {
float4 position : SV_Position;
float4 colour : TEXCOORD0;
};
LineOutput lineVertex(LineInput input) {
LineOutput output;

File diff suppressed because it is too large Load diff

View file

@ -34,7 +34,6 @@
#define SINDEN_ARG_MAX 8
// Number of --sindengun arguments selects the border style.
typedef enum ToolModeE {
TOOL_NONE = 0,
TOOL_PACK,
@ -42,6 +41,7 @@ typedef enum ToolModeE {
TOOL_UNPACK
} ToolModeE;
// Number of --sindengun arguments selects the border style.
typedef enum SindenModeE {
SINDEN_WHITE = 1,
SINDEN_WHITE_BLACK = 2,

View file

@ -296,11 +296,16 @@ bool utilPathExists(const char *pathname) {
// Returns a new NUL terminated buffer the caller must free, or NULL.
char *utilReadFile(const char *filename, size_t *bytes) {
char *data = NULL;
FILE *in = fopen(filename, "rb");
FILE *in = NULL;
long size = 0;
*bytes = 0;
// fopen succeeds on a directory; only regular files have contents.
if (!utilFileExists(filename)) {
return NULL;
}
in = fopen(filename, "rb");
if (in) {
fseek(in, 0, SEEK_END);
size = ftell(in);
@ -330,6 +335,10 @@ char *utilReadLine(const char *haystack, size_t length, const char **offset) {
if (start == NULL) {
start = haystack;
}
// Skip blank lines, so a leading blank line is not mistaken for the end of the data.
while ((start < end) && ((*start == '\n') || (*start == '\r'))) {
start++;
}
tail = start;
// Find the end of the line or the end of the data.
@ -402,11 +411,13 @@ bool utilStartsWith(const char *string, const char *start) {
int32_t utilStricmp(const char *a, const char *b) {
int32_t d = 0;
for (;; a++, b++) {
while (true) {
d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
if (d != 0 || !*a) {
if ((d != 0) || (*a == 0)) {
return d;
}
a++;
b++;
}
}
@ -462,6 +473,7 @@ void utilTraceVArgs(const char *fmt, va_list args) {
va_copy(argsCopy, args);
_printLine(stdout, fmt, argsCopy);
va_end(argsCopy);
_outputHappened = true;
}
_printLine(_utilTraceFile, fmt, args);
}

472
src/vfs.c
View file

@ -20,17 +20,18 @@
*
*/
/*
* Singe virtual filesystem. See vfs.h for the lookup rules.
*
* Database layout (format version 1):
* meta(key TEXT PRIMARY KEY, value TEXT) version, gamedir, chunk, packer
* assets(path TEXT PRIMARY KEY, name TEXT, size INTEGER, data BLOB)
* chunks(path TEXT, chunk INTEGER, data BLOB, PRIMARY KEY(path, chunk))
* path is the normalised key (lower case, forward slashes); name keeps the
* author's spelling for unpacking. Files larger than meta.chunk live in
* chunks with assets.data NULL.
*/
// Singe virtual filesystem. See vfs.h for the lookup rules.
//
// Database layout (format version 1):
// meta(key TEXT PRIMARY KEY, value TEXT) version, gamedir, chunk, packer
// assets(path TEXT PRIMARY KEY, name TEXT, size INTEGER, data BLOB)
// chunks(path TEXT, chunk INTEGER, data BLOB, PRIMARY KEY(path, chunk))
// path is the normalised key (lower case, forward slashes); name keeps the
// author's spelling for unpacking. Files larger than meta.chunk live in
// chunks with assets.data NULL.
//
// Nothing read from a database is trusted: sizes are checked against the
// bytes actually stored, meta.chunk must be sane, and names may not escape.
#include <ctype.h>
#include <dirent.h>
@ -55,11 +56,9 @@
#endif
#define CACHE_DIRECTORY "cache"
#define ENGINE_DIRECTORY "Singe"
#define OVERLAY_DIRECTORY "files"
#define META_CHUNK "chunk"
#define META_GAMEDIR "gamedir"
#define META_VERSION "version"
#define LIST_INITIAL_CAPACITY 64
#define MAIN_SCHEMA "main"
typedef struct DatabaseS {
@ -76,6 +75,13 @@ typedef struct DatabaseS {
struct DatabaseS *next;
} DatabaseT;
// A directory listing being gathered: names are appended freely and sorted and de-duplicated once.
typedef struct ListS {
char **names;
int32_t count;
int32_t capacity;
} ListT;
// Where a name lands. A NULL database means the plain filesystem path.
typedef struct TargetS {
DatabaseT *db;
@ -101,17 +107,24 @@ struct VfsStreamS {
static bool _assetExists(DatabaseT *db, const char *key);
static bool _assetDirectory(DatabaseT *db, const char *key);
static bool _assetSize(DatabaseT *db, const char *key, int64_t *size);
static char *_bindListRange(DatabaseT *db, const char *key);
static bool _cacheCurrent(const TargetT *target);
static void _databaseClose(DatabaseT *db);
static DatabaseT *_databaseOpen(const char *path);
static bool _databaseReadMeta(DatabaseT *db, const char *key, char **value);
static void _databasesClose(void);
static bool _fileModified(const char *path, int64_t *size, int64_t *modified);
static void _listAdd(char ***list, int32_t *count, const char *name);
static void _listDirectory(const char *path, char ***list, int32_t *count);
static bool _hasDatabaseExtension(const char *path);
static bool _hasParentComponent(const char *norm);
static void _listAdd(ListT *list, const char *name);
static int _listCompare(const void *a, const void *b); // qsort callback. Not changing int.
static void _listDirectory(const char *path, ListT *list);
static char **_listFinish(ListT *list, int32_t *count);
static bool _isAbsolute(const char *name);
static bool _isEngineName(const char *name);
static char *_normalise(const char *name);
static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer, const char *directory);
static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool sdlMemory);
static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *buffer, int64_t *length);
static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *buffer, int64_t capacity, int64_t *length);
static bool _resolve(const char *name, TargetT *target);
static void _targetFree(TargetT *target);
static bool _writeFile(const char *path, const uint8_t *data, size_t bytes);
@ -121,6 +134,7 @@ static DatabaseT *_databases = NULL;
static DatabaseT *_container = NULL;
static char *_dataDirBase = NULL;
static char *_dataDir = NULL;
static char *_dataDirKey = NULL; // _dataDirBase normalised without trailing slashes, for _isEngineName
static bool _assetExists(DatabaseT *db, const char *key) {
@ -132,17 +146,11 @@ static bool _assetExists(DatabaseT *db, const char *key) {
// True when any asset lives below the key, which is what a directory is in a database.
static bool _assetDirectory(DatabaseT *db, const char *key) {
char *from = utilCreateString("%s/", key);
char *to = utilCreateString("%s0", key);
bool found = false;
char *from = _bindListRange(db, key);
bool found = (sqlite3_step(db->listStmt) == SQLITE_ROW);
sqlite3_reset(db->listStmt);
sqlite3_bind_text(db->listStmt, 1, from, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(db->listStmt, 2, to, -1, SQLITE_TRANSIENT);
found = (sqlite3_step(db->listStmt) == SQLITE_ROW);
sqlite3_reset(db->listStmt);
free(from);
free(to);
return found;
}
@ -163,12 +171,64 @@ static bool _assetSize(DatabaseT *db, const char *key, int64_t *size) {
}
// Opens a game database read only, or returns the cached handle. NULL when the file is not one of ours.
// Binds the listing statement to everything below a directory key: paths sort between "key/" and
// "key0" ('0' follows '/'), and the root (an empty key) has no bounds at all. Returns the lower
// bound, which is also the prefix every listed path carries; the caller frees it.
static char *_bindListRange(DatabaseT *db, const char *key) {
char *from = (key[0] == 0) ? strdup("") : utilCreateString("%s/", key);
char *to = (key[0] == 0) ? strdup("") : utilCreateString("%s0", key);
sqlite3_reset(db->listStmt);
sqlite3_bind_text(db->listStmt, 1, from, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(db->listStmt, 2, to, -1, SQLITE_TRANSIENT);
free(to);
return from;
}
// True when the cached copy of a packed asset is the same size as the asset and no older than the database.
static bool _cacheCurrent(const TargetT *target) {
int64_t cacheSize = 0;
int64_t cacheTime = 0;
int64_t assetSize = 0;
int64_t databaseSize = 0;
int64_t databaseTime = 0;
if (!_fileModified(target->cache, &cacheSize, &cacheTime) || !_assetSize(target->db, target->key, &assetSize)) {
return false;
}
if (!_fileModified(target->db->path, &databaseSize, &databaseTime)) {
return false;
}
return (cacheSize == assetSize) && (cacheTime >= databaseTime);
}
// Releases a database that is not (or no longer) in the open list.
static void _databaseClose(DatabaseT *db) {
sqlite3_finalize(db->assetStmt);
sqlite3_finalize(db->chunkStmt);
sqlite3_finalize(db->listStmt);
sqlite3_close(db->db);
free(db->path);
free(db->loose);
free(db->overlay);
free(db->cache);
free(db->gameDir);
free(db);
}
// Opens a game database read only, or returns the cached handle. NULL when the file is not one of
// ours or cannot be trusted; a newer format than this Singe understands is fatal.
static DatabaseT *_databaseOpen(const char *path) {
DatabaseT *db = NULL;
char *uri = NULL;
char *value = NULL;
char *encoded = NULL;
char *stem = NULL;
size_t i = 0;
size_t o = 0;
int64_t version = 0;
@ -178,9 +238,15 @@ static DatabaseT *_databaseOpen(const char *path) {
return db;
}
}
if (!_hasDatabaseExtension(path)) {
return NULL;
}
// Percent-encode the three characters a URI would misread.
encoded = (char *)calloc(strlen(path) * 3 + 1, 1);
if (encoded == NULL) {
utilDie("Out of memory opening %s.", path);
}
for (i = 0; path[i] != 0; i++) {
if ((path[i] == '?') || (path[i] == '#') || (path[i] == '%')) {
sprintf(encoded + o, "%%%02X", (unsigned char)path[i]);
@ -193,48 +259,62 @@ static DatabaseT *_databaseOpen(const char *path) {
free(encoded);
db = (DatabaseT *)calloc(1, sizeof(DatabaseT));
if (db == NULL) {
utilDie("Out of memory opening %s.", path);
}
db->path = strdup(path);
if (sqlite3_open_v2(uri, &db->db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL) != SQLITE_OK) {
free(uri);
sqlite3_close(db->db);
free(db->path);
free(db);
_databaseClose(db);
return NULL;
}
free(uri);
if (!_databaseReadMeta(db, META_VERSION, &value)) {
sqlite3_close(db->db);
free(db->path);
free(db);
value = vfsReadMeta(db->db, MAIN_SCHEMA, VFS_META_VERSION);
if (value == NULL) {
_databaseClose(db);
return NULL;
}
version = strtoll(value, NULL, 10);
free(value);
if (version > VFS_FORMAT_VERSION) {
utilDie("%s is a format %lld game database; this Singe understands up to format %d.", path, (long long)version, VFS_FORMAT_VERSION);
utilDie("%s is a format %" PRId64 " game database; this Singe understands up to format %d.", path, version, VFS_FORMAT_VERSION);
}
if (sqlite3_prepare_v2(db->db, "SELECT size, data FROM assets WHERE path = ?", -1, &db->assetStmt, NULL) != SQLITE_OK) {
utilDie("%s has no assets table: %s", path, sqlite3_errmsg(db->db));
utilSay("Warning: %s has no assets table; ignoring it.", path);
_databaseClose(db);
return NULL;
}
if (sqlite3_prepare_v2(db->db, "SELECT data FROM chunks WHERE path = ? AND chunk = ?", -1, &db->chunkStmt, NULL) != SQLITE_OK) {
utilDie("%s has no chunks table: %s", path, sqlite3_errmsg(db->db));
utilSay("Warning: %s has no chunks table; ignoring it.", path);
_databaseClose(db);
return NULL;
}
// Everything below a directory key sorts between "key/" and "key0" ('0' follows '/').
if (sqlite3_prepare_v2(db->db, "SELECT path FROM assets WHERE path >= ? AND path < ? ORDER BY path", -1, &db->listStmt, NULL) != SQLITE_OK) {
utilDie("%s: %s", path, sqlite3_errmsg(db->db));
if (sqlite3_prepare_v2(db->db, "SELECT path FROM assets WHERE path >= ?1 AND (?2 = '' OR path < ?2) ORDER BY path", -1, &db->listStmt, NULL) != SQLITE_OK) {
utilSay("Warning: %s: %s; ignoring it.", path, sqlite3_errmsg(db->db));
_databaseClose(db);
return NULL;
}
db->chunkBytes = VFS_CHUNK_BYTES;
if (_databaseReadMeta(db, META_CHUNK, &value)) {
value = vfsReadMeta(db->db, MAIN_SCHEMA, VFS_META_CHUNK);
if (value != NULL) {
db->chunkBytes = strtoll(value, NULL, 10);
free(value);
}
if (_databaseReadMeta(db, META_GAMEDIR, &value)) {
if ((db->chunkBytes <= 0) || (db->chunkBytes > VFS_CHUNK_BYTES_MAX)) {
utilSay("Warning: %s has an unusable chunk size (%" PRId64 "); ignoring it.", path, db->chunkBytes);
_databaseClose(db);
return NULL;
}
value = vfsReadMeta(db->db, MAIN_SCHEMA, VFS_META_GAMEDIR);
if (value != NULL) {
db->gameDir = _normalise(value);
free(value);
}
// Loose overrides live in a directory named like the database without its extension.
db->loose = utilCreateString("%.*s%c", (int)(strlen(path) - strlen(VFS_DATABASE_EXTENSION)), path, utilGetPathSeparator());
stem = vfsDatabaseStem(path);
db->loose = utilCreateString("%s%c", stem, utilGetPathSeparator());
free(stem);
db->next = _databases;
_databases = db;
@ -242,22 +322,17 @@ static DatabaseT *_databaseOpen(const char *path) {
}
static bool _databaseReadMeta(DatabaseT *db, const char *key, char **value) {
sqlite3_stmt *stmt = NULL;
bool found = false;
// Closes every open database. Streams and targets pointing into them must be gone by now.
static void _databasesClose(void) {
DatabaseT *db = NULL;
DatabaseT *next = NULL;
*value = NULL;
if (sqlite3_prepare_v2(db->db, "SELECT value FROM meta WHERE key = ?", -1, &stmt, NULL) != SQLITE_OK) {
return false;
for (db = _databases; db != NULL; db = next) {
next = db->next;
_databaseClose(db);
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
if ((sqlite3_step(stmt) == SQLITE_ROW) && (sqlite3_column_text(stmt, 0) != NULL)) {
*value = strdup((const char *)sqlite3_column_text(stmt, 0));
found = true;
}
sqlite3_finalize(stmt);
return found;
_databases = NULL;
_container = NULL;
}
@ -274,6 +349,30 @@ static bool _fileModified(const char *path, int64_t *size, int64_t *modified) {
}
// Ends in the database extension (any case) with something before it.
static bool _hasDatabaseExtension(const char *path) {
size_t length = strlen(path);
size_t extLen = strlen(VFS_DATABASE_EXTENSION);
return (length > extLen) && (utilStricmp(path + length - extLen, VFS_DATABASE_EXTENSION) == 0);
}
// True when any "/"-separated component of a normalised name is "..".
static bool _hasParentComponent(const char *norm) {
const char *p = norm;
while (*p != 0) {
if ((p[0] == '.') && (p[1] == '.') && ((p[2] == '/') || (p[2] == 0)) && ((p == norm) || (p[-1] == '/'))) {
return true;
}
p++;
}
return false;
}
static bool _isAbsolute(const char *name) {
return (name[0] == '/') || (isalpha((unsigned char)name[0]) && (name[1] == ':'));
}
@ -281,46 +380,49 @@ static bool _isAbsolute(const char *name) {
// Names the engine owns resolve on the filesystem even inside a packed game: Singe/ and the data directory.
static bool _isEngineName(const char *name) {
size_t length = strlen(ENGINE_DIRECTORY);
char *base = NULL;
bool engine = false;
size_t length = strlen(VFS_ENGINE_DIRECTORY);
if ((utilStricmp(name, ENGINE_DIRECTORY) == 0) || ((strncmp(name, ENGINE_DIRECTORY, length) == 0) && (name[length] == '/'))) {
if ((strncasecmp(name, VFS_ENGINE_DIRECTORY, length) == 0) && ((name[length] == '/') || (name[length] == 0))) {
return true;
}
// The data directory base, when it is a real prefix: "data/" or an absolute path. With no
// --datadir the base is "./", which normalises to nothing and must not match everything.
if (_dataDirBase != NULL) {
base = _normalise(_dataDirBase);
length = strlen(base);
while ((length > 0) && (base[length - 1] == '/')) {
length--;
}
engine = (length > 0) && (strncmp(name, base, length) == 0) && ((name[length] == '/') || (name[length] == 0));
free(base);
if ((_dataDirKey != NULL) && (_dataDirKey[0] != 0)) {
length = strlen(_dataDirKey);
return (strncmp(name, _dataDirKey, length) == 0) && ((name[length] == '/') || (name[length] == 0));
}
return engine;
return false;
}
// Appends a name to a listing unless it is already there (case-insensitively, like the keys).
static void _listAdd(char ***list, int32_t *count, const char *name) {
int32_t i = 0;
// Appends a name to a listing. Duplicates are dropped by _listFinish.
static void _listAdd(ListT *list, const char *name) {
char **grown = NULL;
for (i = 0; i < *count; i++) {
if (utilStricmp((*list)[i], name) == 0) {
return;
if (list->count == list->capacity) {
list->capacity = (list->capacity == 0) ? LIST_INITIAL_CAPACITY : list->capacity * 2;
grown = (char **)realloc(list->names, (size_t)list->capacity * sizeof(char *));
if (grown == NULL) {
utilDie("Out of memory listing a directory.");
}
list->names = grown;
}
*list = (char **)realloc(*list, (size_t)(*count + 1) * sizeof(char *));
(*list)[*count] = strdup(name);
(*count)++;
list->names[list->count] = strdup(name);
if (list->names[list->count] == NULL) {
utilDie("Out of memory listing a directory.");
}
list->count++;
}
static int _listCompare(const void *a, const void *b) {
return utilStricmp(*(char *const *)a, *(char *const *)b);
}
// Adds the entries of a filesystem directory to a listing, if it exists.
static void _listDirectory(const char *path, char ***list, int32_t *count) {
static void _listDirectory(const char *path, ListT *list) {
DIR *dir = opendir(path);
struct dirent *de = NULL;
@ -329,19 +431,43 @@ static void _listDirectory(const char *path, char ***list, int32_t *count) {
}
while ((de = readdir(dir)) != NULL) {
if ((strcmp(de->d_name, ".") != 0) && (strcmp(de->d_name, "..") != 0)) {
_listAdd(list, count, de->d_name);
_listAdd(list, de->d_name);
}
}
closedir(dir);
}
// Sorts a listing and drops names that repeat (case-insensitively, like the keys). NULL when empty.
static char **_listFinish(ListT *list, int32_t *count) {
int32_t i = 0;
int32_t kept = 0;
if (list->count > 0) {
qsort(list->names, (size_t)list->count, sizeof(char *), _listCompare);
}
for (i = 0; i < list->count; i++) {
if ((kept > 0) && (utilStricmp(list->names[kept - 1], list->names[i]) == 0)) {
free(list->names[i]);
} else {
list->names[kept++] = list->names[i];
}
}
*count = kept;
return list->names;
}
// Forward slashes, no leading "./", no doubled or trailing slashes. Case is untouched.
static char *_normalise(const char *name) {
char *out = strdup(name);
size_t i = 0;
size_t o = 0;
if (out == NULL) {
utilDie("Out of memory resolving %s.", name);
}
for (i = 0; out[i] != 0; i++) {
char c = (out[i] == '\\') ? '/' : out[i];
@ -366,20 +492,22 @@ static char *_normalise(const char *name) {
// A per-game directory under the data directory: the overlay or the cache.
static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer, const char *directory) {
const char *base = NULL;
size_t length = 0;
char *stem = NULL;
char *result = NULL;
if (isContainer && (dataDir != NULL)) {
return utilCreateString("%s%s%c", dataDir, directory, utilGetPathSeparator());
}
base = utilGetLastPathComponent(databasePath);
length = strlen(base) - strlen(VFS_DATABASE_EXTENSION);
stem = vfsDatabaseStem(utilGetLastPathComponent(databasePath));
result = utilCreateString("%s%s%c%s%c", dataDirBase ? dataDirBase : "", stem, utilGetPathSeparator(), directory, utilGetPathSeparator());
free(stem);
return utilCreateString("%s%.*s%c%s%c", dataDirBase ? dataDirBase : "", (int)length, base, utilGetPathSeparator(), directory, utilGetPathSeparator());
return result;
}
// Whole asset, from the row or reassembled from its chunks.
// Whole asset, from the row or reassembled from its chunks. The stored bytes must fit the size the
// row claims; a database that disagrees with itself is refused rather than overrun.
static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool sdlMemory) {
uint8_t *data = NULL;
int64_t size = 0;
@ -395,18 +523,24 @@ static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool s
return NULL;
}
size = sqlite3_column_int64(db->assetStmt, 0);
if (size < 0) {
utilDie("%s in %s has a negative size.", key, db->path);
}
data = sdlMemory ? (uint8_t *)SDL_malloc((size_t)size + 1) : (uint8_t *)malloc((size_t)size + 1);
if (data == NULL) {
utilDie("Out of memory reading %s from %s.", key, db->path);
}
if (sqlite3_column_type(db->assetStmt, 1) != SQLITE_NULL) {
memcpy(data, sqlite3_column_blob(db->assetStmt, 1), (size_t)sqlite3_column_bytes(db->assetStmt, 1));
offset = sqlite3_column_bytes(db->assetStmt, 1);
if (offset > size) {
utilDie("%s in %s holds more data than its size says.", key, db->path);
}
memcpy(data, sqlite3_column_blob(db->assetStmt, 1), (size_t)offset);
}
sqlite3_reset(db->assetStmt);
while (offset < size) {
if (!_readChunk(db, key, index, data + offset, &length) || (length == 0)) {
utilDie("%s in %s is missing chunk %lld.", key, db->path, (long long)index);
if (!_readChunk(db, key, index, data + offset, size - offset, &length) || (length == 0)) {
utilDie("%s in %s is missing chunk %" PRId64 ".", key, db->path, index);
}
offset += length;
index++;
@ -418,7 +552,8 @@ static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool s
}
static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *buffer, int64_t *length) {
// One chunk into a buffer of the given capacity. False when there is no such chunk.
static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *buffer, int64_t capacity, int64_t *length) {
bool found = false;
*length = 0;
@ -427,8 +562,8 @@ static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *b
sqlite3_bind_int64(db->chunkStmt, 2, index);
if (sqlite3_step(db->chunkStmt) == SQLITE_ROW) {
*length = sqlite3_column_bytes(db->chunkStmt, 0);
if (*length > db->chunkBytes) {
utilDie("%s in %s has an oversized chunk.", key, db->path);
if ((*length > db->chunkBytes) || (*length > capacity)) {
utilDie("%s in %s has an oversized chunk %" PRId64 ".", key, db->path, index);
}
memcpy(buffer, sqlite3_column_blob(db->chunkStmt, 0), (size_t)*length);
found = true;
@ -439,25 +574,26 @@ static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *b
}
// Decides where a name lives. Returns false only for an empty name.
// Decides where a name lives. Returns false for an empty name, and for a relative name inside a
// packed game that climbs out with "..": the packer refused it and so does the lookup.
static bool _resolve(const char *name, TargetT *target) {
char *norm = _normalise(name);
char *prefix = NULL;
const char *inner = NULL;
char *p = NULL;
size_t i = 0;
size_t length = strlen(norm);
size_t extLen = strlen(VFS_DATABASE_EXTENSION);
memset(target, 0, sizeof(*target));
if (norm[0] == 0) {
if (length == 0) {
free(norm);
return false;
}
// A path that passes through a database file addresses its contents: "Games/DLe.game/Overlay/x.png".
// Absolute paths included, so a tool can reach the packed games beside it from inside its own.
{
for (i = extLen; norm[i] != 0; i++) {
for (i = extLen; i < length; i++) {
if ((norm[i] == '/') && (strncasecmp(norm + i - extLen, VFS_DATABASE_EXTENSION, extLen) == 0)) {
prefix = utilStrndup(norm, i);
if (utilFileExists(prefix)) {
@ -476,6 +612,12 @@ static bool _resolve(const char *name, TargetT *target) {
prefix = NULL;
}
}
if ((target->db != NULL) || ((_container != NULL) && !_isAbsolute(norm))) {
if (_hasParentComponent((target->db != NULL) ? inner : norm)) {
memset(target, 0, sizeof(*target));
free(norm);
return false;
}
}
if ((target->db == NULL) && (_container != NULL) && !_isAbsolute(norm) && !_isEngineName(norm)) {
target->db = _container;
@ -535,7 +677,7 @@ static bool _writeFile(const char *path, const uint8_t *data, size_t bytes) {
FILE *file = NULL;
bool ok = false;
if ((directory[0] == 0) || utilMkDirP(directory, 0755)) {
if (utilMkDirP(directory, 0755)) {
file = fopen(path, "wb");
if (file != NULL) {
ok = (fwrite(data, 1, bytes, file) == bytes);
@ -548,6 +690,16 @@ static bool _writeFile(const char *path, const uint8_t *data, size_t bytes) {
}
// The path without its database extension, as a new string; unchanged when it has none.
char *vfsDatabaseStem(const char *path) {
if (_hasDatabaseExtension(path)) {
return utilStrndup(path, strlen(path) - strlen(VFS_DATABASE_EXTENSION));
}
return strdup(path);
}
bool vfsExists(const char *name) {
TargetT target;
bool found = false;
@ -565,32 +717,35 @@ bool vfsExists(const char *name) {
}
// A filesystem path Lua's io library can open for the name. Writes land in the loose directory or
// the overlay, never the database. A packed asset opened for reading is unpacked into the cache,
// which the lookup never searches, so a later patch of the asset is not shadowed by the copy.
// A filesystem path Lua's io library can open for the name, or NULL when the name resolves nowhere.
// Writes land in the loose directory or the overlay, never the database. A packed asset opened for
// reading is unpacked into the cache, which the lookup never searches, so a later patch of the asset
// is not shadowed by the copy; a copy that already matches the database is reused. A packed name
// nothing holds yields the loose candidate, which fails to open like any missing file.
char *vfsFilePath(const char *name, bool forWriting) {
TargetT target;
char *path = NULL;
char *directory = NULL;
uint8_t *data = NULL;
size_t bytes = 0;
if (!_resolve(name, &target)) {
return strdup(name);
return NULL;
}
if (target.db == NULL) {
path = target.path;
target.path = NULL;
} else if (utilFileExists(target.path)) {
if ((target.db == NULL) || utilFileExists(target.path)) {
path = target.path;
target.path = NULL;
} else if (forWriting || utilFileExists(target.overlay)) {
if (forWriting) {
char *directory = utilGetUpToLastPathComponent(target.overlay);
directory = utilGetUpToLastPathComponent(target.overlay);
utilMkDirP(directory, 0755);
free(directory);
}
path = target.overlay;
target.overlay = NULL;
} else if (_cacheCurrent(&target)) {
path = target.cache;
target.cache = NULL;
} else {
data = _readAsset(target.db, target.key, &bytes, false);
if (data != NULL) {
@ -601,7 +756,8 @@ char *vfsFilePath(const char *name, bool forWriting) {
path = target.cache;
target.cache = NULL;
} else {
path = strdup(name);
path = target.path;
target.path = NULL;
}
}
_targetFree(&target);
@ -611,12 +767,25 @@ char *vfsFilePath(const char *name, bool forWriting) {
// container is the game's database, or NULL for a loose game. dataDir carries a trailing separator.
// May be called again with new settings; everything opened before is closed first.
void vfsInit(const char *container, const char *dataDirBase, const char *dataDir) {
size_t length = 0;
_databasesClose();
free(_dataDirBase);
free(_dataDir);
free(_dataDirKey);
_dataDirBase = dataDirBase ? strdup(dataDirBase) : NULL;
_dataDir = dataDir ? strdup(dataDir) : NULL;
_container = NULL;
_dataDirKey = NULL;
if (_dataDirBase != NULL) {
_dataDirKey = _normalise(_dataDirBase);
length = strlen(_dataDirKey);
while ((length > 0) && (_dataDirKey[length - 1] == '/')) {
length--;
}
_dataDirKey[length] = 0;
}
if (container != NULL) {
_container = _databaseOpen(container);
if (_container == NULL) {
@ -631,10 +800,7 @@ void vfsInit(const char *container, const char *dataDirBase, const char *dataDir
bool vfsIsDatabase(const char *path) {
size_t length = strlen(path);
size_t extLen = strlen(VFS_DATABASE_EXTENSION);
if ((length <= extLen) || (utilStricmp(path + length - extLen, VFS_DATABASE_EXTENSION) != 0) || !utilFileExists(path)) {
if (!_hasDatabaseExtension(path) || !utilFileExists(path)) {
return false;
}
@ -662,9 +828,10 @@ bool vfsIsDirectory(const char *name) {
// True when the name is a plain filesystem path, so a caller may use the filesystem directly.
// Inside a packed game a name that resolves nowhere is not one, so it never reaches the filesystem.
bool vfsIsFilesystem(const char *name) {
TargetT target;
bool plain = true;
bool plain = (_container == NULL);
if (_resolve(name, &target)) {
plain = (target.db == NULL);
@ -676,12 +843,11 @@ bool vfsIsFilesystem(const char *name) {
// The entries directly under a directory name, from the loose directory, the overlay and the
// database together, each name once. Free with vfsListFree.
// database together, each name once, sorted. Free with vfsListFree.
char **vfsList(const char *name, int32_t *count) {
TargetT target;
char **list = NULL;
ListT list;
char *from = NULL;
char *to = NULL;
char *entry = NULL;
const char *path = NULL;
const char *slash = NULL;
@ -691,29 +857,25 @@ char **vfsList(const char *name, int32_t *count) {
if (!_resolve(name, &target)) {
return NULL;
}
_listDirectory(target.path, &list, count);
memset(&list, 0, sizeof(list));
_listDirectory(target.path, &list);
if (target.db != NULL) {
_listDirectory(target.overlay, &list, count);
prefix = strlen(target.key);
from = (prefix == 0) ? strdup("") : utilCreateString("%s/", target.key);
to = (prefix == 0) ? strdup("\x7f") : utilCreateString("%s0", target.key);
sqlite3_reset(target.db->listStmt);
sqlite3_bind_text(target.db->listStmt, 1, from, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(target.db->listStmt, 2, to, -1, SQLITE_TRANSIENT);
_listDirectory(target.overlay, &list);
from = _bindListRange(target.db, target.key);
prefix = strlen(from);
while (sqlite3_step(target.db->listStmt) == SQLITE_ROW) {
path = (const char *)sqlite3_column_text(target.db->listStmt, 0) + strlen(from);
path = (const char *)sqlite3_column_text(target.db->listStmt, 0) + prefix;
slash = strchr(path, '/');
entry = slash ? utilStrndup(path, (size_t)(slash - path)) : strdup(path);
_listAdd(&list, count, entry);
_listAdd(&list, entry);
free(entry);
}
sqlite3_reset(target.db->listStmt);
free(from);
free(to);
}
_targetFree(&target);
return list;
return _listFinish(&list, count);
}
@ -735,6 +897,7 @@ SDL_IOStream *vfsOpenIO(const char *name) {
size_t bytes = 0;
if (!_resolve(name, &target)) {
SDL_SetError("%s resolves nowhere", name);
return NULL;
}
if ((target.db == NULL) || utilFileExists(target.path)) {
@ -761,28 +924,13 @@ SDL_IOStream *vfsOpenIO(const char *name) {
void vfsQuit(void) {
DatabaseT *db = NULL;
DatabaseT *next = NULL;
for (db = _databases; db != NULL; db = next) {
next = db->next;
sqlite3_finalize(db->assetStmt);
sqlite3_finalize(db->chunkStmt);
sqlite3_finalize(db->listStmt);
sqlite3_close(db->db);
free(db->path);
free(db->loose);
free(db->overlay);
free(db->cache);
free(db->gameDir);
free(db);
}
_databases = NULL;
_container = NULL;
_databasesClose();
free(_dataDirBase);
free(_dataDir);
free(_dataDirKey);
_dataDirBase = NULL;
_dataDir = NULL;
_dataDirKey = NULL;
}
@ -808,6 +956,26 @@ char *vfsRead(const char *name, size_t *bytes) {
}
// One value of a database's meta table as a new string, or NULL when the key is absent. schema is
// "main" or the name a database was attached as; it comes from code, never from a game.
char *vfsReadMeta(struct sqlite3 *db, const char *schema, const char *key) {
sqlite3_stmt *stmt = NULL;
char *sql = utilCreateString("SELECT value FROM %s.meta WHERE key = ?", schema);
char *value = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
if ((sqlite3_step(stmt) == SQLITE_ROW) && (sqlite3_column_text(stmt, 0) != NULL)) {
value = strdup((const char *)sqlite3_column_text(stmt, 0));
}
}
sqlite3_finalize(stmt);
free(sql);
return value;
}
// Size and modification time, the latter being the database's own for a packed asset.
bool vfsStat(const char *name, int64_t *size, int64_t *modified) {
TargetT target;
@ -854,6 +1022,9 @@ VfsStreamT *vfsStreamOpen(const char *name) {
return NULL;
}
stream = (VfsStreamT *)calloc(1, sizeof(VfsStreamT));
if (stream == NULL) {
utilDie("Out of memory opening %s.", name);
}
stream->chunkIndex = -1;
if ((target.db == NULL) || utilFileExists(target.path)) {
path = target.path;
@ -881,6 +1052,9 @@ VfsStreamT *vfsStreamOpen(const char *name) {
stream->whole = true;
} else {
stream->buffer = (uint8_t *)malloc((size_t)target.db->chunkBytes);
if (stream->buffer == NULL) {
utilDie("Out of memory opening %s.", name);
}
}
} else {
free(stream);
@ -911,7 +1085,7 @@ int64_t vfsStreamRead(VfsStreamT *stream, void *buffer, int64_t bytes) {
index = stream->whole ? 0 : stream->position / stream->db->chunkBytes;
start = stream->whole ? stream->position : stream->position % stream->db->chunkBytes;
if (index != stream->chunkIndex) {
if (!_readChunk(stream->db, stream->key, index, stream->buffer, &stream->chunkLength)) {
if (!_readChunk(stream->db, stream->key, index, stream->buffer, stream->db->chunkBytes, &stream->chunkLength)) {
break;
}
stream->chunkIndex = index;

View file

@ -20,14 +20,15 @@
*
*/
/*
* Singe virtual filesystem: one lookup for every file a game names.
*
* A name resolves to a loose file in the game directory, to a copy-on-write
* overlay in the data directory, or to an asset inside the game's SQLite
* database, in that order. Loose games with no database see exactly the
* filesystem they always did.
*/
// Singe virtual filesystem: one lookup for every file a game names.
//
// A name resolves to a loose file in the game directory, to a copy-on-write
// overlay in the data directory, or to an asset inside the game's SQLite
// database, in that order. Loose games with no database see exactly the
// filesystem they always did.
//
// Inside a packed game a relative name may not climb out with a ".."
// component; such a name resolves nowhere (false, NULL, or an empty listing).
#ifndef VFS_H
#define VFS_H
@ -39,25 +40,40 @@
#include <SDL3/SDL.h>
// The one definition of the database format, shared with the packer.
#define VFS_DATABASE_EXTENSION ".game"
#define VFS_FORMAT_VERSION 1
#define VFS_CHUNK_BYTES (4 * 1024 * 1024)
#define VFS_CHUNK_BYTES_MAX (256 * 1024 * 1024) // Larger than this and meta.chunk is not believed
#define VFS_META_CHUNK "chunk"
#define VFS_META_GAMEDIR "gamedir"
#define VFS_META_PACKER "packer"
#define VFS_META_VERSION "version"
#define VFS_GAMES_DAT "games.dat"
// The directory the engine's own support files live in. Names below it stay on the filesystem
// even inside a packed game; the packer matches it without regard to case.
#define VFS_ENGINE_DIRECTORY "Singe"
struct sqlite3;
typedef struct VfsStreamS VfsStreamT;
char *vfsDatabaseStem(const char *path);
bool vfsExists(const char *name);
char *vfsFilePath(const char *name, bool forWriting);
void vfsInit(const char *container, const char *dataDirBase, const char *dataDir);
bool vfsIsDatabase(const char *path);
bool vfsIsDirectory(const char *name);
bool vfsIsFilesystem(const char *name);
char **vfsList(const char *name, int32_t *count);
void vfsListFree(char **list, int32_t count);
char *vfsFilePath(const char *name, bool forWriting);
void vfsInit(const char *container, const char *dataDirBase, const char *dataDir);
bool vfsIsDatabase(const char *path);
SDL_IOStream *vfsOpenIO(const char *name);
void vfsQuit(void);
char *vfsRead(const char *name, size_t *bytes);
char *vfsRead(const char *name, size_t *bytes); // Whole file, NUL terminated, in malloc memory: free() it, not SDL_free()
char *vfsReadMeta(struct sqlite3 *db, const char *schema, const char *key);
bool vfsStat(const char *name, int64_t *size, int64_t *modified);
void vfsStreamClose(VfsStreamT *stream);
VfsStreamT *vfsStreamOpen(const char *name);

View file

@ -32,8 +32,7 @@
// demuxed and decoded separately on the main thread and fed to an SDL3_mixer track.
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unistd.h> // unlink
#ifdef __linux__
#include <dlfcn.h>
#endif
@ -77,6 +76,9 @@ typedef struct iso639_lang_t iso639_lang_t;
#define DEFAULT_FPS_NUMERATOR 30
#define DEFAULT_FPS_DENOMINATOR 1
#define ERROR_BUFFER_SIZE 1024
#define FNV_OFFSET_BASIS 2166136261u // 32 bit FNV-1a, for index cache names
#define FNV_PRIME 16777619u
#define FRAME_TABLE_INITIAL_CAPACITY 4096
#define INDEX_MAGIC "SINGEIDX"
#define INDEX_VERSION 1
#define KEYFRAME_WARN_SECONDS 2.0 // Seeks decode forward from the previous keyframe
@ -86,11 +88,17 @@ typedef struct iso639_lang_t iso639_lang_t;
#define PERCENT_TO_SCALE 0.01f
#define PLANE_COUNT 3 // Y, U, V
#define SCALER_PLANES 4 // libswscale reads four plane pointers and strides whatever the format
#define SEEK_FORWARD_LIMIT 64 // Decode forward rather than seek when the wanted frame is this close
#define SEEK_RETRY_MAX 3 // Keyframes to back up when a seek lands past its target
#define STRIDE_ALIGNMENT 64 // libswscale stores with aligned vector instructions; rows must start aligned
// What one decode request came to.
typedef enum DecodeResultE {
DECODE_OK, // The wanted frame is in the back buffer
DECODE_NONE, // Nothing decodable there: past the end, or a seek that could not be satisfied
DECODE_ERROR // The decoder failed; threadErrMsg says how
} DecodeResultE;
// One video frame in display order.
typedef struct FrameInfoS {
int64_t pts; // In the video stream's time base
@ -152,6 +160,7 @@ typedef struct VideoPlayerS {
bool videoDrained;
bool packetPending; // videoPacket holds data the decoder refused (EAGAIN)
bool hwReported; // First hardware frame has been traced
bool videoHasAudio; // The video file carries an audio stream of its own
// Audio demuxer, decoder, and resampler. Owned by the main thread.
AVFormatContext *audioFormat;
@ -212,7 +221,7 @@ static int64_t _avioSeek(void *opaque, int64_t offset, int whence);
static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *indexPath);
static int _compareFrames(const void *a, const void *b); // qsort callback. Not changing int.
static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *frame);
static bool _decodeFrame(VideoPlayerT *v, int64_t want);
static DecodeResultE _decodeFrame(VideoPlayerT *v, int64_t want);
static int _decoderThread(void *data); // SDL thread entry. Not changing int.
static void _feedAudio(VideoPlayerT *v);
static int64_t _findFrameIndex(VideoPlayerT *v, int64_t pts);
@ -361,13 +370,9 @@ static int64_t _audioClock(VideoPlayerT *v, uint64_t now) {
static void _audioCloseTrack(VideoPlayerT *v) {
if (v->audioCodec) {
avcodec_free_context(&v->audioCodec);
}
if (v->swr) {
swr_free(&v->swr);
}
}
// Resamples one decoded audio frame to the track's format and queues it, dropping whatever lies
@ -425,9 +430,6 @@ static void _audioSeek(VideoPlayerT *v, int64_t ms) {
}
avcodec_flush_buffers(v->audioCodec);
_audioSetupResampler(v);
if (v->audioPacket->data) {
av_packet_unref(v->audioPacket);
}
v->audioSkipUntilMs = ms;
v->audioNextMs = ms;
v->audioEof = false;
@ -483,16 +485,13 @@ static void _audioSelectTrack(VideoPlayerT *v, int32_t track) {
static void _audioSetupResampler(VideoPlayerT *v) {
AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO;
if (v->swr) {
swr_free(&v->swr);
}
if ((swr_alloc_set_opts2(&v->swr, &stereo, AV_SAMPLE_FMT_FLT, v->audioSpec.freq, &v->audioCodec->ch_layout, v->audioCodec->sample_fmt, v->audioCodec->sample_rate, 0, NULL) < 0) || (swr_init(v->swr) < 0)) {
utilDie("Unable to create the audio resampler.");
}
}
// Loads the frame table from the cache, or demuxes the file once to build it and caches the result.
static int _avioRead(void *opaque, uint8_t *buffer, int size) {
int64_t got = vfsStreamRead((VfsStreamT *)opaque, buffer, size);
@ -511,6 +510,7 @@ static int64_t _avioSeek(void *opaque, int64_t offset, int whence) {
}
// Loads the frame table from the cache, or demuxes the file once to build it and caches the result.
static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *indexPath) {
AVFormatContext *format = NULL;
AVPacket *packet = NULL;
@ -568,7 +568,7 @@ static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *
while (av_read_frame(format, packet) >= 0) {
if (packet->stream_index == v->videoStream) {
if (v->frameCount == capacity) {
capacity = (capacity == 0) ? 4096 : capacity * 2;
capacity = (capacity == 0) ? FRAME_TABLE_INITIAL_CAPACITY : capacity * 2;
grown = realloc(v->frames, sizeof(FrameInfoT) * (size_t)capacity);
if (!grown) {
utilDie("Unable to allocate the frame table.");
@ -581,6 +581,8 @@ static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *
pts = (lastPts == AV_NOPTS_VALUE) ? 0 : lastPts + duration;
}
lastPts = pts;
// The whole entry, padding included, is written to the cache file.
memset(&v->frames[v->frameCount], 0, sizeof(FrameInfoT));
v->frames[v->frameCount].pts = pts;
v->frames[v->frameCount].keyframe = (packet->flags & AV_PKT_FLAG_KEY) != 0;
v->frameCount++;
@ -619,7 +621,7 @@ static int _compareFrames(const void *a, const void *b) {
// Puts a decoded frame into our buffer in the player's format, converting only when the decoder's
// output differs (10 bit sources, 4:2:2, hardware formats).
// output differs (10 bit sources, 4:2:2, full range JPEG sources, hardware formats).
static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *frame) {
enum AVPixelFormat wanted = v->rgb ? AV_PIX_FMT_BGRA : AV_PIX_FMT_YUV420P;
int32_t planes = v->rgb ? 1 : PLANE_COUNT;
@ -627,7 +629,7 @@ static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *
int32_t rows = 0;
int32_t bytes = 0;
if ((frame->format == wanted) || (!v->rgb && (frame->format == AV_PIX_FMT_YUVJ420P))) {
if (frame->format == wanted) {
for (plane = 0; plane < planes; plane++) {
rows = (plane == 0) ? v->height : (v->height + 1) / 2;
bytes = v->rgb ? v->width * BYTES_PER_PIXEL : ((plane == 0) ? v->width : (v->width + 1) / 2);
@ -643,19 +645,21 @@ static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *
}
// Decodes frame "want" into the back buffer. Runs on the decoder thread.
static bool _decodeFrame(VideoPlayerT *v, int64_t want) {
// Decodes frame "want" into the back buffer. Runs on the decoder thread. On DECODE_ERROR the
// message is in threadErrMsg; the caller raises threadError under the lock, which publishes both.
static DecodeResultE _decodeFrame(VideoPlayerT *v, int64_t want) {
int64_t keyframe = want;
int64_t index = 0;
int64_t ts = 0;
int32_t retries = 0;
int32_t result = 0;
// Decide between decoding forward and seeking.
// Decide between decoding forward and seeking. Decoding forward from where the decoder sits is
// never more work than seeking back to a keyframe at or before it, however far ahead the frame is.
while ((keyframe > 0) && !v->frames[keyframe].keyframe) {
keyframe--;
}
if (v->videoDrained || (v->nextDecodeFrame < 0) || (want < v->nextDecodeFrame) || (keyframe > v->nextDecodeFrame) || (want - v->nextDecodeFrame > SEEK_FORWARD_LIMIT)) {
if (v->videoDrained || (v->nextDecodeFrame < 0) || (want < v->nextDecodeFrame) || (keyframe > v->nextDecodeFrame)) {
_seekVideo(v, keyframe);
}
@ -666,25 +670,27 @@ static bool _decodeFrame(VideoPlayerT *v, int64_t want) {
index = (ts == AV_NOPTS_VALUE) ? v->nextDecodeFrame : _findFrameIndex(v, ts);
if (index == want) {
if (v->hwDevice && (v->videoFrame->format == v->hwPixelFormat)) {
// Pull the picture out of the GPU; it arrives as NV12 and is converted like any other format.
// Pull the picture out of the GPU; it arrives as NV12 and is converted like any other
// format. hwFrame keeps its buffers between frames so the read back does not allocate.
if ((v->hwFrame->width != v->videoFrame->width) || (v->hwFrame->height != v->videoFrame->height)) {
av_frame_unref(v->hwFrame);
}
if (av_hwframe_transfer_data(v->hwFrame, v->videoFrame, 0) < 0) {
snprintf(v->threadErrMsg, sizeof(v->threadErrMsg), "Unable to read back a hardware decoded frame.");
v->threadError = true;
av_frame_unref(v->videoFrame);
return false;
return DECODE_ERROR;
}
if (!v->hwReported) {
v->hwReported = true;
utilTrace("Video %d: first hardware frame read back as %s %dx%d", v->id, av_get_pix_fmt_name((enum AVPixelFormat)v->hwFrame->format), v->hwFrame->width, v->hwFrame->height);
}
_convertFrame(v, &v->back, v->hwFrame);
av_frame_unref(v->hwFrame);
} else {
_convertFrame(v, &v->back, v->videoFrame);
}
av_frame_unref(v->videoFrame);
v->nextDecodeFrame = want + 1;
return true;
return DECODE_OK;
}
if (index > want) {
av_frame_unref(v->videoFrame);
@ -698,9 +704,9 @@ static bool _decodeFrame(VideoPlayerT *v, int64_t want) {
_seekVideo(v, keyframe);
continue;
}
// Best we can do: show what we have.
// Nothing decodable at that frame; give it up rather than seek for it forever.
v->nextDecodeFrame = index + 1;
return false;
return DECODE_NONE;
}
// Earlier than wanted: keep going.
av_frame_unref(v->videoFrame);
@ -709,12 +715,11 @@ static bool _decodeFrame(VideoPlayerT *v, int64_t want) {
}
if (result == AVERROR_EOF) {
v->videoDrained = true;
return false;
return DECODE_NONE;
}
if (result != AVERROR(EAGAIN)) {
av_strerror(result, v->threadErrMsg, sizeof(v->threadErrMsg));
v->threadError = true;
return false;
return DECODE_ERROR;
}
// The decoder wants data.
@ -737,21 +742,21 @@ static bool _decodeFrame(VideoPlayerT *v, int64_t want) {
v->packetPending = false;
} else if (result != AVERROR(EAGAIN)) {
av_strerror(result, v->threadErrMsg, sizeof(v->threadErrMsg));
v->threadError = true;
av_packet_unref(v->videoPacket);
v->packetPending = false;
return false;
return DECODE_ERROR;
}
}
}
// Decodes whatever frame was last requested into the back buffer, then waits for the next request.
// Seeks decode forward from a keyframe, so this keeps long seeks off the main loop.
// Seeks decode forward from a keyframe, so this keeps long seeks off the main loop. A frame that
// cannot be decoded stays the pending one, so it is not asked for again until the clock moves on.
static int _decoderThread(void *data) {
VideoPlayerT *v = (VideoPlayerT *)data;
int64_t want = -1;
bool got = false;
DecodeResultE result = DECODE_NONE;
SDL_LockMutex(v->lock);
while (!v->quitThread) {
@ -764,16 +769,14 @@ static int _decoderThread(void *data) {
v->requestedFrame = -1;
SDL_UnlockMutex(v->lock);
got = _decodeFrame(v, want);
result = _decodeFrame(v, want);
SDL_LockMutex(v->lock);
if (got) {
if (result == DECODE_OK) {
v->back.frame = want;
v->backReady = true;
} else if (!v->threadError) {
// Nothing decodable there (past the end, or a seek that could not be satisfied): let the
// main thread ask again for whatever frame it wants next rather than hanging on this one.
v->pendingFrame = -1;
} else if (result == DECODE_ERROR) {
v->threadError = true;
}
}
SDL_UnlockMutex(v->lock);
@ -867,8 +870,17 @@ static AVFormatContext *_formatOpen(const char *filename) {
return NULL;
}
buffer = (unsigned char *)av_malloc(AVIO_BUFFER_BYTES);
if (buffer == NULL) {
utilDie("Unable to allocate the read buffer for %s.", filename);
}
io = avio_alloc_context(buffer, AVIO_BUFFER_BYTES, 0, stream, _avioRead, NULL, _avioSeek);
if (io == NULL) {
utilDie("Unable to allocate the I/O context for %s.", filename);
}
format = avformat_alloc_context();
if (format == NULL) {
utilDie("Unable to allocate the demuxer for %s.", filename);
}
format->pb = io;
format->flags |= AVFMT_FLAG_CUSTOM_IO;
if (avformat_open_input(&format, filename, NULL, NULL) < 0) {
@ -900,12 +912,18 @@ static VideoPlayerT *_getPlayer(int32_t playerHandle, const char *caller) {
}
// Returns a new string the caller must free.
// The index cache file for a video: its base name plus a hash of the whole name, so two videos
// with the same base name in one game do not take turns rebuilding each other's table. Returns a
// new string the caller must free.
static char *_indexFileName(const char *filename, const char *indexPath) {
char *name = utilCreateString("%s%c%s.index", indexPath, utilGetPathSeparator(), utilGetLastPathComponent(filename));
char *name = NULL;
uint32_t hash = FNV_OFFSET_BASIS;
size_t i = 0;
for (i = 0; filename[i] != 0; i++) {
hash = (hash ^ (uint8_t)filename[i]) * FNV_PRIME;
}
name = utilCreateString("%s%c%s-%08" PRIx32 ".index", indexPath, utilGetPathSeparator(), utilGetLastPathComponent(filename), hash);
utilFixPathSeparators(&name, false);
return name;
@ -913,11 +931,10 @@ static char *_indexFileName(const char *filename, const char *indexPath) {
// Opens the audio side of a file (the video file, or the separate audio file of an old framefile) and
// lists its tracks.
// lists its tracks. The track table is sized for every stream; only the audio ones fill it.
static void _loadAudio(VideoPlayerT *v, const char *filename) {
AVDictionaryEntry *tag = NULL;
int32_t x = 0;
int32_t count = 0;
v->audioFormat = _formatOpen(filename);
if (v->audioFormat == NULL) {
@ -926,17 +943,7 @@ static void _loadAudio(VideoPlayerT *v, const char *filename) {
if (avformat_find_stream_info(v->audioFormat, NULL) < 0) {
utilDie("Unable to read stream information from %s.", filename);
}
for (x = 0; x < (int32_t)v->audioFormat->nb_streams; x++) {
if (v->audioFormat->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
count++;
}
}
if (count == 0) {
_formatClose(&v->audioFormat);
return;
}
v->audio = (AudioTrackT *)calloc((size_t)count, sizeof(AudioTrackT));
v->audio = (AudioTrackT *)calloc((size_t)v->audioFormat->nb_streams + 1, sizeof(AudioTrackT));
if (!v->audio) {
utilDie("Unable to allocate audio tracks.");
}
@ -950,6 +957,12 @@ static void _loadAudio(VideoPlayerT *v, const char *filename) {
v->audioSourceCount++;
}
}
if (v->audioSourceCount == 0) {
free(v->audio);
v->audio = NULL;
_formatClose(&v->audioFormat);
return;
}
v->audioPacket = av_packet_alloc();
v->audioFrame = av_frame_alloc();
@ -1053,6 +1066,9 @@ static void _openVideo(VideoPlayerT *v, const char *filename) {
}
for (x = 0; x < (int32_t)v->videoFormat->nb_streams; x++) {
v->videoFormat->streams[x]->discard = (x == v->videoStream) ? AVDISCARD_DEFAULT : AVDISCARD_ALL;
if (v->videoFormat->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
v->videoHasAudio = true;
}
}
stream = v->videoFormat->streams[v->videoStream];
v->videoTimeBase = stream->time_base;
@ -1185,8 +1201,11 @@ static void _seekVideo(VideoPlayerT *v, int64_t keyframe) {
// libavcodec asks which of the formats it can produce we want; take the hardware one when offered.
// The list is every hardware format the build knows followed by the software one, so the fallback
// is the first entry that is not a hardware surface, not the first entry.
static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats) {
VideoPlayerT *v = (VideoPlayerT *)codec->opaque;
const AVPixFmtDescriptor *desc = NULL;
int32_t x = 0;
for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
@ -1194,8 +1213,13 @@ static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum A
return formats[x];
}
}
// The hardware declined this stream: fall back to the decoder's first software format.
utilTrace("Video %d: hardware decoder declined the stream; decoding in software.", v->id);
for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
desc = av_pix_fmt_desc_get(formats[x]);
if ((desc != NULL) && !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
return formats[x];
}
}
return formats[0];
}
@ -1516,8 +1540,11 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch
utilDie("%s", SDL_GetError());
}
// Audio: a mixer track that never halts, fed from its own demuxer on the main thread.
// Audio: a mixer track that never halts, fed from its own demuxer on the main thread. A silent
// video file is not demuxed a second time to find that out.
if ((audioFilename != NULL) || v->videoHasAudio) {
_loadAudio(v, audioFilename ? audioFilename : videoFilename);
}
if (v->audioSourceCount > 0) {
v->track = MIX_CreateTrack(_mixer);
if (!v->track) {
@ -1603,12 +1630,6 @@ void videoSetAudioDelay(int32_t milliseconds) {
}
// Chosen before any video loads; existing players keep whatever they opened with.
void videoSetHardwareDecoding(bool enabled) {
_hardwareDecoding = enabled;
}
void videoSetAudioTrack(int32_t playerHandle, int32_t track) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetAudioTrack");
@ -1623,6 +1644,12 @@ void videoSetAudioTrack(int32_t playerHandle, int32_t track) {
}
// Chosen before any video loads; existing players keep whatever they opened with.
void videoSetHardwareDecoding(bool enabled) {
_hardwareDecoding = enabled;
}
void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetVolume");
MIX_StereoGains gains;

View file

@ -45,11 +45,11 @@ int64_t videoGetFrameCount(int32_t playerHandle);
int32_t videoGetHeight(int32_t playerHandle);
const char *videoGetLanguage(int32_t playerHandle, int32_t audioTrack);
const char *videoGetLanguageDescription(const char *languageCode);
MIX_Mixer *videoGetMixer(void);
bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b);
bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch);
void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent);
int32_t videoGetWidth(int32_t playerHandle);
MIX_Mixer *videoGetMixer(void);
void videoInit(MIX_Mixer *mixer);
bool videoIsPlaying(int32_t playerHandle);
void videoLockAudio(void);