diff --git a/CHANGELOG b/CHANGELOG index e23eb7054..eec3f485e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 -.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 .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 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a1af5bbd..1ee464f47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/cmake/shaderHeader.cmake b/cmake/shaderHeader.cmake index b79e0761b..b9e531c1e 100644 --- a/cmake/shaderHeader.cmake +++ b/cmake/shaderHeader.cmake @@ -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= -DSOURCE= -DOUTPUT= -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() diff --git a/docs/Manual.adoc b/docs/Manual.adoc index acabe3027..65796c2a2 100644 --- a/docs/Manual.adoc +++ b/docs/Manual.adoc @@ -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 (`.index`). Indexing is one pass over the file +to the game's other data (`-.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:* <>, <>, <> @@ -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:* <>, <>, <> @@ -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:* <>, <>, <> @@ -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:* <>, <>, <> @@ -4707,7 +4721,7 @@ Singe has two text renderers. The built-in console font is drawn by <>, <> @@ -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:* <>, <>, <> @@ -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:* <>, <> @@ -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 <>. 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:* <>, <>, <> @@ -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 <>). +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 <>). 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:* <> @@ -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 <>. +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 <>. [#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:* diff --git a/src/frameFile.c b/src/frameFile.c index 02f2d2cc2..bb1192821 100644 --- a/src/frameFile.c +++ b/src/frameFile.c @@ -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; } diff --git a/src/frameFile.h b/src/frameFile.h index 73c436860..8f36e3a94 100644 --- a/src/frameFile.h +++ b/src/frameFile.h @@ -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); diff --git a/src/hdr.c b/src/hdr.c index 94c4b2d31..f31752922 100644 --- a/src/hdr.c +++ b/src/hdr.c @@ -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); + } + ready = true; } - return SDL_powf((v + 0.055f) / 1.055f, 2.4f); + 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; } diff --git a/src/ktx2Basis.cpp b/src/ktx2Basis.cpp index 039e23a18..7ee73e666 100644 --- a/src/ktx2Basis.cpp +++ b/src/ktx2Basis.cpp @@ -22,6 +22,7 @@ // Singe - KTX2 textures on the Basis Universal transcoder, behind the C interface in ktx2.h. +#include #include #include #include @@ -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; diff --git a/src/main.c b/src/main.c index 89013a4c6..fe5256019 100644 --- a/src/main.c +++ b/src/main.c @@ -22,6 +22,7 @@ #include +#include #include #include #include @@ -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 *name = NULL; - char *path = 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; diff --git a/src/main.h b/src/main.h index 9928361e2..d24459831 100644 --- a/src/main.h +++ b/src/main.h @@ -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 diff --git a/src/math3d.c b/src/math3d.c index 8093e5fd8..a6c25ba8e 100644 --- a/src/math3d.c +++ b/src/math3d.c @@ -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); diff --git a/src/math3d.h b/src/math3d.h index 859269f6b..40646feda 100644 --- a/src/math3d.h +++ b/src/math3d.h @@ -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); diff --git a/src/model.c b/src/model.c index 74db481bd..203f46162 100644 --- a/src/model.c +++ b/src/model.c @@ -42,15 +42,22 @@ #include "model.h" -#define NO_HANDLE -1 -#define ERROR_LENGTH 256 -#define MAX_MORPH_TARGETS 64 -#define MAX_LAYERS (ANIMATION_LAYERS + 1) // The base and the layers above it -#define POSE_TRANSLATION 1 -#define POSE_ROTATION 2 -#define POSE_SCALE 4 -#define POSE_MORPH 8 -#define BASE_LAYER 0 +#define NO_HANDLE -1 +#define ERROR_LENGTH 256 +#define MAX_MORPH_TARGETS 64 // Morph weights an animation may drive per node; scene.c's MAX_MORPHS (8) is how many one draw blends +#define MAX_LAYERS (ANIMATION_LAYERS + 1) // The base and the layers above it +#define POSE_TRANSLATION 1 +#define POSE_ROTATION 2 +#define POSE_SCALE 4 +#define POSE_MORPH 8 +#define BASE_LAYER 0 +#define CUBIC_SPLINE_STRIDE 3 // Output elements per keyframe: in-tangent, value, out-tangent +#define CUBIC_SPLINE_VALUE 1 // Which of the three is the value +#define MIN_TRIANGLE_INDICES 3 +#define BASE64_QUAD_CHARS 4 // Four base64 characters ... +#define BASE64_QUAD_BYTES 3 // ... hold three bytes +#define DATA_URI_PREFIX "data:" +#define SLOTS_MIN 8 // Models or instances allocated at once typedef struct PrimitiveS { @@ -63,19 +70,23 @@ typedef struct ModelMeshS { int32_t count; } ModelMeshT; +// A model template; used comes first so _allocSlot can find free slots. typedef struct ModelS { + bool used; cgltf_data *data; int32_t *materials; // Scene material per glTF material, NO_HANDLE when it failed ModelMeshT *meshes; // Per glTF mesh - bool used; + double *animationLengths; // Per animation: its last keyframe time + int32_t morphStride; // The most morph targets any of its primitives has } ModelT; -// One glTF node's transform and morph weights, and which of them a clip has driven. +// One glTF node's transform and which parts of it a clip has driven. A pose array holds one of +// these per glTF node followed by the nodes' morph weights, morphStride floats each (see _poseMorph), +// so a model with few or no morph targets pays for none. typedef struct PoseS { Vec3T translation; QuatT rotation; Vec3T scale; - float morph[MAX_MORPH_TARGETS]; int32_t morphCount; uint8_t animated; // POSE_* bits } PoseT; @@ -99,61 +110,89 @@ typedef struct LayerS { float fadeTime; bool fadeIn; // An upper layer starting from nothing ramps its weight instead float weight; - int32_t maskNode; // glTF node index whose subtree the layer is limited to, or -1 for all + int32_t maskNode; // glTF node index whose subtree the layer is limited to, or ANIMATION_NO_MASK for all } LayerT; // A model placed in the scene: which scene node stands for each glTF node, and the animation -// layers driving those nodes. +// layers driving those nodes. used comes first so _allocSlot can find free slots. typedef struct InstanceS { + bool used; int32_t model; int32_t root; uint32_t rootGeneration; + int32_t nodeCount; // glTF nodes int32_t *nodes; // Scene node per glTF node, NO_HANDLE where none was made + uint32_t *generations; // That node's generation when it was made; a reused handle is not ours + int32_t morphStride; // Morph weights per node in every pose array + size_t poseBytes; // One pose array's size LayerT layers[MAX_LAYERS]; PoseT *rest; // Per glTF node: the file's pose, animated bits clear PoseT *work; // Scratch: the frame's result ... PoseT *layerPose; // ... one layer's contribution ... PoseT *fromPose; // ... and what it fades from - bool used; } InstanceT; +// One glTF image decoded at most once while a model's materials load, however many textures use it. +typedef struct CachedImageS { + SDL_Surface *surface; + Ktx2ImageT ktx2; + bool surfaceTried; + bool ktx2Tried; +} CachedImageT; + +typedef struct ImageCacheS { + CachedImageT *items; // Per glTF image + const cgltf_image *images; // The model's, to index by +} ImageCacheT; + static void _advanceClip(const InstanceT *instance, ClipT *clip, double delta); -static void _applyTexture(int32_t handle, const cgltf_texture_view *view, MaterialMapE map, float strength); -static int32_t _allocInstance(void); -static int32_t _allocModel(void); -static void _attachSkins(ModelT *model, InstanceT *instance); +static PoseT *_allocPose(const InstanceT *instance); +static int32_t _allocSlot(void **items, int32_t *count, int32_t *capacity, size_t size); static double _animationLength(const cgltf_animation *animation); +static void _applyTexture(int32_t handle, ImageCacheT *cache, const cgltf_texture_view *view, MaterialMapE map, float strength); +static void _attachSkins(InstanceT *instance); static void _blendPose(const InstanceT *instance, PoseT *dst, const PoseT *src, float t, int32_t maskNode); static void _buildNode(ModelT *model, InstanceT *instance, const cgltf_node *node, int32_t parent); +static const Ktx2ImageT *_cachedKtx2(ImageCacheT *cache, const cgltf_image *image); +static SDL_Surface *_cachedSurface(ImageCacheT *cache, const cgltf_image *image); +static void _copyPose(const InstanceT *instance, PoseT *dst, const PoseT *src); static void _evaluate(InstanceT *instance); -static InstanceT *_findInstance(int32_t root); static void _fail(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +static InstanceT *_findInstance(int32_t root); +static void _findKeys(const cgltf_accessor *input, double time, int32_t *key, int32_t *next, float *t, float *span); +static void _freeImageCache(ImageCacheT *cache, int32_t count); static void _freeInstance(InstanceT *instance); static void _freeModel(ModelT *model); static void _freeStaleInstances(void); -static bool _inMask(const cgltf_data *data, int32_t index, int32_t maskNode); -static bool _layerPose(InstanceT *instance, LayerT *layer, PoseT *out); static bool _imageBytes(const cgltf_image *image, const uint8_t **bytes, size_t *size, void **decoded); -static SDL_Surface *_loadImage(const cgltf_image *image); -static int32_t _loadMaterial(const cgltf_material *material); +static bool _inMask(const cgltf_data *data, int32_t index, int32_t maskNode); +static int32_t _instanceNode(const InstanceT *instance, int32_t index); +static bool _layerPose(InstanceT *instance, LayerT *layer, PoseT *out); +static int32_t _loadMaterial(ImageCacheT *cache, const cgltf_material *material); static bool _loadMeshes(ModelT *model); static void _loadMorphs(int32_t mesh, const cgltf_mesh *source, const cgltf_primitive *primitive, int32_t vertexCount); -static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *primitive); +static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *primitive, int32_t jointLimit); +static int32_t _meshNode(const InstanceT *instance, int32_t index, int32_t which); +static int32_t _meshNodeCount(const InstanceT *instance, int32_t index); static int32_t _nodeIndex(const InstanceT *instance, int32_t node); +static float *_poseMorph(const InstanceT *instance, const PoseT *pose, int32_t index); static bool _readAttribute(const cgltf_accessor *accessor, float *out, int32_t components, int32_t count); static void _sampleChannel(const cgltf_animation_channel *channel, double time, float *out, int32_t components); static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pose); static void _sampleWeights(const cgltf_animation_channel *channel, double time, float *out, int32_t count); static void _snapshotPose(const InstanceT *instance, PoseT *pose); static bool _startClip(InstanceT *instance, LayerT *layer, int32_t index, bool loop, float speed, float fade); +static void _warn(const char *fmt, ...) __attribute__((format(printf, 1, 2))); static void _writePose(const InstanceT *instance, const PoseT *pose); static ModelT *_models; static int32_t _modelCount; +static int32_t _modelCapacity; static InstanceT *_instances; static int32_t _instanceCount; +static int32_t _instanceCapacity; static char _error[ERROR_LENGTH]; static uint64_t _lastTick; @@ -167,7 +206,7 @@ static void _advanceClip(const InstanceT *instance, ClipT *clip, double delta) { if ((clip->animation == NO_HANDLE) || !clip->playing) { return; } - length = _animationLength(&_models[instance->model].data->animations[clip->animation]); + length = _models[instance->model].animationLengths[clip->animation]; clip->time += delta * clip->speed; if (clip->time > length) { if (clip->loop && (length > 0.0)) { @@ -180,97 +219,41 @@ static void _advanceClip(const InstanceT *instance, ClipT *clip, double delta) { } -// A material's texture from a glTF texture view: a KTX2 (KHR_texture_basisu) image transcoded to -// the GPU's block format, or an ordinary image decoded to a surface. -static void _applyTexture(int32_t handle, const cgltf_texture_view *view, MaterialMapE map, float strength) { - const cgltf_texture *texture = view->texture; - SDL_Surface *image; +// A zeroed pose array for the instance: its nodes' poses and their morph weights. +static PoseT *_allocPose(const InstanceT *instance) { + PoseT *pose = SDL_calloc(1, SDL_max(instance->poseBytes, 1)); - if (texture == NULL) { - return; + if (pose == NULL) { + utilDie("Out of memory instancing a model."); } - if (texture->has_basisu && (texture->basisu_image != NULL)) { - const uint8_t *bytes = NULL; - size_t size = 0; - void *decoded = NULL; - Ktx2ImageT ktx2; + return pose; +} - if (_imageBytes(texture->basisu_image, &bytes, &size, &decoded)) { - if (ktx2Transcode(bytes, size, sceneCompressedFormat(), &ktx2)) { - materialSetMap(handle, map, &ktx2, strength); - ktx2Free(&ktx2); - } else { - _fail("%s", SDL_GetError()); + +// A free slot in an array of structs whose first member is a bool "used" flag, growing the array +// by doubling when every slot is taken. The slot comes back zeroed and marked used. +static int32_t _allocSlot(void **items, int32_t *count, int32_t *capacity, size_t size) { + int32_t x; + uint8_t *item; + + for (x = 0; x < *count; x++) { + if (!*(bool *)((uint8_t *)*items + (size_t)x * size)) { + break; + } + } + if (x == *count) { + if (*count == *capacity) { + *capacity = SDL_max(*capacity * 2, SLOTS_MIN); + *items = SDL_realloc(*items, size * (size_t)*capacity); + if (*items == NULL) { + utilDie("Out of memory allocating a model."); } - free(decoded); } - return; + (*count)++; } - if (texture->image == NULL) { - return; - } - image = _loadImage(texture->image); - if (image == NULL) { - return; - } - switch (map) { - case MAP_NORMAL: - materialSetNormalMap(handle, image, strength); - break; - case MAP_OCCLUSION: - materialSetOcclusionMap(handle, image, strength); - break; - case MAP_METALLIC_ROUGHNESS: - materialSetMetallicRoughnessMap(handle, image); - break; - case MAP_EMISSIVE: - materialSetEmissiveMap(handle, image); - break; - default: - materialSetTexture(handle, image); - break; - } - SDL_DestroySurface(image); -} - -static int32_t _allocInstance(void) { - int32_t x; - - for (x = 0; x < _instanceCount; x++) { - if (!_instances[x].used) { - break; - } - } - if (x == _instanceCount) { - _instances = SDL_realloc(_instances, sizeof(InstanceT) * (size_t)(_instanceCount + 1)); - if (_instances == NULL) { - utilDie("Out of memory allocating a model instance."); - } - _instanceCount++; - } - memset(&_instances[x], 0, sizeof(InstanceT)); - _instances[x].used = true; - return x; -} - - -static int32_t _allocModel(void) { - int32_t x; - - for (x = 0; x < _modelCount; x++) { - if (!_models[x].used) { - break; - } - } - if (x == _modelCount) { - _models = SDL_realloc(_models, sizeof(ModelT) * (size_t)(_modelCount + 1)); - if (_models == NULL) { - utilDie("Out of memory allocating a model."); - } - _modelCount++; - } - memset(&_models[x], 0, sizeof(ModelT)); - _models[x].used = true; + item = (uint8_t *)*items + (size_t)x * size; + memset(item, 0, size); + *(bool *)item = true; return x; } @@ -282,24 +265,98 @@ static double _animationLength(const cgltf_animation *animation) { for (x = 0; x < (int32_t)animation->channels_count; x++) { const cgltf_accessor *input = animation->channels[x].sampler->input; + float last = 0.0f; - if (input->has_max && (input->max[0] > length)) { - length = input->max[0]; + if ((input->count > 0) && cgltf_accessor_read_float(input, input->count - 1, &last, 1) && (last > length)) { + length = last; } } return length; } -// dst = lerp(dst, src, t) for the parts src animates, within the mask (a glTF node index, or -1 -// for everything). A part dst does not animate yet holds the rest pose, so a clip that moves -// only some nodes fades in from rest. +// A material's texture from a glTF texture view: a KTX2 (KHR_texture_basisu) image transcoded to +// the GPU's block format, or an ordinary image decoded to a surface. +static void _applyTexture(int32_t handle, ImageCacheT *cache, const cgltf_texture_view *view, MaterialMapE map, float strength) { + const cgltf_texture *texture = view->texture; + SDL_Surface *image; + + if (texture == NULL) { + return; + } + if (view->texcoord != 0) { + _warn("A texture uses TEXCOORD_%d; only the first set is read.", (int32_t)view->texcoord); + } + if (view->has_transform) { + _warn("A texture's KHR_texture_transform is ignored."); + } + if (texture->has_basisu && (texture->basisu_image != NULL)) { + const Ktx2ImageT *ktx2 = _cachedKtx2(cache, texture->basisu_image); + + if (ktx2 != NULL) { + materialSetMap(handle, map, ktx2, strength); + } + return; + } + if (texture->image == NULL) { + return; + } + image = _cachedSurface(cache, texture->image); + if (image == NULL) { + return; + } + materialSetMapSurface(handle, map, image, strength); +} + + +// Once every node exists: each skinned glTF node's mesh node(s) get the skin's joints, resolved +// to this instance's scene nodes, with the inverse bind matrices (identity when the file has none). +static void _attachSkins(InstanceT *instance) { + cgltf_data *data = _models[instance->model].data; + int32_t x; + int32_t y; + + for (x = 0; x < instance->nodeCount; x++) { + const cgltf_skin *skin = data->nodes[x].skin; + int32_t *joints; + Mat4T *inverseBind; + int32_t count; + int32_t meshNodes = _meshNodeCount(instance, x); + + if ((skin == NULL) || (meshNodes == 0)) { + continue; + } + count = (int32_t)skin->joints_count; // At most MAX_JOINTS; modelLoad refuses more + joints = SDL_malloc(sizeof(int32_t) * (size_t)SDL_max(count, 1)); + inverseBind = SDL_malloc(sizeof(Mat4T) * (size_t)SDL_max(count, 1)); + if ((joints == NULL) || (inverseBind == NULL)) { + utilDie("Out of memory attaching a skin."); + } + for (y = 0; y < count; y++) { + joints[y] = instance->nodes[skin->joints[y] - data->nodes]; + inverseBind[y] = mat4Identity(); + if (skin->inverse_bind_matrices != NULL) { + cgltf_accessor_read_float(skin->inverse_bind_matrices, (cgltf_size)y, inverseBind[y].m, 16); + } + } + for (y = 0; y < meshNodes; y++) { + nodeSetSkin(_meshNode(instance, x, y), joints, inverseBind, count); + } + SDL_free(joints); + SDL_free(inverseBind); + } +} + + +// dst = lerp(dst, src, t) for the parts src animates, within the mask (a glTF node index, or +// ANIMATION_NO_MASK for everything). A part dst does not animate yet holds the rest pose, so a clip +// that moves only some nodes fades in from rest. static void _blendPose(const InstanceT *instance, PoseT *dst, const PoseT *src, float t, int32_t maskNode) { const cgltf_data *data = _models[instance->model].data; int32_t x; int32_t w; - for (x = 0; x < (int32_t)data->nodes_count; x++) { + for (x = 0; x < instance->nodeCount; x++) { const PoseT *s = &src[x]; PoseT *d = &dst[x]; @@ -316,8 +373,11 @@ static void _blendPose(const InstanceT *instance, PoseT *dst, const PoseT *src, d->scale = vec3Lerp(d->scale, s->scale, t); } if (s->animated & POSE_MORPH) { + const float *sm = _poseMorph(instance, src, x); + float *dm = _poseMorph(instance, dst, x); + for (w = 0; w < s->morphCount; w++) { - d->morph[w] += (s->morph[w] - d->morph[w]) * t; + dm[w] += (sm[w] - dm[w]) * t; } d->morphCount = SDL_max(d->morphCount, s->morphCount); } @@ -326,53 +386,9 @@ static void _blendPose(const InstanceT *instance, PoseT *dst, const PoseT *src, } -// Once every node exists: each skinned glTF node's mesh node(s) get the skin's joints, resolved -// to this instance's scene nodes, with the inverse bind matrices (identity when the file has none). -static void _attachSkins(ModelT *model, InstanceT *instance) { - cgltf_data *data = model->data; - int32_t x; - int32_t y; - - for (x = 0; x < (int32_t)data->nodes_count; x++) { - const cgltf_node *node = &data->nodes[x]; - const cgltf_skin *skin = node->skin; - int32_t *joints; - Mat4T *inverseBind; - int32_t count; - int32_t handle = instance->nodes[x]; - - if ((skin == NULL) || (node->mesh == NULL) || (handle == NO_HANDLE)) { - continue; - } - count = (int32_t)SDL_min(skin->joints_count, 128); - joints = SDL_malloc(sizeof(int32_t) * (size_t)SDL_max(count, 1)); - inverseBind = SDL_malloc(sizeof(Mat4T) * (size_t)SDL_max(count, 1)); - if ((joints == NULL) || (inverseBind == NULL)) { - utilDie("Out of memory attaching a skin."); - } - for (y = 0; y < count; y++) { - joints[y] = instance->nodes[skin->joints[y] - data->nodes]; - inverseBind[y] = mat4Identity(); - if (skin->inverse_bind_matrices != NULL) { - cgltf_accessor_read_float(skin->inverse_bind_matrices, (cgltf_size)y, inverseBind[y].m, 16); - } - } - // A multi-primitive mesh lives in child nodes; each gets the skin. - if (model->meshes[node->mesh - data->meshes].count == 1) { - nodeSetSkin(handle, joints, inverseBind, count); - } else { - for (y = 0; y < nodeGetChildCount(handle); y++) { - nodeSetSkin(nodeGetChild(handle, y), joints, inverseBind, count); - } - } - SDL_free(joints); - SDL_free(inverseBind); - } -} - - // A scene node for one glTF node and, recursively, its children. A mesh with several primitives -// becomes child nodes, one per primitive, so each can have its own material. +// becomes child nodes, one per primitive, so each can have its own material; they are made before +// the glTF children, so they are always the first children (see _meshNode). static void _buildNode(ModelT *model, InstanceT *instance, const cgltf_node *node, int32_t parent) { int32_t handle = nodeNew(parent); int32_t index = (int32_t)(node - model->data->nodes); @@ -386,7 +402,8 @@ static void _buildNode(ModelT *model, InstanceT *instance, const cgltf_node *nod if (handle == NO_HANDLE) { return; } - instance->nodes[index] = handle; + instance->nodes[index] = handle; + instance->generations[index] = nodeGetGeneration(handle); // Exporters often leave nodes unnamed but name the mesh; scripts find nodes by name. nodeSetName(handle, (node->name != NULL) ? node->name : ((node->mesh != NULL) ? node->mesh->name : NULL)); if (node->has_matrix) { @@ -414,21 +431,22 @@ static void _buildNode(ModelT *model, InstanceT *instance, const cgltf_node *nod mesh = &model->meshes[node->mesh - model->data->meshes]; if (mesh->count == 1) { nodeSetMesh(handle, mesh->primitives[0].mesh, mesh->primitives[0].material); - for (w = 0; w < weightCount; w++) { - nodeSetMorphWeight(handle, w, weights[w]); - } } else { for (x = 0; x < mesh->count; x++) { int32_t child = nodeNew(handle); if (child != NO_HANDLE) { nodeSetMesh(child, mesh->primitives[x].mesh, mesh->primitives[x].material); - for (w = 0; w < weightCount; w++) { - nodeSetMorphWeight(child, w, weights[w]); - } } } } + for (x = 0; x < _meshNodeCount(instance, index); x++) { + int32_t target = _meshNode(instance, index, x); + + for (w = 0; w < weightCount; w++) { + nodeSetMorphWeight(target, w, weights[w]); + } + } } if (node->light != NULL) { LightTypeE type = LIGHT_POINT; @@ -439,7 +457,8 @@ static void _buildNode(ModelT *model, InstanceT *instance, const cgltf_node *nod type = LIGHT_SPOT; } lightAttach(handle, type); - lightSetColor(handle, (uint8_t)(node->light->color[0] * 255.0f), (uint8_t)(node->light->color[1] * 255.0f), (uint8_t)(node->light->color[2] * 255.0f)); + // glTF colours are linear; a range of 0 means unbounded in both worlds. + lightSetColorLinear(handle, node->light->color[0], node->light->color[1], node->light->color[2]); lightSetIntensity(handle, node->light->intensity); lightSetRange(handle, node->light->range); if (type == LIGHT_SPOT) { @@ -452,29 +471,60 @@ static void _buildNode(ModelT *model, InstanceT *instance, const cgltf_node *nod } -// The instance whose root is this node, or NULL (a reused handle is not a match). -static InstanceT *_findInstance(int32_t root) { - int32_t x; +// The image transcoded from KTX2, done once per image; NULL when it cannot be. +static const Ktx2ImageT *_cachedKtx2(ImageCacheT *cache, const cgltf_image *image) { + CachedImageT *cached = &cache->items[image - cache->images]; + const uint8_t *bytes = NULL; + size_t size = 0; + void *decoded = NULL; - for (x = 0; x < _instanceCount; x++) { - if (_instances[x].used && (_instances[x].root == root) && nodeValid(root) && (nodeGetGeneration(root) == _instances[x].rootGeneration)) { - return &_instances[x]; + if (!cached->ktx2Tried) { + cached->ktx2Tried = true; + if (_imageBytes(image, &bytes, &size, &decoded)) { + if (!ktx2Transcode(bytes, size, sceneCompressedFormat(), &cached->ktx2)) { + _warn("%s", SDL_GetError()); + } + free(decoded); } } - return NULL; + return (cached->ktx2.levels != NULL) ? &cached->ktx2 : NULL; +} + + +// The image decoded with SDL_image, done once per image; NULL when it cannot be. +static SDL_Surface *_cachedSurface(ImageCacheT *cache, const cgltf_image *image) { + CachedImageT *cached = &cache->items[image - cache->images]; + const uint8_t *bytes = NULL; + size_t size = 0; + void *decoded = NULL; + + if (!cached->surfaceTried) { + cached->surfaceTried = true; + if (_imageBytes(image, &bytes, &size, &decoded)) { + cached->surface = IMG_Load_IO(SDL_IOFromConstMem(bytes, size), true); + if (cached->surface == NULL) { + _warn("Unable to decode an embedded image: %s", SDL_GetError()); + } + free(decoded); + } + } + return cached->surface; +} + + +static void _copyPose(const InstanceT *instance, PoseT *dst, const PoseT *src) { + memcpy(dst, src, instance->poseBytes); } -// Remembers why the last load failed, for the script's error message. // The frame's pose: the rest pose, the base layer over it, each upper layer blended in by its // weight, written to the nodes. Nothing is written when no layer contributes, so a stopped or // paused model keeps whatever pose the script or a ragdoll gives it. static void _evaluate(InstanceT *instance) { - const cgltf_data *data = _models[instance->model].data; - int32_t x; - bool any = false; + int32_t x; + bool any = false; - memcpy(instance->work, instance->rest, sizeof(PoseT) * data->nodes_count); + _copyPose(instance, instance->work, instance->rest); for (x = 0; x < MAX_LAYERS; x++) { LayerT *layer = &instance->layers[x]; float t = 1.0f; @@ -497,6 +547,7 @@ static void _evaluate(InstanceT *instance) { } +// Remembers why the last load failed, for the script's error message. static void _fail(const char *fmt, ...) { va_list args; @@ -507,6 +558,61 @@ static void _fail(const char *fmt, ...) { } +// The instance whose root is this node, or NULL (a reused handle is not a match). +static InstanceT *_findInstance(int32_t root) { + int32_t x; + + for (x = 0; x < _instanceCount; x++) { + if (_instances[x].used && (_instances[x].root == root) && nodeValid(root) && (nodeGetGeneration(root) == _instances[x].rootGeneration)) { + return &_instances[x]; + } + } + return NULL; +} + + +// The keyframe pair around a time, by binary search over the sampler's input: the last key at or +// before it (the first when the time is before them all), the key after (the same one at the end), +// how far between them (0..1) and the seconds between them. The input must have at least one key. +static void _findKeys(const cgltf_accessor *input, double time, int32_t *key, int32_t *next, float *t, float *span) { + int32_t count = (int32_t)input->count; + int32_t low = 0; + int32_t high = count - 1; + float t0; + float t1; + + while (low < high) { + int32_t mid = (low + high + 1) / 2; + + cgltf_accessor_read_float(input, (cgltf_size)mid, &t1, 1); + if (time < t1) { + high = mid - 1; + } else { + low = mid; + } + } + *key = low; + *next = SDL_min(low + 1, count - 1); + cgltf_accessor_read_float(input, (cgltf_size)low, &t0, 1); + cgltf_accessor_read_float(input, (cgltf_size)*next, &t1, 1); + *span = t1 - t0; + *t = (t1 > t0) ? (float)((time - t0) / (t1 - t0)) : 0.0f; + *t = SDL_clamp(*t, 0.0f, 1.0f); +} + + +static void _freeImageCache(ImageCacheT *cache, int32_t count) { + int32_t x; + + for (x = 0; x < count; x++) { + SDL_DestroySurface(cache->items[x].surface); + ktx2Free(&cache->items[x].ktx2); + } + SDL_free(cache->items); + cache->items = NULL; +} + + static void _freeInstance(InstanceT *instance) { int32_t x; @@ -518,6 +624,7 @@ static void _freeInstance(InstanceT *instance) { SDL_free(instance->layerPose); SDL_free(instance->fromPose); SDL_free(instance->nodes); + SDL_free(instance->generations); memset(instance, 0, sizeof(*instance)); } @@ -529,11 +636,11 @@ static void _freeModel(ModelT *model) { if (model->data != NULL) { for (x = 0; x < (int32_t)model->data->materials_count; x++) { - if (model->materials[x] != NO_HANDLE) { + if ((model->materials != NULL) && (model->materials[x] != NO_HANDLE)) { materialDelete(model->materials[x]); } } - for (x = 0; x < (int32_t)model->data->meshes_count; x++) { + for (x = 0; (model->meshes != NULL) && (x < (int32_t)model->data->meshes_count); x++) { for (y = 0; y < model->meshes[x].count; y++) { meshDelete(model->meshes[x].primitives[y].mesh); } @@ -543,6 +650,7 @@ static void _freeModel(ModelT *model) { } SDL_free(model->materials); SDL_free(model->meshes); + SDL_free(model->animationLengths); memset(model, 0, sizeof(*model)); } @@ -559,12 +667,47 @@ static void _freeStaleInstances(void) { } -// Decodes an embedded image (a buffer view, or a base64 data URI) with SDL_image. -// Whether a glTF node lies in the mask node's subtree (-1 means everything). +// Where an embedded image's bytes are: in a buffer view, or decoded from a data URI (freed by the +// caller through decoded). +static bool _imageBytes(const cgltf_image *image, const uint8_t **bytes, size_t *size, void **decoded) { + const char *comma; + cgltf_options options; + + *decoded = NULL; + if (image->buffer_view != NULL) { + *bytes = cgltf_buffer_view_data(image->buffer_view); + *size = image->buffer_view->size; + return *bytes != NULL; + } + if ((image->uri != NULL) && (strncmp(image->uri, DATA_URI_PREFIX, strlen(DATA_URI_PREFIX)) == 0) && ((comma = strchr(image->uri, ',')) != NULL)) { + size_t encoded = strlen(comma + 1); + + // Three bytes per four characters, whether or not the tail is padded with '='. + *size = encoded * BASE64_QUAD_BYTES / BASE64_QUAD_CHARS; + if ((encoded >= 1) && (comma[encoded] == '=')) { + (*size)--; + } + if ((encoded >= 2) && (comma[encoded - 1] == '=')) { + (*size)--; + } + memset(&options, 0, sizeof(options)); + if (cgltf_load_buffer_base64(&options, *size, comma + 1, decoded) != cgltf_result_success) { + _warn("Unable to decode an embedded image."); + return false; + } + *bytes = *decoded; + return true; + } + _warn("Image %s is not embedded in the model.", image->uri ? image->uri : "(unnamed)"); + return false; +} + + +// Whether a glTF node lies in the mask node's subtree (ANIMATION_NO_MASK means everything). static bool _inMask(const cgltf_data *data, int32_t index, int32_t maskNode) { const cgltf_node *node; - if (maskNode < 0) { + if (maskNode == ANIMATION_NO_MASK) { return true; } for (node = &data->nodes[index]; node != NULL; node = node->parent) { @@ -576,12 +719,23 @@ static bool _inMask(const cgltf_data *data, int32_t index, int32_t maskNode) { } +// The scene node standing for a glTF node, or NO_HANDLE when none was made or the script deleted +// it (a handle reused since is somebody else's). +static int32_t _instanceNode(const InstanceT *instance, int32_t index) { + int32_t node = instance->nodes[index]; + + if ((node == NO_HANDLE) || !nodeValid(node) || (nodeGetGeneration(node) != instance->generations[index])) { + return NO_HANDLE; + } + return node; +} + + // One layer's pose: its clip, crossfaded from the clip or the snapshot fading out. False when // the layer has nothing to say (no clip, or a clip paused or finished and not fading). static bool _layerPose(InstanceT *instance, LayerT *layer, PoseT *out) { - const cgltf_data *data = _models[instance->model].data; - int32_t x; - float t; + int32_t x; + float t; if ((layer->clip.animation == NO_HANDLE) || (!layer->clip.playing && (layer->fade <= 0.0f))) { return false; @@ -594,75 +748,23 @@ static bool _layerPose(InstanceT *instance, LayerT *layer, PoseT *out) { if (layer->from.animation != NO_HANDLE) { _samplePose(instance, &layer->from, instance->fromPose); } else { - memcpy(instance->fromPose, layer->snapshot, sizeof(PoseT) * data->nodes_count); + _copyPose(instance, instance->fromPose, layer->snapshot); } // What only the old side animates fades to rest, which the new side already holds there. - for (x = 0; x < (int32_t)data->nodes_count; x++) { + for (x = 0; x < instance->nodeCount; x++) { out[x].animated |= instance->fromPose[x].animated; } - _blendPose(instance, instance->fromPose, out, t, -1); - memcpy(out, instance->fromPose, sizeof(PoseT) * data->nodes_count); + _blendPose(instance, instance->fromPose, out, t, ANIMATION_NO_MASK); + _copyPose(instance, out, instance->fromPose); return true; } -// Where an embedded image's bytes are: in a buffer view, or decoded from a data URI (freed by the -// caller through decoded). -static bool _imageBytes(const cgltf_image *image, const uint8_t **bytes, size_t *size, void **decoded) { - const char *comma; - cgltf_options options; - - *decoded = NULL; - if (image->buffer_view != NULL) { - *bytes = (const uint8_t *)image->buffer_view->buffer->data + image->buffer_view->offset; - *size = image->buffer_view->size; - return true; - } - if ((image->uri != NULL) && (strncmp(image->uri, "data:", 5) == 0) && ((comma = strchr(image->uri, ',')) != NULL)) { - size_t encoded = strlen(comma + 1); - - *size = encoded / 4 * 3; - if ((encoded >= 1) && (comma[encoded] == '=')) { - (*size)--; - } - if ((encoded >= 2) && (comma[encoded - 1] == '=')) { - (*size)--; - } - memset(&options, 0, sizeof(options)); - if (cgltf_load_buffer_base64(&options, *size, comma + 1, decoded) != cgltf_result_success) { - _fail("Unable to decode an embedded image."); - return false; - } - *bytes = *decoded; - return true; - } - _fail("Image %s is not embedded in the model.", image->uri ? image->uri : "(unnamed)"); - return false; -} - - -static SDL_Surface *_loadImage(const cgltf_image *image) { - const uint8_t *bytes = NULL; - size_t size = 0; - void *decoded = NULL; - SDL_Surface *surface; - - if (!_imageBytes(image, &bytes, &size, &decoded)) { - return NULL; - } - surface = IMG_Load_IO(SDL_IOFromConstMem(bytes, size), true); - if (surface == NULL) { - _fail("Unable to decode an embedded image: %s", SDL_GetError()); - } - free(decoded); - return surface; -} - - // One scene material from a glTF material; returns NO_HANDLE on failure. -static int32_t _loadMaterial(const cgltf_material *material) { - int32_t handle = materialNew(); +static int32_t _loadMaterial(ImageCacheT *cache, const cgltf_material *material) { + int32_t handle = materialNew(); const float *color; + float strength = material->has_emissive_strength ? material->emissive_strength.emissive_strength : 1.0f; if (handle == NO_HANDLE) { return NO_HANDLE; @@ -672,13 +774,16 @@ static int32_t _loadMaterial(const cgltf_material *material) { materialSetColorLinear(handle, color[0], color[1], color[2], color[3]); materialSetMetallic(handle, material->pbr_metallic_roughness.metallic_factor); materialSetRoughness(handle, material->pbr_metallic_roughness.roughness_factor); - _applyTexture(handle, &material->pbr_metallic_roughness.base_color_texture, MAP_BASE, 1.0f); - _applyTexture(handle, &material->pbr_metallic_roughness.metallic_roughness_texture, MAP_METALLIC_ROUGHNESS, 1.0f); + _applyTexture(handle, cache, &material->pbr_metallic_roughness.base_color_texture, MAP_BASE, 1.0f); + _applyTexture(handle, cache, &material->pbr_metallic_roughness.metallic_roughness_texture, MAP_METALLIC_ROUGHNESS, 1.0f); + } + _applyTexture(handle, cache, &material->normal_texture, MAP_NORMAL, material->normal_texture.scale); + _applyTexture(handle, cache, &material->occlusion_texture, MAP_OCCLUSION, material->occlusion_texture.scale); + _applyTexture(handle, cache, &material->emissive_texture, MAP_EMISSIVE, 1.0f); + materialSetEmissiveLinear(handle, material->emissive_factor[0] * strength, material->emissive_factor[1] * strength, material->emissive_factor[2] * strength); + if (material->alpha_mode == cgltf_alpha_mode_mask) { + _warn("Material %s uses alpha masking (cutoff %.2f), which is drawn opaque.", material->name ? material->name : "(unnamed)", material->alpha_cutoff); } - _applyTexture(handle, &material->normal_texture, MAP_NORMAL, material->normal_texture.scale); - _applyTexture(handle, &material->occlusion_texture, MAP_OCCLUSION, material->occlusion_texture.scale); - _applyTexture(handle, &material->emissive_texture, MAP_EMISSIVE, 1.0f); - materialSetEmissiveLinear(handle, material->emissive_factor[0], material->emissive_factor[1], material->emissive_factor[2]); materialSetBlend(handle, material->alpha_mode == cgltf_alpha_mode_blend); materialSetDoubleSided(handle, material->double_sided); materialSetUnlit(handle, material->unlit); @@ -689,18 +794,30 @@ static int32_t _loadMaterial(const cgltf_material *material) { // Every material, then every primitive of every mesh. static bool _loadMeshes(ModelT *model) { cgltf_data *data = model->data; + ImageCacheT cache; int32_t x; int32_t y; model->materials = SDL_calloc(SDL_max(data->materials_count, 1), sizeof(int32_t)); model->meshes = SDL_calloc(SDL_max(data->meshes_count, 1), sizeof(ModelMeshT)); - if ((model->materials == NULL) || (model->meshes == NULL)) { + cache.items = SDL_calloc(SDL_max(data->images_count, 1), sizeof(CachedImageT)); + cache.images = data->images; + if ((model->materials == NULL) || (model->meshes == NULL) || (cache.items == NULL)) { utilDie("Out of memory loading a model."); } for (x = 0; x < (int32_t)data->materials_count; x++) { - model->materials[x] = _loadMaterial(&data->materials[x]); + model->materials[x] = _loadMaterial(&cache, &data->materials[x]); } + _freeImageCache(&cache, (int32_t)data->images_count); for (x = 0; x < (int32_t)data->meshes_count; x++) { + int32_t jointLimit = MAX_JOINTS; + + // The smallest skin driving this mesh bounds its vertices' joint ids. + for (y = 0; y < (int32_t)data->nodes_count; y++) { + if ((data->nodes[y].mesh == &data->meshes[x]) && (data->nodes[y].skin != NULL)) { + jointLimit = SDL_min(jointLimit, (int32_t)data->nodes[y].skin->joints_count); + } + } model->meshes[x].primitives = SDL_calloc(SDL_max(data->meshes[x].primitives_count, 1), sizeof(PrimitiveT)); if (model->meshes[x].primitives == NULL) { utilDie("Out of memory loading a model."); @@ -710,16 +827,17 @@ static bool _loadMeshes(ModelT *model) { int32_t mesh; if (primitive->type != cgltf_primitive_type_triangles) { - utilTrace("Model: mesh %s primitive %d is not triangles; skipped.", data->meshes[x].name ? data->meshes[x].name : "(unnamed)", y); + _warn("Mesh %s primitive %d is not triangles; skipped.", data->meshes[x].name ? data->meshes[x].name : "(unnamed)", y); continue; } - mesh = _loadPrimitive(&data->meshes[x], primitive); + mesh = _loadPrimitive(&data->meshes[x], primitive, jointLimit); if (mesh == NO_HANDLE) { return false; } model->meshes[x].primitives[model->meshes[x].count].mesh = mesh; model->meshes[x].primitives[model->meshes[x].count].material = (primitive->material != NULL) ? model->materials[primitive->material - data->materials] : NO_HANDLE; model->meshes[x].count++; + model->morphStride = SDL_max(model->morphStride, (int32_t)SDL_min(primitive->targets_count, MAX_MORPH_TARGETS)); } } return true; @@ -757,6 +875,7 @@ static void _loadMorphs(int32_t mesh, const cgltf_mesh *source, const cgltf_prim continue; } if (!_readAttribute(attribute->data, values, 3, vertexCount)) { + _warn("Mesh %s morph target %d has unreadable deltas.", source->name ? source->name : "(unnamed)", t); continue; } for (x = 0; x < vertexCount; x++) { @@ -776,52 +895,68 @@ static void _loadMorphs(int32_t mesh, const cgltf_mesh *source, const cgltf_prim // One primitive as a scene mesh: positions, normals (computed when absent), texture -// coordinates, joints and weights when skinned, and indices (generated when absent). -static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *primitive) { +// coordinates, joints (each below jointLimit) and weights when skinned, and indices (generated +// when absent). +static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *primitive, int32_t jointLimit) { const cgltf_accessor *positions = NULL; const cgltf_accessor *normals = NULL; const cgltf_accessor *uvs = NULL; const cgltf_accessor *joints = NULL; const cgltf_accessor *weights = NULL; const cgltf_accessor *tangents = NULL; + const char *name = source->name ? source->name : "(unnamed)"; SceneVertexT *vertices; uint32_t *indices; float *values; int32_t vertexCount; int32_t indexCount; int32_t x; + int32_t c; int32_t mesh; for (x = 0; x < (int32_t)primitive->attributes_count; x++) { const cgltf_attribute *attribute = &primitive->attributes[x]; - if ((attribute->type == cgltf_attribute_type_position) && (attribute->index == 0)) { + if (attribute->index != 0) { + continue; + } + if (attribute->type == cgltf_attribute_type_position) { positions = attribute->data; - } else if ((attribute->type == cgltf_attribute_type_normal) && (attribute->index == 0)) { + } else if (attribute->type == cgltf_attribute_type_normal) { normals = attribute->data; - } else if ((attribute->type == cgltf_attribute_type_texcoord) && (attribute->index == 0)) { + } else if (attribute->type == cgltf_attribute_type_texcoord) { uvs = attribute->data; - } else if ((attribute->type == cgltf_attribute_type_joints) && (attribute->index == 0)) { + } else if (attribute->type == cgltf_attribute_type_joints) { joints = attribute->data; - } else if ((attribute->type == cgltf_attribute_type_weights) && (attribute->index == 0)) { + } else if (attribute->type == cgltf_attribute_type_weights) { weights = attribute->data; - } else if ((attribute->type == cgltf_attribute_type_tangent) && (attribute->index == 0)) { + } else if (attribute->type == cgltf_attribute_type_tangent) { tangents = attribute->data; } } if (positions == NULL) { - _fail("A primitive has no positions."); + _fail("Mesh %s has a primitive with no positions.", name); return NO_HANDLE; } vertexCount = (int32_t)positions->count; indexCount = (primitive->indices != NULL) ? (int32_t)primitive->indices->count : vertexCount; - vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT)); - indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t)); - values = SDL_calloc((size_t)vertexCount * 4, sizeof(float)); + if ((vertexCount == 0) || (indexCount < MIN_TRIANGLE_INDICES)) { + _fail("Mesh %s has a primitive with no triangles.", name); + return NO_HANDLE; + } + vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT)); + indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t)); + values = SDL_calloc((size_t)vertexCount * 4, sizeof(float)); if ((vertices == NULL) || (indices == NULL) || (values == NULL)) { utilDie("Out of memory loading a model."); } - _readAttribute(positions, values, 3, vertexCount); + if (!_readAttribute(positions, values, 3, vertexCount)) { + _fail("Mesh %s has a primitive with unreadable positions.", name); + SDL_free(values); + SDL_free(indices); + SDL_free(vertices); + return NO_HANDLE; + } for (x = 0; x < vertexCount; x++) { vertices[x].position[0] = values[x * 3]; vertices[x].position[1] = values[x * 3 + 1]; @@ -850,19 +985,30 @@ static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *p vertices[x].tangent[3] = (values[x * 4 + 3] < 0.0f) ? -1.0f : 1.0f; } } - if ((joints != NULL) && (weights != NULL) && _readAttribute(weights, values, 4, vertexCount)) { + // Joint ids are integers read as floats; each must name a joint the skin has, or the shader + // reads past its matrices. + if ((joints != NULL) && (weights != NULL) && _readAttribute(joints, values, 4, vertexCount)) { for (x = 0; x < vertexCount; x++) { - cgltf_uint ids[4] = { 0, 0, 0, 0 }; + for (c = 0; c < 4; c++) { + int32_t id = (int32_t)values[x * 4 + c]; - cgltf_accessor_read_uint(joints, (cgltf_size)x, ids, 4); - vertices[x].joints[0] = (uint8_t)ids[0]; - vertices[x].joints[1] = (uint8_t)ids[1]; - vertices[x].joints[2] = (uint8_t)ids[2]; - vertices[x].joints[3] = (uint8_t)ids[3]; - vertices[x].weights[0] = values[x * 4]; - vertices[x].weights[1] = values[x * 4 + 1]; - vertices[x].weights[2] = values[x * 4 + 2]; - vertices[x].weights[3] = values[x * 4 + 3]; + if ((id < 0) || (id >= jointLimit)) { + _fail("Mesh %s names joint %d of a skin with %d.", name, id, jointLimit); + SDL_free(values); + SDL_free(indices); + SDL_free(vertices); + return NO_HANDLE; + } + vertices[x].joints[c] = (uint8_t)id; + } + } + if (_readAttribute(weights, values, 4, vertexCount)) { + for (x = 0; x < vertexCount; x++) { + vertices[x].weights[0] = values[x * 4]; + vertices[x].weights[1] = values[x * 4 + 1]; + vertices[x].weights[2] = values[x * 4 + 2]; + vertices[x].weights[3] = values[x * 4 + 3]; + } } } if (primitive->indices != NULL) { @@ -879,7 +1025,7 @@ static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *p } mesh = meshNewVertices(vertices, vertexCount, indices, indexCount, (joints != NULL) && (weights != NULL)); if (mesh == NO_HANDLE) { - _fail("Unable to upload a primitive."); + _fail("Unable to upload a primitive of mesh %s.", name); } else if (primitive->targets_count > 0) { _loadMorphs(mesh, source, primitive, vertexCount); } @@ -890,14 +1036,40 @@ static int32_t _loadPrimitive(const cgltf_mesh *source, const cgltf_primitive *p } -// Reads count elements of the accessor as floats, components per element. +// The which'th scene node carrying glTF node index's mesh (see _meshNodeCount): the node itself, +// or one of the primitive children _buildNode made first. NO_HANDLE when it is gone. +static int32_t _meshNode(const InstanceT *instance, int32_t index, int32_t which) { + int32_t node = _instanceNode(instance, index); + + if (node == NO_HANDLE) { + return NO_HANDLE; + } + if (_meshNodeCount(instance, index) == 1) { + return node; + } + return nodeGetChild(node, which); +} + + +// How many scene nodes carry glTF node index's mesh: none, the node itself, or one child per +// primitive when the mesh has several. +static int32_t _meshNodeCount(const InstanceT *instance, int32_t index) { + const ModelT *model = &_models[instance->model]; + const cgltf_node *node = &model->data->nodes[index]; + + if (node->mesh == NULL) { + return 0; + } + return model->meshes[node->mesh - model->data->meshes].count; +} + + // The glTF node index behind one of this instance's scene nodes, or -1. static int32_t _nodeIndex(const InstanceT *instance, int32_t node) { - const cgltf_data *data = _models[instance->model].data; - int32_t x; + int32_t x; - for (x = 0; x < (int32_t)data->nodes_count; x++) { - if (instance->nodes[x] == node) { + for (x = 0; x < instance->nodeCount; x++) { + if (_instanceNode(instance, x) == node) { return x; } } @@ -905,18 +1077,21 @@ static int32_t _nodeIndex(const InstanceT *instance, int32_t node) { } -static bool _readAttribute(const cgltf_accessor *accessor, float *out, int32_t components, int32_t count) { - int32_t x; +// A glTF node's morph weights within a pose array: morphStride floats after the nodes' poses. +static float *_poseMorph(const InstanceT *instance, const PoseT *pose, int32_t index) { + return (float *)(pose + instance->nodeCount) + (size_t)index * (size_t)instance->morphStride; +} - if ((int32_t)accessor->count < count) { + +// Reads count elements of the accessor as floats, components per element; sparse accessors (how +// Blender writes shape keys), strides and normalised integers are all applied. +static bool _readAttribute(const cgltf_accessor *accessor, float *out, int32_t components, int32_t count) { + cgltf_size floats = (cgltf_size)components * (cgltf_size)count; + + if (((int32_t)accessor->count < count) || ((int32_t)cgltf_num_components(accessor->type) != components)) { return false; } - for (x = 0; x < count; x++) { - if (!cgltf_accessor_read_float(accessor, (cgltf_size)x, out + x * components, (cgltf_size)components)) { - return false; - } - } - return true; + return cgltf_accessor_unpack_floats(accessor, out, floats) == floats; } @@ -925,34 +1100,20 @@ static bool _readAttribute(const cgltf_accessor *accessor, float *out, int32_t c static void _sampleChannel(const cgltf_animation_channel *channel, double time, float *out, int32_t components) { const cgltf_accessor *input = channel->sampler->input; const cgltf_accessor *output = channel->sampler->output; - int32_t count = (int32_t)input->count; int32_t k; int32_t next; - float t0; - float t1; float t; + float dt; float a[4]; float b[4]; int32_t c; - if (count == 0) { + if (input->count == 0) { return; } - // The keyframe pair around time; before the first or after the last just holds. - for (k = 0; k < count - 1; k++) { - cgltf_accessor_read_float(input, (cgltf_size)(k + 1), &t1, 1); - if (time < t1) { - break; - } - } - next = SDL_min(k + 1, count - 1); - cgltf_accessor_read_float(input, (cgltf_size)k, &t0, 1); - cgltf_accessor_read_float(input, (cgltf_size)next, &t1, 1); - t = (t1 > t0) ? (float)((time - t0) / (t1 - t0)) : 0.0f; - t = SDL_clamp(t, 0.0f, 1.0f); + _findKeys(input, time, &k, &next, &t, &dt); if (channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline) { // Three vectors per keyframe: in-tangent, value, out-tangent. Hermite basis. - float dt = t1 - t0; float p0[4]; float m0[4]; float p1[4]; @@ -960,10 +1121,10 @@ static void _sampleChannel(const cgltf_animation_channel *channel, double time, float t2 = t * t; float t3 = t2 * t; - cgltf_accessor_read_float(output, (cgltf_size)(k * 3 + 1), p0, (cgltf_size)components); - cgltf_accessor_read_float(output, (cgltf_size)(k * 3 + 2), m0, (cgltf_size)components); - cgltf_accessor_read_float(output, (cgltf_size)(next * 3), m1, (cgltf_size)components); - cgltf_accessor_read_float(output, (cgltf_size)(next * 3 + 1), p1, (cgltf_size)components); + cgltf_accessor_read_float(output, (cgltf_size)(k * CUBIC_SPLINE_STRIDE + 1), p0, (cgltf_size)components); + cgltf_accessor_read_float(output, (cgltf_size)(k * CUBIC_SPLINE_STRIDE + 2), m0, (cgltf_size)components); + cgltf_accessor_read_float(output, (cgltf_size)(next * CUBIC_SPLINE_STRIDE), m1, (cgltf_size)components); + cgltf_accessor_read_float(output, (cgltf_size)(next * CUBIC_SPLINE_STRIDE + 1), p1, (cgltf_size)components); for (c = 0; c < components; c++) { out[c] = (2.0f * t3 - 3.0f * t2 + 1.0f) * p0[c] + dt * (t3 - 2.0f * t2 + t) * m0[c] + (-2.0f * t3 + 3.0f * t2) * p1[c] + dt * (t3 - t2) * m1[c]; } @@ -1001,8 +1162,6 @@ static void _sampleChannel(const cgltf_animation_channel *channel, double time, } -// A weights channel at a time: count scalars per keyframe, linear or step (cubic spline treated -// as linear between its values), clamped at both ends. // The clip's pose at its time over the rest pose, marking what it drives. static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pose) { const cgltf_data *data = _models[instance->model].data; @@ -1010,7 +1169,7 @@ static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pos int32_t x; float values[4]; - memcpy(pose, instance->rest, sizeof(PoseT) * data->nodes_count); + _copyPose(instance, pose, instance->rest); if ((clip->animation < 0) || (clip->animation >= (int32_t)data->animations_count)) { return; } @@ -1024,7 +1183,7 @@ static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pos continue; } index = (int32_t)(channel->target_node - data->nodes); - if (instance->nodes[index] == NO_HANDLE) { + if (_instanceNode(instance, index) == NO_HANDLE) { continue; } p = &pose[index]; @@ -1044,13 +1203,21 @@ static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pos p->scale = vec3(values[0], values[1], values[2]); p->animated |= POSE_SCALE; } else if (channel->target_path == cgltf_animation_path_type_weights) { - // One weight per target per keyframe. - int32_t count = (int32_t)(channel->sampler->output->count / channel->sampler->input->count); + // One weight per target per keyframe (three per target for a cubic spline). + const cgltf_animation_sampler *sampler = channel->sampler; + int32_t count; - if ((count <= 0) || (count > MAX_MORPH_TARGETS)) { + if (sampler->input->count == 0) { continue; } - _sampleWeights(channel, clip->time, p->morph, count); + count = (int32_t)(sampler->output->count / sampler->input->count); + if (sampler->interpolation == cgltf_interpolation_type_cubic_spline) { + count /= CUBIC_SPLINE_STRIDE; + } + if ((count <= 0) || (count > instance->morphStride)) { + continue; + } + _sampleWeights(channel, clip->time, _poseMorph(instance, pose, index), count); p->morphCount = count; p->animated |= POSE_MORPH; } @@ -1058,59 +1225,48 @@ static void _samplePose(const InstanceT *instance, const ClipT *clip, PoseT *pos } +// A weights channel at a time: count scalars per keyframe, linear or step (cubic spline treated +// as linear between its values), clamped at both ends. static void _sampleWeights(const cgltf_animation_channel *channel, double time, float *out, int32_t count) { const cgltf_accessor *input = channel->sampler->input; const cgltf_accessor *output = channel->sampler->output; - int32_t keys = (int32_t)input->count; + bool cubic = channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline; + int32_t stride = cubic ? CUBIC_SPLINE_STRIDE : 1; + int32_t value = cubic ? CUBIC_SPLINE_VALUE : 0; int32_t k; int32_t next; - int32_t stride = (channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline) ? 3 : 1; - float t0; - float t1; float t; + float dt; float a; float b; int32_t w; - if (keys == 0) { + if (input->count == 0) { return; } - for (k = 0; k < keys - 1; k++) { - cgltf_accessor_read_float(input, (cgltf_size)(k + 1), &t1, 1); - if (time < t1) { - break; - } - } - next = SDL_min(k + 1, keys - 1); - cgltf_accessor_read_float(input, (cgltf_size)k, &t0, 1); - cgltf_accessor_read_float(input, (cgltf_size)next, &t1, 1); - t = (t1 > t0) ? (float)((time - t0) / (t1 - t0)) : 0.0f; - t = SDL_clamp(t, 0.0f, 1.0f); + _findKeys(input, time, &k, &next, &t, &dt); if (channel->sampler->interpolation == cgltf_interpolation_type_step) { t = 0.0f; } for (w = 0; w < count; w++) { - // Cubic spline keyframes hold in-tangent, value, out-tangent per weight; the value is the middle. - cgltf_accessor_read_float(output, (cgltf_size)((k * stride + (stride == 3 ? 1 : 0)) * count + w), &a, 1); - cgltf_accessor_read_float(output, (cgltf_size)((next * stride + (stride == 3 ? 1 : 0)) * count + w), &b, 1); + cgltf_accessor_read_float(output, (cgltf_size)((k * stride + value) * count + w), &a, 1); + cgltf_accessor_read_float(output, (cgltf_size)((next * stride + value) * count + w), &b, 1); out[w] = a + (b - a) * t; } } -// ===== Animation (on a model instance's root node) ===== - -// The nodes' current transforms and morph weights (a node's own, or its primitive children's), +// The nodes' current transforms and morph weights (a node's own, or its first primitive child's), // as the pose to fade out from. static void _snapshotPose(const InstanceT *instance, PoseT *pose) { - const cgltf_data *data = _models[instance->model].data; - int32_t x; - int32_t w; + int32_t x; + int32_t w; - for (x = 0; x < (int32_t)data->nodes_count; x++) { - int32_t node = instance->nodes[x]; - int32_t source = node; + for (x = 0; x < instance->nodeCount; x++) { + int32_t node = _instanceNode(instance, x); + int32_t source = (_meshNodeCount(instance, x) > 0) ? _meshNode(instance, x, 0) : NO_HANDLE; PoseT *p = &pose[x]; + float *morph = _poseMorph(instance, pose, x); memset(p, 0, sizeof(*p)); if (node == NO_HANDLE) { @@ -1120,12 +1276,12 @@ static void _snapshotPose(const InstanceT *instance, PoseT *pose) { p->rotation = nodeGetRotation(node); p->scale = nodeGetScale(node); p->animated = POSE_TRANSLATION | POSE_ROTATION | POSE_SCALE; - if ((nodeGetMorphCount(node) == 0) && (nodeGetChildCount(node) > 0)) { - source = nodeGetChild(node, 0); + if (source == NO_HANDLE) { + continue; } - p->morphCount = SDL_min(nodeGetMorphCount(source), MAX_MORPH_TARGETS); + p->morphCount = SDL_min(nodeGetMorphCount(source), instance->morphStride); for (w = 0; w < p->morphCount; w++) { - p->morph[w] = nodeGetMorphWeight(source, w); + morph[w] = nodeGetMorphWeight(source, w); } if (p->morphCount > 0) { p->animated |= POSE_MORPH; @@ -1165,17 +1321,29 @@ static bool _startClip(InstanceT *instance, LayerT *layer, int32_t index, bool l } +// Something the model does without, noted in the trace but not held against the load. +static void _warn(const char *fmt, ...) { + char message[ERROR_LENGTH]; + va_list args; + + va_start(args, fmt); + vsnprintf(message, sizeof(message), fmt, args); + va_end(args); + utilTrace("Model: %s", message); +} + + // Writes the animated parts of a pose into the nodes (morph weights to the node's own mesh or // its primitive children). static void _writePose(const InstanceT *instance, const PoseT *pose) { - const cgltf_data *data = _models[instance->model].data; - int32_t x; - int32_t w; - int32_t c; + int32_t x; + int32_t w; + int32_t c; - for (x = 0; x < (int32_t)data->nodes_count; x++) { - int32_t node = instance->nodes[x]; - const PoseT *p = &pose[x]; + for (x = 0; x < instance->nodeCount; x++) { + int32_t node = _instanceNode(instance, x); + const PoseT *p = &pose[x]; + const float *morph = _poseMorph(instance, pose, x); if ((node == NO_HANDLE) || (p->animated == 0)) { continue; @@ -1190,15 +1358,11 @@ static void _writePose(const InstanceT *instance, const PoseT *pose) { nodeSetScale(node, p->scale); } if (p->animated & POSE_MORPH) { - if (nodeGetMorphCount(node) > 0) { + for (c = 0; c < _meshNodeCount(instance, x); c++) { + int32_t target = _meshNode(instance, x, c); + for (w = 0; w < p->morphCount; w++) { - nodeSetMorphWeight(node, w, p->morph[w]); - } - } else { - for (c = 0; c < nodeGetChildCount(node); c++) { - for (w = 0; w < p->morphCount; w++) { - nodeSetMorphWeight(nodeGetChild(node, c), w, p->morph[w]); - } + nodeSetMorphWeight(target, w, morph[w]); } } } @@ -1206,6 +1370,8 @@ static void _writePose(const InstanceT *instance, const PoseT *pose) { } +// ===== Animation (on a model instance's root node) ===== + double animationGetTime(int32_t root) { InstanceT *instance = _findInstance(root); @@ -1264,15 +1430,15 @@ bool animationResume(int32_t root) { } -// Limits a layer to the subtree under one of the instance's nodes; NO_HANDLE lifts the limit. +// Limits a layer to the subtree under one of the instance's nodes; ANIMATION_NO_MASK lifts the limit. bool animationSetLayerMask(int32_t root, int32_t layer, int32_t node) { InstanceT *instance = _findInstance(root); - int32_t index = -1; + int32_t index = ANIMATION_NO_MASK; if ((instance == NULL) || (layer < 1) || (layer > ANIMATION_LAYERS)) { return false; } - if (node != NO_HANDLE) { + if (node != ANIMATION_NO_MASK) { index = _nodeIndex(instance, node); if (index < 0) { return false; @@ -1356,6 +1522,7 @@ int32_t modelAnimationIndex(int32_t model, const char *name) { return NO_HANDLE; } + // Frees the model's meshes and materials; nodes already instanced stay, without their meshes. bool modelDelete(int32_t model) { int32_t x; @@ -1409,19 +1576,23 @@ int32_t modelInstance(int32_t model, int32_t parent) { if (root == NO_HANDLE) { return NO_HANDLE; } - index = _allocInstance(); + index = _allocSlot((void **)&_instances, &_instanceCount, &_instanceCapacity, sizeof(InstanceT)); instance = &_instances[index]; instance->model = model; instance->root = root; instance->rootGeneration = nodeGetGeneration(root); + instance->nodeCount = (int32_t)data->nodes_count; + instance->morphStride = m->morphStride; + instance->poseBytes = sizeof(PoseT) * data->nodes_count + sizeof(float) * data->nodes_count * (size_t)m->morphStride; instance->nodes = SDL_malloc(sizeof(int32_t) * SDL_max(data->nodes_count, 1)); - instance->rest = SDL_calloc(SDL_max(data->nodes_count, 1), sizeof(PoseT)); - instance->work = SDL_calloc(SDL_max(data->nodes_count, 1), sizeof(PoseT)); - instance->layerPose = SDL_calloc(SDL_max(data->nodes_count, 1), sizeof(PoseT)); - instance->fromPose = SDL_calloc(SDL_max(data->nodes_count, 1), sizeof(PoseT)); - if ((instance->nodes == NULL) || (instance->rest == NULL) || (instance->work == NULL) || (instance->layerPose == NULL) || (instance->fromPose == NULL)) { + instance->generations = SDL_calloc(SDL_max(data->nodes_count, 1), sizeof(uint32_t)); + if ((instance->nodes == NULL) || (instance->generations == NULL)) { utilDie("Out of memory instancing a model."); } + instance->rest = _allocPose(instance); + instance->work = _allocPose(instance); + instance->layerPose = _allocPose(instance); + instance->fromPose = _allocPose(instance); for (x = 0; x < MAX_LAYERS; x++) { LayerT *layer = &instance->layers[x]; @@ -1429,13 +1600,10 @@ int32_t modelInstance(int32_t model, int32_t parent) { layer->clip.speed = 1.0f; layer->from.animation = NO_HANDLE; layer->weight = 1.0f; - layer->maskNode = -1; - layer->snapshot = SDL_calloc(SDL_max(data->nodes_count, 1), sizeof(PoseT)); - if (layer->snapshot == NULL) { - utilDie("Out of memory instancing a model."); - } + layer->maskNode = ANIMATION_NO_MASK; + layer->snapshot = _allocPose(instance); } - for (x = 0; x < (int32_t)data->nodes_count; x++) { + for (x = 0; x < instance->nodeCount; x++) { instance->nodes[x] = NO_HANDLE; } if (data->scene != NULL) { @@ -1443,16 +1611,16 @@ int32_t modelInstance(int32_t model, int32_t parent) { _buildNode(m, instance, data->scene->nodes[x], root); } } else { - for (x = 0; x < (int32_t)data->nodes_count; x++) { + for (x = 0; x < instance->nodeCount; x++) { if (data->nodes[x].parent == NULL) { _buildNode(m, instance, &data->nodes[x], root); } } } - _attachSkins(m, instance); + _attachSkins(instance); // The nodes stand in the file's pose now: that is what clips blend over. _snapshotPose(instance, instance->rest); - for (x = 0; x < (int32_t)data->nodes_count; x++) { + for (x = 0; x < instance->nodeCount; x++) { instance->rest[x].animated = 0; } return root; @@ -1492,37 +1660,48 @@ int32_t modelLoad(const char *name) { free(bytes); return NO_HANDLE; } + // cgltf keeps the GLB's binary chunk pointing into bytes, so the file stays with the model and + // cgltf_free releases it. + data->file_data = bytes; + handle = _allocSlot((void **)&_models, &_modelCount, &_modelCapacity, sizeof(ModelT)); + _models[handle].data = data; // Every buffer must be the GLB binary chunk or a data URI; a file beside the model is refused. for (x = 0; x < (int32_t)data->buffers_count; x++) { - if ((data->buffers[x].uri != NULL) && (strncmp(data->buffers[x].uri, "data:", 5) != 0)) { + if ((data->buffers[x].uri != NULL) && (strncmp(data->buffers[x].uri, DATA_URI_PREFIX, strlen(DATA_URI_PREFIX)) != 0)) { _fail("%s refers to the external file %s; pack everything into the .glb.", name, data->buffers[x].uri); - cgltf_free(data); - free(bytes); + _freeModel(&_models[handle]); return NO_HANDLE; } } result = cgltf_load_buffers(&options, data, NULL); if (result != cgltf_result_success) { _fail("Unable to load the buffers of %s (cgltf result %d).", name, (int32_t)result); - cgltf_free(data); - free(bytes); + _freeModel(&_models[handle]); return NO_HANDLE; } if (cgltf_validate(data) != cgltf_result_success) { _fail("%s failed validation.", name); - cgltf_free(data); - free(bytes); + _freeModel(&_models[handle]); return NO_HANDLE; } - handle = _allocModel(); - _models[handle].data = data; + for (x = 0; x < (int32_t)data->skins_count; x++) { + if (data->skins[x].joints_count > MAX_JOINTS) { + _fail("%s has a skin with %d joints; at most %d are supported.", name, (int32_t)data->skins[x].joints_count, MAX_JOINTS); + _freeModel(&_models[handle]); + return NO_HANDLE; + } + } + _models[handle].animationLengths = SDL_malloc(sizeof(double) * SDL_max(data->animations_count, 1)); + if (_models[handle].animationLengths == NULL) { + utilDie("Out of memory loading a model."); + } + for (x = 0; x < (int32_t)data->animations_count; x++) { + _models[handle].animationLengths[x] = _animationLength(&data->animations[x]); + } if (!_loadMeshes(&_models[handle])) { _freeModel(&_models[handle]); - free(bytes); return NO_HANDLE; } - // cgltf keeps the GLB's binary chunk pointing into bytes, so the file stays with the model. - _models[handle].data->file_data = bytes; utilTrace("Model %d: %s, %d meshes, %d materials, %d nodes, %d animations", handle, name, (int32_t)data->meshes_count, (int32_t)data->materials_count, (int32_t)data->nodes_count, (int32_t)data->animations_count); return handle; } @@ -1543,10 +1722,12 @@ void modelQuit(void) { } SDL_free(_models); SDL_free(_instances); - _models = NULL; - _instances = NULL; - _modelCount = 0; - _instanceCount = 0; + _models = NULL; + _instances = NULL; + _modelCount = 0; + _modelCapacity = 0; + _instanceCount = 0; + _instanceCapacity = 0; } diff --git a/src/nav.h b/src/nav.h index 927a2fa43..6bba3a0b2 100644 --- a/src/nav.h +++ b/src/nav.h @@ -32,7 +32,14 @@ #include #include "math3d.h" -#define NAV_NO_HANDLE -1 +#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 diff --git a/src/navRecast.cpp b/src/navRecast.cpp index e284fa312..08f807ae6 100644 --- a/src/navRecast.cpp +++ b/src/navRecast.cpp @@ -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(¶ms, 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, ¶ms); } 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,13 +406,8 @@ 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 = 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); + 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,27 +620,29 @@ 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; } // 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); - int32_t count = 0; - int32_t t; - int32_t p; - int32_t v; + NavRecordT *record = _navOf(nav); + const dtNavMesh *mesh; + int32_t count = 0; + int32_t t; + int32_t p; + int32_t v; 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]; - int32_t polyCount = 0; - float *straight; + dtPolyRef polys[NAV_MAX_PATH]; + int32_t polyCount = 0; 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(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; } diff --git a/src/pack.c b/src/pack.c index 040e46125..ff512912f 100644 --- a/src/pack.c +++ b/src/pack.c @@ -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 #include @@ -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,12 +147,13 @@ 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); - ok = false; + 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; @@ -212,7 +221,10 @@ static bool _collect(const char *root, const char *relative, EntryT **list) { if (utilStricmp(utilGetFileExtension(de->d_name), "index") == 0) { utilSay(">>> Skipping stale index %s", child); } else { - entry = (EntryT *)calloc(1, sizeof(EntryT)); + 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); @@ -290,7 +307,8 @@ static bool _insertFile(WriterT *writer, const EntryT *entry) { sqlite3_bind_text(writer->assetStmt, 2, entry->relative, -1, SQLITE_STATIC); sqlite3_bind_int64(writer->assetStmt, 3, entry->size); if (entry->size <= writer->chunkBytes) { - got = fread(buffer, 1, (size_t)entry->size, file); + 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) { - number++; - if ((strstr(line, "../") != NULL) || (strstr(line, "..\\") != 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++; + } + } 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,9 +600,8 @@ 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; + char *text = NULL; + bool ok = true; memset(writer, 0, sizeof(*writer)); writer->chunkBytes = VFS_CHUNK_BYTES; @@ -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; @@ -655,18 +739,17 @@ 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; - bool found = false; + sqlite3 *db = 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,17 +831,22 @@ 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); - files++; + if ((fclose(file) != 0) || !written) { + utilSay("!!! Unable to write %s", path); + ok = false; + } else { + files++; + } } } free(parent); @@ -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; diff --git a/src/pack.h b/src/pack.h index 57966c956..5337bf2b6 100644 --- a/src/pack.h +++ b/src/pack.h @@ -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 diff --git a/src/particles.c b/src/particles.c index a3b3e553c..e4c6cec78 100644 --- a/src/particles.c +++ b/src/particles.c @@ -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++; } - n++; } - return false; + return n; } diff --git a/src/particles.h b/src/particles.h index 3902777e2..a4054e2e9 100644 --- a/src/particles.h +++ b/src/particles.h @@ -30,6 +30,8 @@ #include #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); diff --git a/src/physics.h b/src/physics.h index 5a9b05b0f..7690537d8 100644 --- a/src/physics.h +++ b/src/physics.h @@ -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,29 +102,36 @@ typedef enum VehicleKindE { } VehicleKindE; -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); -bool bodyExists(int32_t node); -Vec3T bodyGetAngularVelocity(int32_t node); -Vec3T bodyGetVelocity(int32_t node); -bool bodyIsResting(int32_t node); -bool bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c); -bool bodySetAngularVelocity(int32_t node, Vec3T velocity); -bool bodySetBounce(int32_t node, float bounce); -bool bodySetBuoyancy(int32_t node, float factor); -bool bodySetCurrent(int32_t node, Vec3T flow); -bool bodySetEnabled(int32_t node, bool enabled); -bool bodySetFriction(int32_t node, float friction); -bool bodySetMass(int32_t node, float kilograms); -bool bodySetTrigger(int32_t node, bool trigger); -bool bodySetVelocity(int32_t node, Vec3T velocity); -bool bodySetWater(int32_t node, float density, float linearDrag, float angularDrag); -bool jointDelete(int32_t joint); +// 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); +bool bodyExists(int32_t node); +Vec3T bodyGetAngularVelocity(int32_t node); +Vec3T bodyGetVelocity(int32_t node); +bool bodyIsResting(int32_t node); +bool bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c); +bool bodySetAngularVelocity(int32_t node, Vec3T velocity); +bool bodySetBounce(int32_t node, float bounce); +bool bodySetBuoyancy(int32_t node, float factor); +bool bodySetCurrent(int32_t node, Vec3T flow); +bool bodySetEnabled(int32_t node, bool enabled); +bool bodySetFriction(int32_t node, float friction); +bool bodySetMass(int32_t node, float kilograms); +bool bodySetTrigger(int32_t node, bool trigger); +bool bodySetVelocity(int32_t node, Vec3T velocity); +bool bodySetWater(int32_t node, float density, float linearDrag, float angularDrag); +bool jointDelete(int32_t joint); int32_t jointNew(JointTypeE type, int32_t nodeA, int32_t nodeB, Vec3T anchor, Vec3T axis); -bool jointSetLimits(int32_t joint, float low, float high); -bool jointValid(int32_t joint); -bool physicsAvailable(void); +bool jointSetLimits(int32_t joint, float low, float high); +bool jointValid(int32_t joint); +bool physicsAvailable(void); bool playerDelete(int32_t node); bool playerExists(int32_t node); int32_t playerGetGround(int32_t node, Vec3T *normal); @@ -183,14 +190,14 @@ bool vehicleSetSuspension(int32_t node, float frequency, float damping); bool vehicleSetThrust(int32_t node, float maxForce, Vec3T point); bool vehicleSetWheel(int32_t node, int32_t index, bool steered, bool driven); int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum); -bool physicsInit(void); -void physicsQuit(void); -bool physicsRaycast(Vec3T origin, Vec3T direction, float maxDistance, int32_t *node, Vec3T *point, Vec3T *normal); -void physicsSet2D(bool planar); -void physicsSetDebug(uint32_t mask); -void physicsSetEnabled(bool enabled); -void physicsSetGravity(Vec3T gravity); -void physicsUpdate(bool advance); +bool physicsInit(void); +void physicsQuit(void); +bool physicsRaycast(Vec3T origin, Vec3T direction, float maxDistance, int32_t *node, Vec3T *point, Vec3T *normal); +void physicsSet2D(bool planar); +void physicsSetDebug(uint32_t mask); +void physicsSetEnabled(bool enabled); +void physicsSetGravity(Vec3T gravity); +void physicsUpdate(bool advance); #ifdef __cplusplus diff --git a/src/physicsJolt.cpp b/src/physicsJolt.cpp index 175910f32..8f58f89a9 100644 --- a/src/physicsJolt.cpp +++ b/src/physicsJolt.cpp @@ -68,6 +68,7 @@ #include #endif #include +#include #include extern "C" { #include "util.h" @@ -86,7 +87,9 @@ extern "C" { #define MAX_SOFT 16 #define MAX_SOFT_PINS 32 #define ROPE_SIDES 6 -#define SOFT_WELD 1.0e-4f +#define ROPE_INDICES_PER_QUAD 6 // Two triangles between neighbouring rings +#define SOFT_WELD 1.0e-4f // Mesh vertices this close are one particle +#define SOFT_VERTEX_RADIUS 0.03f // Cloth and pressure bodies; a rope uses its own radius #define STRETCH_COMPLIANCE 1.0e-3f // At stiffness 0; stiffness 1 is rigid #define BEND_COMPLIANCE 1.0e-2f #define DEFAULT_SOFT_STRETCH 0.9f @@ -101,21 +104,42 @@ extern "C" { #define RAGDOLL_TWIST_DEGREES 30.0f #define RAGDOLL_MOTOR_HZ 4.0f #define RAGDOLL_MOTOR_DAMPING 1.0f +#define RAGDOLL_LINEAR_DAMPING 0.2f +#define RAGDOLL_ANGULAR_DAMPING 0.5f +#define LEAF_BONE_RADII 4.0f // A bone with no child joint is this many minimum radii long ... +#define MIN_BONE_RADII 2.0f // ... and no bone is shorter than this many +#define MAX_CONE_DEGREES 179.0f // Widest swing or twist a ragdoll joint allows #define DEFAULT_BUOYANCY 1.2f #define DEFAULT_SINK_SPEED 0.3f #define DEFAULT_SWIM_DRAG 2.0f #define MAX_WHEELS 16 -#define MAX_GEARS 8 #define DEFAULT_ENGINE_TORQUE 500.0f #define DEFAULT_ENGINE_MAX_RPM 6000.0f -#define DEFAULT_ENGINE_MIN_RPM 1000.0f +#define MIN_ENGINE_TORQUE 1.0f +#define MIN_ENGINE_RPM 1.0f // The lowest idle a script may ask for ... +#define MIN_ENGINE_MAX_RPM 100.0f // ... and the lowest redline #define DEFAULT_STEER_DEGREES 35.0f +#define MAX_STEER_DEGREES 89.0f #define DEFAULT_BRAKE_TORQUE 1500.0f #define DEBUG_CONTACT_SIZE 0.1f // Half the cross drawn at a contact #define DEFAULT_HANDBRAKE_TORQUE 4000.0f #define DEFAULT_SUSPENSION_HZ 1.5f #define DEFAULT_SUSPENSION_DAMPING 0.5f +#define MIN_SUSPENSION_HZ 0.1f #define VEHICLE_MAX_TILT_DEGREES 60.0f +#define FRONT_EPSILON 1.0e-4f // A wheel this far ahead of the mean is a front wheel +#define WHEEL_CAST_RADIUS 0.05f // Convex radius of the cylinder a wheel feels the ground with +#define MIN_INVERSE_INERTIA 1.0e-9f // Below this the chassis' roll inertia is taken as LEAN_INERTIA +#define TANK_PIVOT_THROTTLE 0.35f // Track drive when a tank turns on the spot +#define TANK_TURN_RATIO 0.9f // How much the inner track slows at full steer ... +#define TANK_TURN_MIN 0.1f // ... but never below this +#define DEFAULT_THRUST 2000.0f // Boats: propeller force ... +#define DEFAULT_THRUST_Y -0.2f // ... applied here in the hull's frame ... +#define DEFAULT_THRUST_Z 1.0f +#define DEFAULT_RUDDER 800.0f // ... and turning torque ... +#define RUDDER_FULL_SPEED 3.0f // ... which bites fully from this speed ... +#define RUDDER_MIN_BITE 0.2f // ... and this much when still +#define BOAT_BRAKE_RATIO 0.5f // A boat's braking force as a share of its thrust #define LEAN_SPRING 5000.0f // Jolt's motorcycle lean spring and damping ... #define LEAN_DAMPING 1000.0f #define LEAN_INERTIA 40.0f // ... for a chassis of this roll inertia (kg m^2); scaled from there @@ -127,18 +151,23 @@ extern "C" { #define STICK_PER_EXTENT (0.5f / 0.3f) #define STEP_TEST_PER_EXTENT (0.15f / 0.3f) #define DEFAULT_SLOPE_DEGREES 45.0f +#define MAX_SLOPE_DEGREES 89.0f #define DEFAULT_PUSH_STRENGTH 300.0f -#define MAX_BODY_PAIRS 4096 +#define STANDING_VERTICAL_SPEED 0.1f // Rising faster than this off the ground counts as airborne +#define ZERO_GRAVITY_SQ 1.0e-8f // Gravity shorter than this leaves up as +Y +#define MAX_BODY_PAIRS 65536 // Broad phase pairs a step may find: many more than touch #define MAX_CONTACTS 8192 #define TEMP_ALLOCATOR_BYTES (16 * 1024 * 1024) #define MIN_JOB_THREADS 1 #define STEP_SECONDS (1.0 / 60.0) #define MAX_STEPS_PER_FRAME 4 #define MIN_DIMENSION 0.001f +#define MIN_MASS 0.001f +#define MIN_SCALE 1.0e-6f // A node axis scaled below this is not divided by +#define ZERO_LENGTH_SQ 1.0e-10f #define DEFAULT_FRICTION 0.5f #define DEFAULT_BOUNCE 0.1f #define NO_HANDLE -1 -#define MAX_EVENTS 512 // Per frame; the rest of a busy step is dropped #define DEFAULT_RAY_DISTANCE 1000.0f #define WORLD_NODE -1 // A joint's other side fixed to the world @@ -267,6 +296,25 @@ namespace { }; + // A mesh vertex's position quantised to SOFT_WELD cells, for welding vertices into particles. + struct WeldKeyT { + int32_t x; + int32_t y; + int32_t z; + + bool operator==(const WeldKeyT &other) const { + return (x == other.x) && (y == other.y) && (z == other.z); + } + }; + + + struct WeldHashT { + size_t operator()(const WeldKeyT &key) const { + return ((size_t)(uint32_t)key.x * 73856093u) ^ ((size_t)(uint32_t)key.y * 19349663u) ^ ((size_t)(uint32_t)key.z * 83492791u); + } + }; + + // A vertex of a soft body held in place, or held to a node. struct SoftPinT { int32_t vertex; @@ -302,14 +350,15 @@ namespace { // A wheel of a vehicle: the node the engine poses, and its geometry relative to the chassis. struct WheelRecordT { - int32_t node; - Vec3T rest; // Where the wheel sits in the chassis' frame, taken when it was added - float radius; - float width; - float suspension; - bool steered; - bool driven; - bool steeredSet; // vehicleSetWheel called; otherwise front wheels steer and all drive + int32_t node; + uint32_t generation; + Vec3T rest; // Where the wheel sits in the chassis' frame, taken when it was added + float radius; + float width; + float suspension; + bool steered; + bool driven; + bool steeredSet; // vehicleSetWheel called; otherwise front wheels steer and all drive }; @@ -325,7 +374,7 @@ namespace { float maxTorque; float maxRpm; float minRpm; - float gears[MAX_GEARS]; + float gears[VEHICLE_MAX_GEARS]; int32_t gearCount; float reverseGear; bool automatic; @@ -347,56 +396,21 @@ namespace { }; - struct WorldT; - extern WorldT *_world; - bool _isTrigger(JPH::BodyID id); - PlayerRecordT *_findPlayer(int32_t node); - - - // Collects contacts from Jolt's job threads; the engine drains it after the step. + // Collects contacts from Jolt's job threads; the engine drains it after the step. Trigger + // overlaps are counted per (trigger, body) pair, since Jolt reports every sub-shape pair on its + // own (a mesh trigger: one per triangle) and the engine wants one enter and one leave. class ContactListenerT final : public JPH::ContactListener { public: - std::mutex lock; - std::vector events; - - void OnContactAdded(const JPH::Body &a, const JPH::Body &b, const JPH::ContactManifold &manifold, JPH::ContactSettings &settings) override { - PhysicsEventT event; - JPH::Vec3 relative; - - (void)settings; - event.nodeA = (int32_t)(uint32_t)a.GetUserData(); - event.nodeB = (int32_t)(uint32_t)b.GetUserData(); - event.point = vec3((float)manifold.GetWorldSpaceContactPointOn1(0).GetX(), (float)manifold.GetWorldSpaceContactPointOn1(0).GetY(), (float)manifold.GetWorldSpaceContactPointOn1(0).GetZ()); - relative = a.GetLinearVelocity() - b.GetLinearVelocity(); - event.speed = fabsf(relative.Dot(manifold.mWorldSpaceNormal)); - if (a.IsSensor() || b.IsSensor()) { - event.type = PHYSICS_EVENT_ENTER; - if (b.IsSensor()) { - // The trigger comes first. - int32_t swap = event.nodeA; - - event.nodeA = event.nodeB; - event.nodeB = swap; - } - if (_findPlayer(event.nodeB) != nullptr) { - // A player's inner body: the engine reports players entering triggers itself. - return; - } - } else { - event.type = PHYSICS_EVENT_COLLISION; - } - push(event); - } + std::mutex lock; + std::vector events; + std::unordered_map overlaps; // (trigger, body) to sub-shape pairs touching + void OnContactAdded(const JPH::Body &a, const JPH::Body &b, const JPH::ContactManifold &manifold, JPH::ContactSettings &settings) override; void OnContactRemoved(const JPH::SubShapeIDPair &pair) override; - - void push(const PhysicsEventT &event) { - std::lock_guard guard(lock); - - if (events.size() < MAX_EVENTS) { - events.push_back(event); - } - } + void forget(int32_t node); // Drops the overlap counts a node is party to + void overlap(int32_t trigger, int32_t node, bool entered); // One sub-shape pair started or stopped touching + void push(const PhysicsEventT &event); // Queues an event, taking the lock + void pushLocked(const PhysicsEventT &event); // Queues an event under a lock already held }; @@ -420,6 +434,9 @@ namespace { }; + // The tables are fixed-size (bodyCount is the highest slot ever used, so loops stay short); + // each is indexed by node handle through a vector grown on demand, since handles are small + // dense integers. struct WorldT { JPH::TempAllocatorImpl *tempAllocator; JPH::JobSystemThreadPool *jobs; @@ -430,8 +447,7 @@ namespace { ContactListenerT *contacts; BodyRecordT *bodies; int32_t bodyCount; - JointRecordT *joints; - int32_t jointCount; + std::vector joints; PlayerRecordT *players; int32_t playerCount; PlayerListenerT *playerListener; @@ -441,6 +457,11 @@ namespace { int32_t ragdollCount; SoftRecordT *softs; int32_t softCount; + std::vector bodyOfNode; // Node handle to slot in each table, or NO_HANDLE + std::vector playerOfNode; + std::vector vehicleOfNode; + std::vector ragdollOfNode; + std::vector softOfNode; JPH::uint32 ragdollGroups; // Next collision group id WaterSurfaceT waters[MAX_WATERS]; int32_t waterCount; @@ -454,79 +475,165 @@ namespace { WorldT *_world = nullptr; - // A contact ending only matters for triggers: the pair is reported as left. This runs inside - // Jolt's step on a job thread, where taking a body lock deadlocks, so the lock-free interface - // reads the user data; the trigger flag comes from our records. - void ContactListenerT::OnContactRemoved(const JPH::SubShapeIDPair &pair) { - PhysicsEventT event; - JPH::uint64 userA = _world->system->GetBodyInterfaceNoLock().GetUserData(pair.GetBody1ID()); - JPH::uint64 userB = _world->system->GetBodyInterfaceNoLock().GetUserData(pair.GetBody2ID()); - bool sensorA = _isTrigger(pair.GetBody1ID()); - bool sensorB = _isTrigger(pair.GetBody2ID()); + void _applyWater(float dt); + JPH::RefConst _buildHeightField(int32_t mesh, Vec3T scale); + JPH::RefConst _buildMeshShape(int32_t node, ShapeTypeE shape, Vec3T position, QuatT rotation); + JPH::RefConst _buildShape(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c, Vec3T position, QuatT rotation, Vec3T scale); + bool _buildSoft(SoftRecordT *record); + bool _buildVehicle(VehicleRecordT *record); + void _collectGeometry(int32_t node, const Mat4T *toBody, ShapeTypeE shape, JPH::Array &points, JPH::IndexedTriangleList &triangles, JPH::VertexList &vertices); + void _destroyBody(JPH::BodyID &id); + void _drawDebug(void); + void _driveVehicles(void); + BodyRecordT *_find(int32_t node); + PlayerRecordT *_findPlayer(int32_t node); + RagdollRecordT *_findRagdoll(int32_t node); + int32_t _findSkinned(int32_t node); + SoftRecordT *_findSoft(int32_t node); + VehicleRecordT *_findVehicle(int32_t node); + JPH::Quat _fromQuat(QuatT q); + JPH::Vec3 _fromVec3(Vec3T v); + int32_t _indexGet(const std::vector &index, int32_t node); + void _indexSet(std::vector &index, int32_t node, int32_t slot); + int32_t _nearestSoftVertex(const SoftRecordT *record, Vec3T point); + void _pinSoft(void); + void _playerInside(PlayerRecordT *record, const int32_t *now, int32_t count); + void _playerTriggers(PlayerRecordT *record); + JPH::Vec3 _playerUp(void); + void _poseWheels(void); + int32_t _ragdollPartOf(RagdollRecordT *record, int32_t joint); + void _release(BodyRecordT *record); + void _releasePlayer(PlayerRecordT *record); + void _releaseRagdoll(RagdollRecordT *record); + void _releaseRagdollBodies(RagdollRecordT *record); + void _releaseSoft(SoftRecordT *record); + void _releaseSoftBody(SoftRecordT *record); + void _releaseVehicle(VehicleRecordT *record); + void _resetRagdoll(RagdollRecordT *record); + void _resetSoft(SoftRecordT *record); + void _resetVehicle(VehicleRecordT *record); + void _ropeMesh(const SoftRecordT *record, Vec3T *out); + void _setDrivetrain(const VehicleRecordT *record, JPH::VehicleEngineSettings &engine, JPH::VehicleTransmissionSettings &transmission); + float _softInvMass(const SoftRecordT *record); + void _softSetMasses(SoftRecordT *record); + void _steerRagdolls(void); + void _step(void); + void _stepPlayers(float dt); + QuatT _toQuat(JPH::Quat q); + Vec3T _toVec3(JPH::Vec3 v); + void _trace(const char *fmt, ...); + PhysicsEventT _triggerEvent(PhysicsEventTypeE type, int32_t trigger, int32_t node); + int32_t _triggerNode(JPH::BodyID id); + bool _underWater(JPH::RVec3Arg point, JPH::Vec3 *current); + void _writePlayers(void); + void _writeRagdolls(void); + void _writeSoft(void); - if (!sensorA && !sensorB) { + + uint32_t _debugMask = DEBUG_NONE; + + + void ContactListenerT::OnContactAdded(const JPH::Body &a, const JPH::Body &b, const JPH::ContactManifold &manifold, JPH::ContactSettings &settings) { + PhysicsEventT event; + JPH::Vec3 relative; + + (void)settings; + if (a.IsSensor() || b.IsSensor()) { + const JPH::Body &trigger = a.IsSensor() ? a : b; + const JPH::Body &other = a.IsSensor() ? b : a; + int32_t node = (int32_t)(uint32_t)other.GetUserData(); + + // A player's inner body: the engine reports players entering triggers itself. + if (_findPlayer(node) == nullptr) { + overlap((int32_t)(uint32_t)trigger.GetUserData(), node, true); + } return; } - event.type = PHYSICS_EVENT_LEAVE; - event.nodeA = (int32_t)(uint32_t)(sensorA ? userA : userB); - event.nodeB = (int32_t)(uint32_t)(sensorA ? userB : userA); - if (_findPlayer(event.nodeB) != nullptr) { - return; - } - event.point = vec3(0.0f, 0.0f, 0.0f); - event.speed = 0.0f; + event.type = PHYSICS_EVENT_COLLISION; + event.nodeA = (int32_t)(uint32_t)a.GetUserData(); + event.nodeB = (int32_t)(uint32_t)b.GetUserData(); + event.point = _toVec3(JPH::Vec3(manifold.GetWorldSpaceContactPointOn1(0))); + relative = a.GetLinearVelocity() - b.GetLinearVelocity(); + event.speed = fabsf(relative.Dot(manifold.mWorldSpaceNormal)); push(event); } - JPH::RefConst _buildHeightField(int32_t mesh, Vec3T scale); - JPH::RefConst _buildMeshShape(int32_t node, ShapeTypeE shape, Vec3T position, QuatT rotation); - JPH::RefConst _buildShape(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c, Vec3T position, QuatT rotation, Vec3T scale); - PlayerRecordT *_findPlayer(int32_t node); - void _playerTriggers(PlayerRecordT *record); - VehicleRecordT *_findVehicle(int32_t node); - RagdollRecordT *_findRagdoll(int32_t node); - SoftRecordT *_findSoft(int32_t node); - bool _buildSoft(SoftRecordT *record); - void _pinSoft(void); - void _releaseSoft(SoftRecordT *record); - void _releaseSoftBody(SoftRecordT *record); - void _resetSoft(SoftRecordT *record); - void _ropeMesh(const SoftRecordT *record, Vec3T *out); - void _writeSoft(void); - int32_t _nearestSoftVertex(const SoftRecordT *record, Vec3T point); - int32_t _findSkinned(int32_t node); - int32_t _ragdollPartOf(RagdollRecordT *record, int32_t joint); - void _releaseRagdoll(RagdollRecordT *record); - void _releaseRagdollBodies(RagdollRecordT *record); - void _resetRagdoll(RagdollRecordT *record); - void _steerRagdolls(void); - void _writeRagdolls(void); - void _applyWater(float dt); - bool _underWater(JPH::RVec3Arg point, JPH::Vec3 *current); - bool _buildVehicle(VehicleRecordT *record); - void _driveVehicles(void); - void _poseWheels(void); - void _releaseVehicle(VehicleRecordT *record); - void _resetVehicle(VehicleRecordT *record); - JPH::Vec3 _playerUp(void); - void _releasePlayer(PlayerRecordT *record); - void _stepPlayers(float dt); - void _writePlayers(void); - void _collectGeometry(int32_t node, const Mat4T *toBody, JPH::Array &points, JPH::IndexedTriangleList &triangles, JPH::VertexList &vertices); - BodyRecordT *_find(int32_t node); - JPH::Quat _fromQuat(QuatT q); - JPH::Vec3 _fromVec3(Vec3T v); - bool _isTrigger(JPH::BodyID id); - void _release(BodyRecordT *record); - QuatT _toQuat(JPH::Quat q); - Vec3T _toVec3(JPH::Vec3 v); - void _trace(const char *fmt, ...); - void _drawDebug(void); - void _step(void); + // A contact ending only matters for triggers. This runs inside Jolt's step on a job thread, + // where the locking interface deadlocks, so the bodies are read through the lock-free one (a + // body destroyed since the contact was made simply fails to lock). + void ContactListenerT::OnContactRemoved(const JPH::SubShapeIDPair &pair) { + JPH::BodyLockRead lockA(_world->system->GetBodyLockInterfaceNoLock(), pair.GetBody1ID()); + JPH::BodyLockRead lockB(_world->system->GetBodyLockInterfaceNoLock(), pair.GetBody2ID()); + const JPH::Body *trigger; + const JPH::Body *other; + int32_t node; + + if (!lockA.Succeeded() || !lockB.Succeeded()) { + return; + } + if (lockA.GetBody().IsSensor()) { + trigger = &lockA.GetBody(); + other = &lockB.GetBody(); + } else if (lockB.GetBody().IsSensor()) { + trigger = &lockB.GetBody(); + other = &lockA.GetBody(); + } else { + return; + } + node = (int32_t)(uint32_t)other->GetUserData(); + if (_findPlayer(node) == nullptr) { + overlap((int32_t)(uint32_t)trigger->GetUserData(), node, false); + } + } - uint32_t _debugMask = DEBUG_NONE; + void ContactListenerT::forget(int32_t node) { + std::lock_guard guard(lock); + std::unordered_map::iterator it = overlaps.begin(); + + while (it != overlaps.end()) { + if (((int32_t)(uint32_t)(it->first >> 32) == node) || ((int32_t)(uint32_t)it->first == node)) { + it = overlaps.erase(it); + } else { + ++it; + } + } + } + + + void ContactListenerT::overlap(int32_t trigger, int32_t node, bool entered) { + uint64_t key = ((uint64_t)(uint32_t)trigger << 32) | (uint32_t)node; + std::lock_guard guard(lock); + + if (entered) { + if (++overlaps[key] != 1) { + return; + } + } else { + std::unordered_map::iterator it = overlaps.find(key); + + if ((it == overlaps.end()) || (--it->second > 0)) { + return; + } + overlaps.erase(it); + } + pushLocked(_triggerEvent(entered ? PHYSICS_EVENT_ENTER : PHYSICS_EVENT_LEAVE, trigger, node)); + } + + + void ContactListenerT::push(const PhysicsEventT &event) { + std::lock_guard guard(lock); + + pushLocked(event); + } + + + void ContactListenerT::pushLocked(const PhysicsEventT &event) { + if (events.size() < PHYSICS_MAX_EVENTS) { + events.push_back(event); + } + } #ifdef JPH_DEBUG_RENDERER @@ -741,7 +848,7 @@ namespace { if (!mat4Invert(bodyWorld, &toBody)) { return nullptr; } - _collectGeometry(node, &toBody, points, triangles, vertices); + _collectGeometry(node, &toBody, shape, points, triangles, vertices); if (shape == SHAPE_HULL) { JPH::ConvexHullShapeSettings settings(points); JPH::Shape::ShapeResult result; @@ -775,8 +882,9 @@ namespace { } - // Gathers the node's mesh and its descendants' into the body's frame. - void _collectGeometry(int32_t node, const Mat4T *toBody, JPH::Array &points, JPH::IndexedTriangleList &triangles, JPH::VertexList &vertices) { + // Gathers the node's mesh and its descendants' into the body's frame: points for a hull, + // vertices and triangles for a mesh. + void _collectGeometry(int32_t node, const Mat4T *toBody, ShapeTypeE shape, JPH::Array &points, JPH::IndexedTriangleList &triangles, JPH::VertexList &vertices) { const float *positions; const uint32_t *indices; int32_t vertexCount; @@ -796,40 +904,53 @@ namespace { for (x = 0; x < vertexCount; x++) { Vec3T v = mat4TransformPoint(local, vec3(positions[x * 3], positions[x * 3 + 1], positions[x * 3 + 2])); - points.push_back(JPH::Vec3(v.x, v.y, v.z)); - vertices.push_back(JPH::Float3(v.x, v.y, v.z)); + if (shape == SHAPE_HULL) { + points.push_back(JPH::Vec3(v.x, v.y, v.z)); + } else { + vertices.push_back(JPH::Float3(v.x, v.y, v.z)); + } } - for (x = 0; x + 2 < indexCount; x += 3) { - triangles.push_back(JPH::IndexedTriangle(base + indices[x], base + indices[x + 1], base + indices[x + 2])); + if (shape != SHAPE_HULL) { + for (x = 0; x + 2 < indexCount; x += 3) { + triangles.push_back(JPH::IndexedTriangle(base + indices[x], base + indices[x + 1], base + indices[x + 2])); + } } } for (x = 0; x < nodeGetChildCount(node); x++) { - _collectGeometry(nodeGetChild(node, x), toBody, points, triangles, vertices); + _collectGeometry(nodeGetChild(node, x), toBody, shape, points, triangles, vertices); } } + // Takes a body out of the world (a no-op when it is not in it) and destroys it. + void _destroyBody(JPH::BodyID &id) { + JPH::BodyInterface &bodies = _world->system->GetBodyInterface(); + + bodies.RemoveBody(id); + bodies.DestroyBody(id); + id = JPH::BodyID(); + } + + // The record for a node's body, or NULL. A record whose node was deleted (or reused) is // released on the way. BodyRecordT *_find(int32_t node) { - int32_t x; + BodyRecordT *record; + int32_t slot; if (_world == nullptr) { return nullptr; } - for (x = 0; x < _world->bodyCount; x++) { - BodyRecordT *record = &_world->bodies[x]; - - if (!record->used || (record->node != node)) { - continue; - } - if (!nodeValid(node) || (nodeGetGeneration(node) != record->generation)) { - _release(record); - return nullptr; - } - return record; + slot = _indexGet(_world->bodyOfNode, node); + if (slot == NO_HANDLE) { + return nullptr; } - return nullptr; + record = &_world->bodies[slot]; + if (!nodeValid(node) || (nodeGetGeneration(node) != record->generation)) { + _release(record); + return nullptr; + } + return record; } @@ -843,38 +964,46 @@ namespace { } - // Whether a Jolt body is one of our triggers (by record, so a body being destroyed is safe). - bool _isTrigger(JPH::BodyID id) { - int32_t x; - - for (x = 0; x < _world->bodyCount; x++) { - if (_world->bodies[x].used && (_world->bodies[x].id == id)) { - return _world->bodies[x].trigger; - } + // The slot a node maps to in one of the tables, or NO_HANDLE. + int32_t _indexGet(const std::vector &index, int32_t node) { + if ((node < 0) || ((size_t)node >= index.size())) { + return NO_HANDLE; } - return false; + return index[(size_t)node]; } - // Takes the body out of the world (its joints first) and frees its slot. - void _release(BodyRecordT *record) { - JPH::BodyInterface &bodies = _world->system->GetBodyInterface(); - int32_t x; - - for (x = 0; x < _world->vehicleCount; x++) { - if (_world->vehicles[x].used && (_world->vehicles[x].node == record->node)) { - _releaseVehicle(&_world->vehicles[x]); - } + // Maps a node to a slot (NO_HANDLE unmaps it), growing the index to reach it. + void _indexSet(std::vector &index, int32_t node, int32_t slot) { + if (node < 0) { + return; } - for (x = 0; x < _world->jointCount; x++) { - if (_world->joints[x].used && ((_world->joints[x].nodeA == record->node) || (_world->joints[x].nodeB == record->node))) { + if ((size_t)node >= index.size()) { + if (slot == NO_HANDLE) { + return; + } + index.resize((size_t)node + 1, NO_HANDLE); + } + index[(size_t)node] = slot; + } + + + // Takes the body out of the world (its vehicle and joints first) and frees its slot. + void _release(BodyRecordT *record) { + VehicleRecordT *vehicle = _findVehicle(record->node); + int32_t x; + + if (vehicle != nullptr) { + _releaseVehicle(vehicle); + } + for (x = 0; x < (int32_t)_world->joints.size(); x++) { + if (_world->joints[(size_t)x].used && ((_world->joints[(size_t)x].nodeA == record->node) || (_world->joints[(size_t)x].nodeB == record->node))) { jointDelete(x); } } - if (record->enabled) { - bodies.RemoveBody(record->id); - } - bodies.DestroyBody(record->id); + _world->contacts->forget(record->node); + _indexSet(_world->bodyOfNode, record->node, NO_HANDLE); + _destroyBody(record->id); memset(record, 0, sizeof(*record)); } @@ -893,10 +1022,9 @@ namespace { event.type = PHYSICS_EVENT_COLLISION; event.nodeA = (int32_t)(uint32_t)character->GetUserData(); event.nodeB = (int32_t)(uint32_t)contact.mUserData; - event.point = vec3((float)contact.mPosition.GetX(), (float)contact.mPosition.GetY(), (float)contact.mPosition.GetZ()); + event.point = _toVec3(JPH::Vec3(contact.mPosition)); event.speed = fabsf((character->GetLinearVelocity() - contact.mLinearVelocity).Dot(contact.mContactNormal)); - std::lock_guard guard(_world->contacts->lock); - _world->contacts->events.push_back(event); + _world->contacts->push(event); } @@ -924,7 +1052,7 @@ namespace { utilTrace("Physics: a mesh shape can only be static or kinematic; use a hull for node %d.", node); return nullptr; } - if ((shape == SHAPE_MESH) && (nodeGetMesh(node) >= 0)) { + if ((shape == SHAPE_MESH) && (nodeGetMesh(node) != NO_HANDLE)) { // A heightmap mesh gets Jolt's height field, far cheaper than its triangles. JPH::RefConst field = _buildHeightField(nodeGetMesh(node), scale); @@ -941,17 +1069,9 @@ namespace { PlayerRecordT *_findPlayer(int32_t node) { - int32_t x; + int32_t slot = (_world != nullptr) ? _indexGet(_world->playerOfNode, node) : NO_HANDLE; - if (_world == nullptr) { - return nullptr; - } - for (x = 0; x < _world->playerCount; x++) { - if (_world->players[x].used && (_world->players[x].node == node)) { - return &_world->players[x]; - } - } - return nullptr; + return (slot == NO_HANDLE) ? nullptr : &_world->players[slot]; } @@ -959,7 +1079,7 @@ namespace { JPH::Vec3 _playerUp(void) { JPH::Vec3 gravity = _world->system->GetGravity(); - if (gravity.LengthSq() < 1e-8f) { + if (gravity.LengthSq() < ZERO_GRAVITY_SQ) { return JPH::Vec3::sAxisY(); } return -gravity.Normalized(); @@ -967,6 +1087,9 @@ namespace { void _releasePlayer(PlayerRecordT *record) { + if (record->used) { + _indexSet(_world->playerOfNode, record->node, NO_HANDLE); + } record->character = nullptr; record->node = 0; record->generation = 0; @@ -997,6 +1120,7 @@ namespace { JPH::Vec3 velocity; JPH::Vec3 scaledGravity; float vertical; + bool onGround; JPH::CharacterVirtual::ExtendedUpdateSettings settings; if (!record->used || !record->enabled) { @@ -1010,6 +1134,7 @@ namespace { record->character->SetUp(up); velocity = record->character->GetLinearVelocity(); vertical = (velocity - record->character->GetGroundVelocity()).Dot(up); + onGround = record->character->GetGroundState() == JPH::CharacterBase::EGroundState::OnGround; record->swimming = _underWater(record->character->GetPosition() + up * (record->height * 0.5f), &record->swimCurrent); if (record->swimming) { // Afloat: the script steers in three axes and the water drags it toward that; with no @@ -1018,15 +1143,18 @@ namespace { JPH::Vec3 target = intent - up * intent.Dot(up) + up * ((intent.Dot(up) != 0.0f) ? intent.Dot(up) : -record->sinkSpeed) + record->swimCurrent; velocity = velocity + (target - velocity) * SDL_min(1.0f, record->swimDrag * dt); - } else if ((record->character->GetGroundState() == JPH::CharacterBase::EGroundState::OnGround) && (vertical < 0.1f)) { - // Standing: ride the ground, and jump off it if asked. - velocity = record->character->GetGroundVelocity(); - if (record->jumpSpeed > 0.0f) { + } else { + if (onGround && (vertical < STANDING_VERTICAL_SPEED)) { + // Standing: ride the ground. + velocity = record->character->GetGroundVelocity(); + } else { + // Airborne (or on a slope too steep): keep only the vertical part, and keep falling. + velocity = up * velocity.Dot(up) + scaledGravity * dt; + } + // A jump granted with ground underfoot is taken even as a platform lifts the player. + if (onGround && (record->jumpSpeed > 0.0f)) { velocity += up * record->jumpSpeed; } - } else { - // Airborne (or on a slope too steep): keep only the vertical part, and keep falling. - velocity = up * velocity.Dot(up) + scaledGravity * dt; } if (!record->swimming) { velocity += JPH::Vec3(record->intent.x, 0.0f, _world->planar ? 0.0f : record->intent.z); @@ -1052,31 +1180,26 @@ namespace { SoftRecordT *_findSoft(int32_t node) { - int32_t x; + int32_t slot = (_world != nullptr) ? _indexGet(_world->softOfNode, node) : NO_HANDLE; - if (_world == nullptr) { - return nullptr; - } - for (x = 0; x < _world->softCount; x++) { - if (_world->softs[x].used && (_world->softs[x].node == node)) { - return &_world->softs[x]; - } - } - return nullptr; + return (slot == NO_HANDLE) ? nullptr : &_world->softs[slot]; } void _releaseSoftBody(SoftRecordT *record) { if (!record->body.IsInvalid()) { - _world->system->GetBodyInterface().RemoveBody(record->body); - _world->system->GetBodyInterface().DestroyBody(record->body); - record->body = JPH::BodyID(); + _destroyBody(record->body); } } + // A rope's tube mesh is the engine's own and goes with it; cloth and pressure bodies use the + // node's mesh, which stays. void _releaseSoft(SoftRecordT *record) { _releaseSoftBody(record); + if (record->kind == SOFT_ROPE) { + meshDelete(record->mesh); + } SDL_free(record->meshToSoft); SDL_free(record->positions); SDL_free(record->meshPositions); @@ -1085,6 +1208,9 @@ namespace { void _resetSoft(SoftRecordT *record) { + if (record->used) { + _indexSet(_world->softOfNode, record->node, NO_HANDLE); + } record->shared = nullptr; record->body = JPH::BodyID(); record->node = 0; @@ -1135,7 +1261,7 @@ namespace { bool _buildSoft(SoftRecordT *record) { JPH::BodyInterface &bodies = _world->system->GetBodyInterface(); int32_t x; - float invMass = (float)record->count / SDL_max(record->mass, 0.001f); + float invMass = _softInvMass(record); float stretch = STRETCH_COMPLIANCE * (1.0f - SDL_clamp(record->stretch, 0.0f, 1.0f)); float bend = BEND_COMPLIANCE * (1.0f - SDL_clamp(record->bend, 0.0f, 1.0f)); @@ -1159,7 +1285,10 @@ namespace { const float *unused; JPH::Array attributes; - meshGetGeometry(record->mesh, &unused, &vertexCount, &indices, &indexCount); + if (!meshGetGeometry(record->mesh, &unused, &vertexCount, &indices, &indexCount) || (vertexCount != record->meshVertexCount)) { + utilTrace("Physics: the mesh under soft body node %d is gone.", record->node); + return false; + } for (x = 0; x + 2 < indexCount; x += 3) { JPH::SoftBodySharedSettings::Face face; @@ -1184,7 +1313,7 @@ namespace { settings.mLinearDamping = record->damping; settings.mNumIterations = SOFT_ITERATIONS; settings.mFacesDoubleSided = (record->kind == SOFT_CLOTH); - settings.mVertexRadius = (record->kind == SOFT_ROPE) ? record->ropeRadius : 0.03f; + settings.mVertexRadius = (record->kind == SOFT_ROPE) ? record->ropeRadius : SOFT_VERTEX_RADIUS; settings.mFriction = DEFAULT_FRICTION; body = bodies.CreateSoftBody(settings); if (body == nullptr) { @@ -1268,6 +1397,39 @@ namespace { } + // Every particle's inverse mass, from the body's mass shared out evenly. + float _softInvMass(const SoftRecordT *record) { + return (float)record->count / SDL_max(record->mass, MIN_MASS); + } + + + // Writes the particles' inverse masses into the live body (pins stay held) and wakes it. + void _softSetMasses(SoftRecordT *record) { + float invMass = _softInvMass(record); + int32_t v; + + if (record->body.IsInvalid()) { + return; + } + { + JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->body); + + if (lock.Succeeded()) { + JPH::SoftBodyMotionProperties *motion = static_cast(lock.GetBody().GetMotionProperties()); + + for (v = 0; v < record->count; v++) { + motion->GetVertex((JPH::uint)v).mInvMass = invMass; + } + for (v = 0; v < record->pinCount; v++) { + motion->GetVertex((JPH::uint)record->pins[v].vertex).mInvMass = 0.0f; + } + } + } + // Outside the lock: activating takes locks of its own. + _world->system->GetBodyInterface().ActivateBody(record->body); + } + + // After the steps: particles back from Jolt, then the mesh, in the node's own space. void _writeSoft(void) { int32_t x; @@ -1309,10 +1471,7 @@ namespace { } // World particles to mesh vertices in the node's frame. nodeGetWorldTransform(record->node, &position, &rotation, &scale); - inverse = quatNormalize(rotation); - inverse.x = -inverse.x; - inverse.y = -inverse.y; - inverse.z = -inverse.z; + inverse = quatInverse(rotation); if (record->kind == SOFT_ROPE) { _ropeMesh(record, (Vec3T *)record->meshPositions); } @@ -1320,9 +1479,9 @@ namespace { Vec3T world = (record->kind == SOFT_ROPE) ? ((Vec3T *)record->meshPositions)[v] : vec3(record->positions[record->meshToSoft[v] * 3], record->positions[record->meshToSoft[v] * 3 + 1], record->positions[record->meshToSoft[v] * 3 + 2]); Vec3T local = quatRotate(inverse, vec3Subtract(world, position)); - record->meshPositions[v * 3] = local.x / SDL_max(scale.x, 1e-6f); - record->meshPositions[v * 3 + 1] = local.y / SDL_max(scale.y, 1e-6f); - record->meshPositions[v * 3 + 2] = local.z / SDL_max(scale.z, 1e-6f); + record->meshPositions[v * 3] = local.x / SDL_max(scale.x, MIN_SCALE); + record->meshPositions[v * 3 + 1] = local.y / SDL_max(scale.y, MIN_SCALE); + record->meshPositions[v * 3 + 2] = local.z / SDL_max(scale.z, MIN_SCALE); } meshSetPositions(record->mesh, record->meshPositions); } @@ -1330,17 +1489,9 @@ namespace { RagdollRecordT *_findRagdoll(int32_t node) { - int32_t x; + int32_t slot = (_world != nullptr) ? _indexGet(_world->ragdollOfNode, node) : NO_HANDLE; - if (_world == nullptr) { - return nullptr; - } - for (x = 0; x < _world->ragdollCount; x++) { - if (_world->ragdolls[x].used && (_world->ragdolls[x].node == node)) { - return &_world->ragdolls[x]; - } - } - return nullptr; + return (slot == NO_HANDLE) ? nullptr : &_world->ragdolls[slot]; } @@ -1375,8 +1526,7 @@ namespace { void _releaseRagdollBodies(RagdollRecordT *record) { - JPH::BodyInterface &bodies = _world->system->GetBodyInterface(); - int32_t x; + int32_t x; for (x = 0; x < record->partCount; x++) { RagdollPartT *part = &record->parts[x]; @@ -1390,9 +1540,7 @@ namespace { RagdollPartT *part = &record->parts[x]; if (!part->body.IsInvalid()) { - bodies.RemoveBody(part->body); - bodies.DestroyBody(part->body); - part->body = JPH::BodyID(); + _destroyBody(part->body); } } record->active = false; @@ -1408,6 +1556,9 @@ namespace { void _resetRagdoll(RagdollRecordT *record) { int32_t x; + if (record->used) { + _indexSet(_world->ragdollOfNode, record->node, NO_HANDLE); + } for (x = 0; x < MAX_RAGDOLL_PARTS; x++) { record->parts[x].constraint = nullptr; record->parts[x].body = JPH::BodyID(); @@ -1518,7 +1669,7 @@ namespace { parentScale = JPH::Vec3::sOne(); } local = parentRotation.Conjugated() * JPH::Vec3(worldPosition[p] - parentPosition); - local = JPH::Vec3(local.GetX() / SDL_max(parentScale.GetX(), 1e-6f), local.GetY() / SDL_max(parentScale.GetY(), 1e-6f), local.GetZ() / SDL_max(parentScale.GetZ(), 1e-6f)); + local = JPH::Vec3(local.GetX() / SDL_max(parentScale.GetX(), MIN_SCALE), local.GetY() / SDL_max(parentScale.GetY(), MIN_SCALE), local.GetZ() / SDL_max(parentScale.GetZ(), MIN_SCALE)); localRotation = parentRotation.Conjugated() * worldRotation[p]; nodeSetPosition(part->joint, vec3(local.GetX(), local.GetY(), local.GetZ())); nodeSetRotation(part->joint, _toQuat(localRotation)); @@ -1528,17 +1679,9 @@ namespace { VehicleRecordT *_findVehicle(int32_t node) { - int32_t x; + int32_t slot = (_world != nullptr) ? _indexGet(_world->vehicleOfNode, node) : NO_HANDLE; - if (_world == nullptr) { - return nullptr; - } - for (x = 0; x < _world->vehicleCount; x++) { - if (_world->vehicles[x].used && (_world->vehicles[x].node == node)) { - return &_world->vehicles[x]; - } - } - return nullptr; + return (slot == NO_HANDLE) ? nullptr : &_world->vehicles[slot]; } @@ -1554,15 +1697,18 @@ namespace { void _resetVehicle(VehicleRecordT *record) { + if (record->used) { + _indexSet(_world->vehicleOfNode, record->node, NO_HANDLE); + } record->node = 0; record->generation = 0; record->kind = VEHICLE_CAR; record->wheelCount = 0; record->maxTorque = DEFAULT_ENGINE_TORQUE; record->maxRpm = DEFAULT_ENGINE_MAX_RPM; - record->minRpm = DEFAULT_ENGINE_MIN_RPM; + record->minRpm = VEHICLE_DEFAULT_MIN_RPM; record->gearCount = 0; - record->reverseGear = 0.0f; + record->reverseGear = -VEHICLE_DEFAULT_REVERSE_GEAR; record->automatic = true; record->suspensionHz = DEFAULT_SUSPENSION_HZ; record->suspensionDamping = DEFAULT_SUSPENSION_DAMPING; @@ -1574,14 +1720,33 @@ namespace { record->inputRight = 0.0f; record->inputBrake = 0.0f; record->inputHandBrake = 0.0f; - record->thrust = 2000.0f; - record->thrustPoint = vec3(0.0f, -0.2f, 1.0f); - record->rudder = 800.0f; + record->thrust = DEFAULT_THRUST; + record->thrustPoint = vec3(0.0f, DEFAULT_THRUST_Y, DEFAULT_THRUST_Z); + record->rudder = DEFAULT_RUDDER; record->dirty = false; record->used = false; } + // Engine and gearbox settings from the recipe (a script that set no gears keeps Jolt's). + void _setDrivetrain(const VehicleRecordT *record, JPH::VehicleEngineSettings &engine, JPH::VehicleTransmissionSettings &transmission) { + int32_t v; + + engine.mMaxTorque = record->maxTorque; + engine.mMaxRPM = record->maxRpm; + engine.mMinRPM = record->minRpm; + if (record->gearCount > 0) { + transmission.mGearRatios.clear(); + for (v = 0; v < record->gearCount; v++) { + transmission.mGearRatios.push_back(record->gears[v]); + } + transmission.mReverseGearRatios.clear(); + transmission.mReverseGearRatios.push_back(record->reverseGear); + } + transmission.mMode = record->automatic ? JPH::ETransmissionMode::Auto : JPH::ETransmissionMode::Manual; + } + + // Buoyancy, drag and current for every dynamic body inside a water volume, and the surfaces // players and boats test against this step. A volume's surface is the top of its box. void _applyWater(float dt) { @@ -1614,13 +1779,23 @@ namespace { } _world->system->GetBroadPhaseQuery().CollideAABox(surface.box, hits, _world->system->GetDefaultBroadPhaseLayerFilter(LAYER_MOVING), _world->system->GetDefaultLayerFilter(LAYER_MOVING)); for (h = 0; h < hits.mHits.size(); h++) { - JPH::BodyID id = hits.mHits[h]; + JPH::BodyID id = hits.mHits[h]; BodyRecordT *other; + int32_t node; - if ((id == record->id) || (bodies.GetMotionType(id) != JPH::EMotionType::Dynamic) || _isTrigger(id)) { + if (id == record->id) { continue; } - other = _find((int32_t)(uint32_t)bodies.GetUserData(id)); + { + // Rigid, dynamic and solid: soft bodies have no buoyancy (Jolt asserts on them). + JPH::BodyLockRead lock(_world->system->GetBodyLockInterface(), id); + + if (!lock.Succeeded() || !lock.GetBody().IsRigidBody() || !lock.GetBody().IsDynamic() || lock.GetBody().IsSensor()) { + continue; + } + node = (int32_t)(uint32_t)lock.GetBody().GetUserData(); + } + other = _find(node); bodies.ApplyBuoyancyImpulse(id, surface.position, surface.normal, record->waterDensity * ((other != nullptr) ? other->buoyancy : DEFAULT_BUOYANCY), record->waterLinearDrag, record->waterAngularDrag, surface.current, gravity, dt); } } @@ -1646,7 +1821,8 @@ namespace { // Builds (or rebuilds) the Jolt constraint from the recipe. The chassis faces -Z with Y up, like - // everything else in the scene; wheels sit where their nodes are relative to the chassis. + // everything else in the scene; wheels sit where their nodes are relative to the chassis. One + // try per change: a recipe that cannot be built is left alone until a setting changes. bool _buildVehicle(VehicleRecordT *record) { BodyRecordT *body = _find(record->node); JPH::VehicleConstraintSettings settings; @@ -1660,6 +1836,7 @@ namespace { int32_t w; int32_t v; + record->dirty = false; if ((body == nullptr) || (body->type != BODY_DYNAMIC)) { utilTrace("Physics: vehicle node %d needs a dynamic body as its chassis.", record->node); return false; @@ -1680,7 +1857,7 @@ namespace { if (lock.Succeeded() && (lock.GetBody().GetMotionProperties() != nullptr)) { float inverse = forward.Dot(lock.GetBody().GetMotionProperties()->GetLocalSpaceInverseInertia().Multiply3x3(forward)); - if (inverse > 1e-9f) { + if (inverse > MIN_INVERSE_INERTIA) { rollInertia = 1.0f / inverse; } } @@ -1696,7 +1873,7 @@ namespace { settings.mMaxPitchRollAngle = (record->kind == VEHICLE_MOTORCYCLE) ? JPH::DegreesToRadians(VEHICLE_MAX_TILT_DEGREES) : JPH::JPH_PI; for (w = 0; w < record->wheelCount; w++) { WheelRecordT *wheel = &record->wheels[w]; - bool front = along[w] > meanAlong + 1e-4f; + bool front = along[w] > meanAlong + FRONT_EPSILON; bool steered = wheel->steeredSet ? wheel->steered : front; bool driven = wheel->steeredSet ? wheel->driven : (record->kind != VEHICLE_MOTORCYCLE || !front); JPH::WheelSettings *base; @@ -1727,22 +1904,14 @@ namespace { wheel->driven = driven; settings.mWheels.push_back(base); } + // The settings object holds the controller settings from the moment they are made, so an + // early return frees them. if (record->kind == VEHICLE_TANK) { JPH::TrackedVehicleControllerSettings *controller = new JPH::TrackedVehicleControllerSettings(); int32_t sides[2] = { 0, 0 }; - controller->mEngine.mMaxTorque = record->maxTorque; - controller->mEngine.mMaxRPM = record->maxRpm; - controller->mEngine.mMinRPM = record->minRpm; - if (record->gearCount > 0) { - controller->mTransmission.mGearRatios.clear(); - for (v = 0; v < record->gearCount; v++) { - controller->mTransmission.mGearRatios.push_back(record->gears[v]); - } - controller->mTransmission.mReverseGearRatios.clear(); - controller->mTransmission.mReverseGearRatios.push_back(record->reverseGear); - } - controller->mTransmission.mMode = record->automatic ? JPH::ETransmissionMode::Auto : JPH::ETransmissionMode::Manual; + settings.mController = controller; + _setDrivetrain(record, controller->mEngine, controller->mTransmission); for (w = 0; w < record->wheelCount; w++) { int32_t side = (local[w].Dot(left) > 0.0f) ? (int32_t)JPH::ETrackSide::Left : (int32_t)JPH::ETrackSide::Right; @@ -1756,12 +1925,12 @@ namespace { utilTrace("Physics: a tank needs wheels on both sides of node %d.", record->node); return false; } - settings.mController = controller; } else { JPH::WheeledVehicleControllerSettings *controller = (record->kind == VEHICLE_MOTORCYCLE) ? new JPH::MotorcycleControllerSettings() : new JPH::WheeledVehicleControllerSettings(); - int32_t paired[MAX_WHEELS]; + bool paired[MAX_WHEELS]; int32_t axles = 0; + settings.mController = controller; if (record->kind == VEHICLE_MOTORCYCLE) { // Jolt's lean spring is tuned for one bike; a lighter or slimmer one flips with it. JPH::MotorcycleControllerSettings *bike = static_cast(controller); @@ -1769,23 +1938,11 @@ namespace { bike->mLeanSpringConstant = LEAN_SPRING * rollInertia / LEAN_INERTIA; bike->mLeanSpringDamping = LEAN_DAMPING * rollInertia / LEAN_INERTIA; } - - controller->mEngine.mMaxTorque = record->maxTorque; - controller->mEngine.mMaxRPM = record->maxRpm; - controller->mEngine.mMinRPM = record->minRpm; - if (record->gearCount > 0) { - controller->mTransmission.mGearRatios.clear(); - for (v = 0; v < record->gearCount; v++) { - controller->mTransmission.mGearRatios.push_back(record->gears[v]); - } - controller->mTransmission.mReverseGearRatios.clear(); - controller->mTransmission.mReverseGearRatios.push_back(record->reverseGear); - } - controller->mTransmission.mMode = record->automatic ? JPH::ETransmissionMode::Auto : JPH::ETransmissionMode::Manual; + _setDrivetrain(record, controller->mEngine, controller->mTransmission); // Driven wheels pair up across the chassis into differentials, one per axle; a lone wheel // (a motorcycle's) is an axle of its own. Anti-roll bars follow the same pairs. for (w = 0; w < record->wheelCount; w++) { - paired[w] = 0; + paired[w] = false; } for (w = 0; w < record->wheelCount; w++) { JPH::VehicleDifferentialSettings differential; @@ -1800,7 +1957,7 @@ namespace { break; } } - paired[w] = 1; + paired[w] = true; if (local[w].Dot(left) > 0.0f) { differential.mLeftWheel = w; differential.mRightWheel = partner; @@ -1809,7 +1966,7 @@ namespace { differential.mRightWheel = w; } if (partner >= 0) { - paired[partner] = 1; + paired[partner] = true; if (record->antiRoll > 0.0f) { JPH::VehicleAntiRollBar bar; @@ -1829,7 +1986,6 @@ namespace { utilTrace("Physics: vehicle node %d has no driven wheel.", record->node); return false; } - settings.mController = controller; } { JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), body->id); @@ -1844,12 +2000,11 @@ namespace { if (record->kind == VEHICLE_TANK) { record->tester = new JPH::VehicleCollisionTesterRay(LAYER_MOVING, up); } else { - record->tester = new JPH::VehicleCollisionTesterCastCylinder(LAYER_MOVING, 0.05f); + record->tester = new JPH::VehicleCollisionTesterCastCylinder(LAYER_MOVING, WHEEL_CAST_RADIUS); } record->constraint->SetVehicleCollisionTester(record->tester); _world->system->AddConstraint(record->constraint); _world->system->AddStepListener(record->constraint); - record->dirty = false; return true; } @@ -1866,8 +2021,9 @@ namespace { if (!record->used) { continue; } + // _find checks the node and its generation, which the vehicle shares with its body. body = _find(record->node); - if ((body == nullptr) || !nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) { + if (body == nullptr) { _releaseVehicle(record); continue; } @@ -1885,10 +2041,10 @@ namespace { } if (record->inputRight != 0.0f) { // The rudder bites with speed. - bodies.AddTorque(body->id, up * (-record->inputRight * record->rudder * SDL_clamp(fabsf(speed) / 3.0f, 0.2f, 1.0f) * ((speed < 0.0f) ? -1.0f : 1.0f))); + bodies.AddTorque(body->id, up * (-record->inputRight * record->rudder * SDL_clamp(fabsf(speed) / RUDDER_FULL_SPEED, RUDDER_MIN_BITE, 1.0f) * ((speed < 0.0f) ? -1.0f : 1.0f))); } if (record->inputBrake > 0.0f) { - bodies.AddForce(body->id, -bodies.GetLinearVelocity(body->id) * (record->thrust * record->inputBrake * 0.5f)); + bodies.AddForce(body->id, -bodies.GetLinearVelocity(body->id) * (record->thrust * record->inputBrake * BOAT_BRAKE_RATIO)); } } continue; @@ -1908,13 +2064,13 @@ namespace { if ((forward == 0.0f) && (record->inputRight != 0.0f)) { // Turning on the spot: the tracks run against each other, gently, or a heavy // hull hops off the ground. - forward = fabsf(record->inputRight) * 0.35f; + forward = fabsf(record->inputRight) * TANK_PIVOT_THROTTLE; leftRatio = (record->inputRight > 0.0f) ? 1.0f : -1.0f; rightRatio = -leftRatio; } else if (record->inputRight > 0.0f) { - rightRatio = SDL_max(1.0f - record->inputRight * 0.9f, 0.1f); + rightRatio = SDL_max(1.0f - record->inputRight * TANK_TURN_RATIO, TANK_TURN_MIN); } else if (record->inputRight < 0.0f) { - leftRatio = SDL_max(1.0f + record->inputRight * 0.9f, 0.1f); + leftRatio = SDL_max(1.0f + record->inputRight * TANK_TURN_RATIO, TANK_TURN_MIN); } controller->SetDriverInput(forward, leftRatio, rightRatio, record->inputBrake); } else { @@ -1945,7 +2101,7 @@ namespace { JPH::Quat rotation; JPH::RVec3 position; - if (!nodeValid(record->wheels[w].node)) { + if (!nodeValid(record->wheels[w].node) || (nodeGetGeneration(record->wheels[w].node) != record->wheels[w].generation)) { continue; } transform = record->constraint->GetWheelWorldTransform((JPH::uint)w, JPH::Vec3::sAxisX(), JPH::Vec3::sAxisY()); @@ -1957,33 +2113,11 @@ namespace { } - // Which triggers a player overlaps, against the last frame's answer: onTrigger enter and leave. - void _playerTriggers(PlayerRecordT *record) { - JPH::AllHitCollisionCollector hits; - JPH::AABox box = record->character->GetShape()->GetWorldSpaceBounds(record->character->GetCenterOfMassTransform(), JPH::Vec3::sOne()); - int32_t now[MAX_PLAYER_TRIGGERS]; - int32_t count = 0; - int32_t x; - int32_t y; - size_t h; + // The triggers a player is in now against those it was in: onTrigger enter and leave. + void _playerInside(PlayerRecordT *record, const int32_t *now, int32_t count) { + int32_t x; + int32_t y; - _world->system->GetBroadPhaseQuery().CollideAABox(box, hits, _world->system->GetDefaultBroadPhaseLayerFilter(LAYER_MOVING), _world->system->GetDefaultLayerFilter(LAYER_MOVING)); - for (h = 0; (h < hits.mHits.size()) && (count < MAX_PLAYER_TRIGGERS); h++) { - int32_t node; - bool seen = false; - - if (!_isTrigger(hits.mHits[h])) { - continue; - } - // The broad phase may name a body more than once. - node = (int32_t)(uint32_t)_world->system->GetBodyInterface().GetUserData(hits.mHits[h]); - for (x = 0; x < count; x++) { - seen = seen || (now[x] == node); - } - if (!seen) { - now[count++] = node; - } - } for (x = 0; x < count; x++) { bool had = false; @@ -1991,15 +2125,7 @@ namespace { had = had || (record->inside[y] == now[x]); } if (!had) { - PhysicsEventT event; - - event.type = PHYSICS_EVENT_ENTER; - event.nodeA = now[x]; - event.nodeB = record->node; - event.point = vec3(0.0f, 0.0f, 0.0f); - event.speed = 0.0f; - std::lock_guard guard(_world->contacts->lock); - _world->contacts->events.push_back(event); + _world->contacts->push(_triggerEvent(PHYSICS_EVENT_ENTER, now[x], record->node)); } } for (y = 0; y < record->insideCount; y++) { @@ -2009,22 +2135,69 @@ namespace { still = still || (now[x] == record->inside[y]); } if (!still) { - PhysicsEventT event; - - event.type = PHYSICS_EVENT_LEAVE; - event.nodeA = record->inside[y]; - event.nodeB = record->node; - event.point = vec3(0.0f, 0.0f, 0.0f); - event.speed = 0.0f; - std::lock_guard guard(_world->contacts->lock); - _world->contacts->events.push_back(event); + _world->contacts->push(_triggerEvent(PHYSICS_EVENT_LEAVE, record->inside[y], record->node)); } } - memcpy(record->inside, now, sizeof(int32_t) * (size_t)count); + for (x = 0; x < count; x++) { + record->inside[x] = now[x]; + } record->insideCount = count; } + // Which triggers a player overlaps this frame, reported against the last frame's answer. + void _playerTriggers(PlayerRecordT *record) { + JPH::AllHitCollisionCollector hits; + JPH::AABox box = record->character->GetShape()->GetWorldSpaceBounds(record->character->GetCenterOfMassTransform(), JPH::Vec3::sOne()); + int32_t now[MAX_PLAYER_TRIGGERS]; + int32_t count = 0; + int32_t x; + size_t h; + + _world->system->GetBroadPhaseQuery().CollideAABox(box, hits, _world->system->GetDefaultBroadPhaseLayerFilter(LAYER_MOVING), _world->system->GetDefaultLayerFilter(LAYER_MOVING)); + for (h = 0; (h < hits.mHits.size()) && (count < MAX_PLAYER_TRIGGERS); h++) { + int32_t node = _triggerNode(hits.mHits[h]); + bool seen = false; + + if (node == NO_HANDLE) { + continue; + } + // The broad phase may name a body more than once. + for (x = 0; x < count; x++) { + seen = seen || (now[x] == node); + } + if (!seen) { + now[count++] = node; + } + } + _playerInside(record, now, count); + } + + + // An enter or leave event for a trigger and what crossed it. + PhysicsEventT _triggerEvent(PhysicsEventTypeE type, int32_t trigger, int32_t node) { + PhysicsEventT event; + + event.type = type; + event.nodeA = trigger; + event.nodeB = node; + event.point = vec3(0.0f, 0.0f, 0.0f); + event.speed = 0.0f; + return event; + } + + + // The node of a Jolt body that is a trigger, or NO_HANDLE. + int32_t _triggerNode(JPH::BodyID id) { + JPH::BodyLockRead lock(_world->system->GetBodyLockInterface(), id); + + if (!lock.Succeeded() || !lock.GetBody().IsSensor()) { + return NO_HANDLE; + } + return (int32_t)(uint32_t)lock.GetBody().GetUserData(); + } + + // Players drive their nodes' positions; the script owns the rotation. void _writePlayers(void) { int32_t x; @@ -2036,7 +2209,11 @@ namespace { PlayerRecordT *record = &_world->players[x]; JPH::RVec3 where; - if (!record->used || !record->enabled || !nodeValid(record->node)) { + if (!record->used || !record->enabled) { + continue; + } + if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) { + _releasePlayer(record); continue; } _playerTriggers(record); @@ -2182,9 +2359,9 @@ bool bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, f } } if (x == _world->bodyCount) { - _world->bodies = (BodyRecordT *)SDL_realloc(_world->bodies, sizeof(BodyRecordT) * (size_t)(_world->bodyCount + 1)); - if (_world->bodies == nullptr) { - utilDie("Out of memory allocating a physics body."); + if (_world->bodyCount == MAX_BODIES) { + utilTrace("Physics: no room for another body (%d already).", MAX_BODIES); + return false; } _world->bodyCount++; } @@ -2214,6 +2391,7 @@ bool bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, f record->enabled = true; record->used = true; record->buoyancy = DEFAULT_BUOYANCY; + _indexSet(_world->bodyOfNode, node, x); return true; } @@ -2303,6 +2481,9 @@ bool bodySetTrigger(int32_t node, bool trigger) { } lock.GetBody().SetIsSensor(trigger); } + if (!trigger) { + _world->contacts->forget(node); + } record->trigger = trigger; return true; } @@ -2322,12 +2503,15 @@ bool bodySetVelocity(int32_t node, Vec3T velocity) { // ===== Joints ===== bool jointDelete(int32_t joint) { + JointRecordT *record; + if (!jointValid(joint)) { return false; } - _world->system->RemoveConstraint(_world->joints[joint].constraint); - _world->joints[joint].constraint = nullptr; - _world->joints[joint].used = false; + record = &_world->joints[(size_t)joint]; + _world->system->RemoveConstraint(record->constraint); + record->constraint = nullptr; + record->used = false; return true; } @@ -2342,6 +2526,7 @@ int32_t jointNew(JointTypeE type, int32_t nodeA, int32_t nodeB, Vec3T anchor, Ve JPH::Ref constraint; JPH::Vec3 direction; JPH::Vec3 normal; + JointRecordT record; int32_t x; if ((_world == nullptr) || (a == nullptr) || ((nodeB != WORLD_NODE) && (b == nullptr))) { @@ -2393,26 +2578,18 @@ int32_t jointNew(JointTypeE type, int32_t nodeA, int32_t nodeB, Vec3T anchor, Ve return NO_HANDLE; } _world->system->AddConstraint(constraint); - for (x = 0; x < _world->jointCount; x++) { - if (!_world->joints[x].used) { - break; + record.constraint = constraint; + record.type = type; + record.nodeA = nodeA; + record.nodeB = nodeB; + record.used = true; + for (x = 0; x < (int32_t)_world->joints.size(); x++) { + if (!_world->joints[(size_t)x].used) { + _world->joints[(size_t)x] = record; + return x; } } - if (x == _world->jointCount) { - JointRecordT *grown = new JointRecordT[_world->jointCount + 1]; - - for (int32_t y = 0; y < _world->jointCount; y++) { - grown[y] = _world->joints[y]; - } - delete[] _world->joints; - _world->joints = grown; - _world->jointCount++; - } - _world->joints[x].constraint = constraint; - _world->joints[x].type = type; - _world->joints[x].nodeA = nodeA; - _world->joints[x].nodeB = nodeB; - _world->joints[x].used = true; + _world->joints.push_back(record); return x; } @@ -2427,12 +2604,12 @@ bool jointSetLimits(int32_t joint, float low, float high) { } low = SDL_min(low, 0.0f); high = SDL_max(high, 0.0f); - if (_world->joints[joint].type == JOINT_HINGE) { - ((JPH::HingeConstraint *)_world->joints[joint].constraint.GetPtr())->SetLimits(DEGREES_TO_RADIANS(-high), DEGREES_TO_RADIANS(-low)); + if (_world->joints[(size_t)joint].type == JOINT_HINGE) { + static_cast(_world->joints[(size_t)joint].constraint.GetPtr())->SetLimits(JPH::DegreesToRadians(-high), JPH::DegreesToRadians(-low)); return true; } - if (_world->joints[joint].type == JOINT_SLIDER) { - ((JPH::SliderConstraint *)_world->joints[joint].constraint.GetPtr())->SetLimits(-high, -low); + if (_world->joints[(size_t)joint].type == JOINT_SLIDER) { + static_cast(_world->joints[(size_t)joint].constraint.GetPtr())->SetLimits(-high, -low); return true; } return false; @@ -2440,7 +2617,7 @@ bool jointSetLimits(int32_t joint, float low, float high) { bool jointValid(int32_t joint) { - return (_world != nullptr) && (joint >= 0) && (joint < _world->jointCount) && _world->joints[joint].used; + return (_world != nullptr) && (joint >= 0) && (joint < (int32_t)_world->joints.size()) && _world->joints[(size_t)joint].used; } @@ -2451,21 +2628,24 @@ bool physicsAvailable(void) { } -// Hands the engine the events the last step produced (up to maximum) and clears them. +// Hands the engine up to maximum of the queued events, oldest first, and drops only those: call +// until it returns 0 to drain a busy step. int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum) { - int32_t count = 0; + int32_t count; + int32_t x; - if (_world == nullptr) { + if ((_world == nullptr) || (maximum <= 0)) { return 0; } { std::lock_guard guard(_world->contacts->lock); + std::vector &queue = _world->contacts->events; - while ((count < maximum) && (count < (int32_t)_world->contacts->events.size())) { - events[count] = _world->contacts->events[(size_t)count]; - count++; + count = SDL_min(maximum, (int32_t)queue.size()); + for (x = 0; x < count; x++) { + events[x] = queue[(size_t)x]; } - _world->contacts->events.clear(); + queue.erase(queue.begin(), queue.begin() + count); } return count; } @@ -2500,25 +2680,25 @@ bool physicsInit(void) { _world->contacts = new ContactListenerT(); _world->system->SetContactListener(_world->contacts); _world->playerListener = new PlayerListenerT(); - _world->players = new PlayerRecordT[MAX_PLAYERS]; + // The tables are value-initialised (all zero, so nothing is used) and then given their defaults. + _world->bodies = new BodyRecordT[MAX_BODIES](); + _world->bodyCount = 0; + _world->players = new PlayerRecordT[MAX_PLAYERS](); _world->playerCount = MAX_PLAYERS; for (x = 0; x < MAX_PLAYERS; x++) { _releasePlayer(&_world->players[x]); } - _world->vehicles = new VehicleRecordT[MAX_VEHICLES]; + _world->vehicles = new VehicleRecordT[MAX_VEHICLES](); _world->vehicleCount = MAX_VEHICLES; for (x = 0; x < MAX_VEHICLES; x++) { _resetVehicle(&_world->vehicles[x]); } - _world->softs = new SoftRecordT[MAX_SOFT]; + _world->softs = new SoftRecordT[MAX_SOFT](); _world->softCount = MAX_SOFT; for (x = 0; x < MAX_SOFT; x++) { - _world->softs[x].meshToSoft = nullptr; - _world->softs[x].positions = nullptr; - _world->softs[x].meshPositions = nullptr; _resetSoft(&_world->softs[x]); } - _world->ragdolls = new RagdollRecordT[MAX_RAGDOLLS]; + _world->ragdolls = new RagdollRecordT[MAX_RAGDOLLS](); _world->ragdollCount = MAX_RAGDOLLS; _world->ragdollGroups = 1; for (x = 0; x < MAX_RAGDOLLS; x++) { @@ -2541,8 +2721,7 @@ void physicsQuit(void) { _release(&_world->bodies[x]); } } - SDL_free(_world->bodies); - delete[] _world->joints; + delete[] _world->bodies; for (x = 0; x < _world->softCount; x++) { if (_world->softs[x].used) { _releaseSoft(&_world->softs[x]); @@ -2627,19 +2806,19 @@ void physicsSet2D(bool planar) { } -// Pauses the simulation (bodies hold still) without losing it. void physicsSetDebug(uint32_t mask) { #ifdef JPH_DEBUG_RENDERER _debugMask = mask; #else _debugMask = DEBUG_NONE; if (mask != DEBUG_NONE) { - _trace("Physics: this build has no debug renderer."); + utilTrace("Physics: this build has no debug renderer."); } #endif } +// Pauses the simulation (bodies hold still) without losing it. void physicsSetEnabled(bool enabled) { if (_world != nullptr) { _world->enabled = enabled; @@ -2764,6 +2943,7 @@ bool playerNew(int32_t node, ShapeTypeE shape, float a, float b, float c) { JPH::RefConst joltShape; JPH::RefConst standing; JPH::CharacterVirtualSettings settings; + JPH::Shape::ShapeResult lifted; JPH::AABox bounds; Vec3T position; QuatT rotation; @@ -2802,7 +2982,12 @@ bool playerNew(int32_t node, ShapeTypeE shape, float a, float b, float c) { up = _playerUp(); bounds = joltShape->GetLocalBounds(); lift = (bounds.mMax.GetY() - bounds.mMin.GetY()) / 2.0f; - standing = JPH::RotatedTranslatedShapeSettings(up * lift, JPH::Quat::sIdentity(), joltShape).Create().Get(); + lifted = JPH::RotatedTranslatedShapeSettings(up * lift, JPH::Quat::sIdentity(), joltShape).Create(); + if (lifted.HasError()) { + utilTrace("Physics: player shape: %s", lifted.GetError().c_str()); + return false; + } + standing = lifted.Get(); settings.mShape = standing; settings.mInnerBodyShape = standing; settings.mInnerBodyLayer = LAYER_MOVING; @@ -2830,16 +3015,34 @@ bool playerNew(int32_t node, ShapeTypeE shape, float a, float b, float c) { record->stepHeight = DEFAULT_STEP_HEIGHT * extent / DEFAULT_EXTENT; record->enabled = true; record->used = true; + _indexSet(_world->playerOfNode, node, x); return true; } +// A disabled player neither moves nor blocks: its inner body leaves the world, and any triggers +// it stood in are left. bool playerSetEnabled(int32_t node, bool enabled) { PlayerRecordT *record = _findPlayer(node); + JPH::BodyID inner; if (record == nullptr) { return false; } + if (enabled == record->enabled) { + return true; + } + inner = record->character->GetInnerBodyID(); + if (enabled) { + if (!inner.IsInvalid()) { + _world->system->GetBodyInterface().AddBody(inner, JPH::EActivation::Activate); + } + } else { + if (!inner.IsInvalid()) { + _world->system->GetBodyInterface().RemoveBody(inner); + } + _playerInside(record, nullptr, 0); + } record->enabled = enabled; return true; } @@ -2862,7 +3065,7 @@ bool playerSetMass(int32_t node, float kilograms) { if (record == nullptr) { return false; } - record->character->SetMass(SDL_max(kilograms, 0.001f)); + record->character->SetMass(SDL_max(kilograms, MIN_MASS)); return true; } @@ -2902,7 +3105,7 @@ bool playerSetSlope(int32_t node, float degrees) { if (record == nullptr) { return false; } - record->character->SetMaxSlopeAngle(JPH::DegreesToRadians(SDL_clamp(degrees, 0.0f, 89.0f))); + record->character->SetMaxSlopeAngle(JPH::DegreesToRadians(SDL_clamp(degrees, 0.0f, MAX_SLOPE_DEGREES))); return true; } @@ -2937,22 +3140,19 @@ int32_t vehicleAddWheel(int32_t node, int32_t wheelNode, float radius, float wid Vec3T chassisPosition; QuatT chassisRotation; Vec3T chassisScale; - QuatT inverse; if ((record == nullptr) || !nodeValid(wheelNode) || (record->wheelCount >= MAX_WHEELS)) { return -1; } - // The attachment point is the wheel node's place in the chassis' frame right now; the engine - // poses the node with suspension travel from here on, so a later rebuild must not read it back. + // The attachment point is the wheel node's place in the chassis' frame right now (the body's + // frame is unscaled: the chassis' scale is baked into its shape); the engine poses the node + // with suspension travel from here on, so a later rebuild must not read it back. sceneUpdateTransforms(); nodeGetWorldTransform(node, &chassisPosition, &chassisRotation, &chassisScale); - inverse = quatNormalize(chassisRotation); - inverse.x = -inverse.x; - inverse.y = -inverse.y; - inverse.z = -inverse.z; wheel = &record->wheels[record->wheelCount]; wheel->node = wheelNode; - wheel->rest = quatRotate(inverse, vec3Subtract(nodeGetWorldPosition(wheelNode), chassisPosition)); + wheel->generation = nodeGetGeneration(wheelNode); + wheel->rest = quatRotate(quatInverse(chassisRotation), vec3Subtract(nodeGetWorldPosition(wheelNode), chassisPosition)); wheel->radius = SDL_max(radius, MIN_DIMENSION); wheel->width = SDL_max(width, MIN_DIMENSION); wheel->suspension = SDL_max(suspension, MIN_DIMENSION); @@ -3088,6 +3288,7 @@ bool vehicleNew(int32_t node, VehicleKindE kind) { record->kind = kind; record->dirty = (kind != VEHICLE_BOAT); record->used = true; + _indexSet(_world->vehicleOfNode, node, x); return true; } @@ -3123,9 +3324,9 @@ bool vehicleSetEngine(int32_t node, float maxTorque, float maxRpm, float minRpm) if (record == nullptr) { return false; } - record->maxTorque = SDL_max(1.0f, maxTorque); - record->maxRpm = SDL_max(100.0f, maxRpm); - record->minRpm = SDL_clamp(minRpm, 1.0f, record->maxRpm); + record->maxTorque = SDL_max(MIN_ENGINE_TORQUE, maxTorque); + record->maxRpm = SDL_max(MIN_ENGINE_MAX_RPM, maxRpm); + record->minRpm = SDL_clamp(minRpm, MIN_ENGINE_RPM, record->maxRpm); record->dirty = true; return true; } @@ -3135,7 +3336,7 @@ bool vehicleSetGears(int32_t node, const float *ratios, int32_t count, float rev VehicleRecordT *record = _findVehicle(node); int32_t x; - if ((record == nullptr) || (count < 1) || (count > MAX_GEARS)) { + if ((record == nullptr) || (count < 1) || (count > VEHICLE_MAX_GEARS)) { return false; } for (x = 0; x < count; x++) { @@ -3155,7 +3356,7 @@ bool vehicleSetSteering(int32_t node, float maxDegrees) { if (record == nullptr) { return false; } - record->maxSteer = SDL_clamp(maxDegrees, 0.0f, 89.0f); + record->maxSteer = SDL_clamp(maxDegrees, 0.0f, MAX_STEER_DEGREES); record->dirty = true; return true; } @@ -3167,7 +3368,7 @@ bool vehicleSetSuspension(int32_t node, float frequency, float damping) { if (record == nullptr) { return false; } - record->suspensionHz = SDL_max(0.1f, frequency); + record->suspensionHz = SDL_max(MIN_SUSPENSION_HZ, frequency); record->suspensionDamping = SDL_max(0.0f, damping); record->dirty = true; return true; @@ -3275,6 +3476,8 @@ bool vehicleSetThrust(int32_t node, float maxForce, Vec3T point) { bool ragdollActivate(int32_t node) { RagdollRecordT *record = _findRagdoll(node); JPH::BodyInterface &bodies = _world->system->GetBodyInterface(); + const int32_t *joints; + int32_t jointCount; int32_t p; if (record == nullptr) { @@ -3285,6 +3488,7 @@ bool ragdollActivate(int32_t node) { } animationStop(record->node, ANIMATION_ALL_LAYERS); sceneUpdateTransforms(); + jointCount = nodeGetSkinJoints(record->skinned, &joints); record->filter = new JPH::GroupFilterTable((JPH::uint)record->partCount); // Bodies: a capsule along each bone, from the joint toward its children. for (p = 0; p < record->partCount; p++) { @@ -3310,12 +3514,10 @@ bool ragdollActivate(int32_t node) { if (_ragdollPartOf(record, child) < 0) { // A leaf joint still gives the bone its length. - bool isJoint = false; + bool isJoint = false; int32_t j; - const int32_t *joints; - int32_t count = nodeGetSkinJoints(record->skinned, &joints); - for (j = 0; j < count; j++) { + for (j = 0; j < jointCount; j++) { isJoint = isJoint || (joints[j] == child); } if (!isJoint) { @@ -3329,23 +3531,23 @@ bool ragdollActivate(int32_t node) { if (children > 0) { toward = toward / (float)children; } else { - toward = origin + JPH::Vec3(0.0f, RAGDOLL_RADIUS_MIN * 4.0f, 0.0f); + toward = origin + JPH::Vec3(0.0f, RAGDOLL_RADIUS_MIN * LEAF_BONE_RADII, 0.0f); } direction = toward - origin; - length = SDL_max(direction.Length(), RAGDOLL_RADIUS_MIN * 2.0f); - direction = (direction.LengthSq() > 1e-10f) ? direction.Normalized() : JPH::Vec3::sAxisY(); + length = SDL_max(direction.Length(), RAGDOLL_RADIUS_MIN * MIN_BONE_RADII); + direction = (direction.LengthSq() > ZERO_LENGTH_SQ) ? direction.Normalized() : JPH::Vec3::sAxisY(); radius = (part->radius > 0.0f) ? part->radius : SDL_max(length * RAGDOLL_RADIUS_RATIO, RAGDOLL_RADIUS_MIN); rotation = JPH::Quat::sFromTo(JPH::Vec3::sAxisY(), direction); centre = JPH::RVec3(origin + direction * (length / 2.0f)); { - JPH::CapsuleShapeSettings capsule(SDL_max(length / 2.0f - radius, 0.001f), radius); + JPH::CapsuleShapeSettings capsule(SDL_max(length / 2.0f - radius, MIN_DIMENSION), radius); JPH::BodyCreationSettings settings(capsule.Create().Get(), centre, rotation, JPH::EMotionType::Dynamic, LAYER_MOVING); settings.mUserData = (JPH::uint64)(uint32_t)part->joint; settings.mFriction = DEFAULT_FRICTION; settings.mRestitution = 0.0f; - settings.mLinearDamping = 0.2f; - settings.mAngularDamping = 0.5f; + settings.mLinearDamping = RAGDOLL_LINEAR_DAMPING; + settings.mAngularDamping = RAGDOLL_ANGULAR_DAMPING; settings.mCollisionGroup = JPH::CollisionGroup(record->filter, _world->ragdollGroups, (JPH::CollisionGroup::SubGroupID)p); part->body = bodies.CreateAndAddBody(settings, JPH::EActivation::Activate); } @@ -3356,7 +3558,6 @@ bool ragdollActivate(int32_t node) { part->offsetPosition = rotation.Conjugated() * (origin - JPH::Vec3(centre)); part->offsetRotation = rotation.Conjugated() * _fromQuat(jointRotation); part->scale = JPH::Vec3(jointScale.x, jointScale.y, jointScale.z); - part->rest = rotation; } _world->ragdollGroups++; // Joints: a swing-twist cone at each joint to the parent bone, parent-child collision off. @@ -3551,6 +3752,7 @@ bool ragdollNew(int32_t node) { record->generation = nodeGetGeneration(node); record->skinned = skinned; record->used = true; + _indexSet(_world->ragdollOfNode, node, (int32_t)(record - _world->ragdolls)); return true; } @@ -3568,8 +3770,8 @@ bool ragdollSetJoint(int32_t node, const char *joint, float radius, float swingD if ((name != nullptr) && (SDL_strcasecmp(name, joint) == 0)) { record->parts[p].radius = SDL_max(0.0f, radius); - record->parts[p].swing = SDL_clamp(swingDegrees, 0.0f, 179.0f); - record->parts[p].twist = SDL_clamp(twistDegrees, 0.0f, 179.0f); + record->parts[p].swing = SDL_clamp(swingDegrees, 0.0f, MAX_CONE_DEGREES); + record->parts[p].twist = SDL_clamp(twistDegrees, 0.0f, MAX_CONE_DEGREES); return true; } } @@ -3607,7 +3809,8 @@ bool softExists(int32_t node) { // The node's mesh becomes cloth or a pressure body: its vertices, welded by position, are the -// particles, in world space where the node has put them. +// particles, in world space where the node has put them. Welding hashes each vertex's SOFT_WELD +// cell, so vertices split for their UVs or normals (the same position exactly) become one particle. bool softNew(int32_t node, SoftKindE kind) { SoftRecordT *record = nullptr; const float *positions; @@ -3619,7 +3822,7 @@ bool softNew(int32_t node, SoftKindE kind) { QuatT rotation; Vec3T scale; int32_t x; - int32_t y; + std::unordered_map welded; if ((_world == nullptr) || !nodeValid(node) || (kind == SOFT_ROPE)) { return false; @@ -3650,22 +3853,22 @@ bool softNew(int32_t node, SoftKindE kind) { utilDie("Out of memory for a soft body."); } record->meshVertexCount = vertexCount; + welded.reserve((size_t)vertexCount); for (x = 0; x < vertexCount; x++) { - Vec3T local = vec3(positions[x * 3] * scale.x, positions[x * 3 + 1] * scale.y, positions[x * 3 + 2] * scale.z); - Vec3T world = vec3Add(position, quatRotate(rotation, local)); - int32_t found = -1; + Vec3T local = vec3(positions[x * 3] * scale.x, positions[x * 3 + 1] * scale.y, positions[x * 3 + 2] * scale.z); + Vec3T world = vec3Add(position, quatRotate(rotation, local)); + WeldKeyT key = { (int32_t)floorf(world.x / SOFT_WELD), (int32_t)floorf(world.y / SOFT_WELD), (int32_t)floorf(world.z / SOFT_WELD) }; + std::unordered_map::iterator it = welded.find(key); + int32_t found; - for (y = 0; y < record->count; y++) { - if ((fabsf(record->positions[y * 3] - world.x) < SOFT_WELD) && (fabsf(record->positions[y * 3 + 1] - world.y) < SOFT_WELD) && (fabsf(record->positions[y * 3 + 2] - world.z) < SOFT_WELD)) { - found = y; - break; - } - } - if (found < 0) { + if (it != welded.end()) { + found = it->second; + } else { found = record->count++; record->positions[found * 3] = world.x; record->positions[found * 3 + 1] = world.y; record->positions[found * 3 + 2] = world.z; + welded[key] = found; } record->meshToSoft[x] = found; } @@ -3674,6 +3877,7 @@ bool softNew(int32_t node, SoftKindE kind) { record->kind = kind; record->mesh = mesh; record->used = true; + _indexSet(_world->softOfNode, node, (int32_t)(record - _world->softs)); if (!_buildSoft(record)) { _releaseSoft(record); return false; @@ -3694,6 +3898,7 @@ bool softNewRope(int32_t node, Vec3T end, int32_t segments, float radius) { QuatT rotation; Vec3T scale; QuatT inverse; + int32_t indexCount; int32_t count; int32_t mesh; int32_t x; @@ -3716,15 +3921,16 @@ bool softNewRope(int32_t node, Vec3T end, int32_t segments, float radius) { _resetSoft(record); sceneUpdateTransforms(); nodeGetWorldTransform(node, &position, &rotation, &scale); - start = position; - count = segments + 1; + start = position; + count = segments + 1; + indexCount = segments * ROPE_SIDES * ROPE_INDICES_PER_QUAD; record->count = count; record->meshVertexCount = count * ROPE_SIDES; record->positions = (float *)SDL_calloc((size_t)count * 3, sizeof(float)); record->meshPositions = (float *)SDL_calloc((size_t)record->meshVertexCount * 3, sizeof(float)); record->meshToSoft = (int32_t *)SDL_calloc((size_t)record->meshVertexCount, sizeof(int32_t)); vertices = (SceneVertexT *)SDL_calloc((size_t)record->meshVertexCount, sizeof(SceneVertexT)); - indices = (uint32_t *)SDL_calloc((size_t)segments * ROPE_SIDES * 6, sizeof(uint32_t)); + indices = (uint32_t *)SDL_calloc((size_t)indexCount, sizeof(uint32_t)); rings = (Vec3T *)SDL_calloc((size_t)record->meshVertexCount, sizeof(Vec3T)); if ((record->positions == nullptr) || (record->meshPositions == nullptr) || (record->meshToSoft == nullptr) || (vertices == nullptr) || (indices == nullptr) || (rings == nullptr)) { utilDie("Out of memory for a rope."); @@ -3740,18 +3946,15 @@ bool softNewRope(int32_t node, Vec3T end, int32_t segments, float radius) { record->kind = SOFT_ROPE; // The tube, in the node's frame, with the rope's length running along V. _ropeMesh(record, rings); - inverse = quatNormalize(rotation); - inverse.x = -inverse.x; - inverse.y = -inverse.y; - inverse.z = -inverse.z; + inverse = quatInverse(rotation); for (x = 0; x < count; x++) { for (side = 0; side < ROPE_SIDES; side++) { int32_t v = x * ROPE_SIDES + side; Vec3T local = quatRotate(inverse, vec3Subtract(rings[v], position)); - vertices[v].position[0] = local.x / SDL_max(scale.x, 1e-6f); - vertices[v].position[1] = local.y / SDL_max(scale.y, 1e-6f); - vertices[v].position[2] = local.z / SDL_max(scale.z, 1e-6f); + vertices[v].position[0] = local.x / SDL_max(scale.x, MIN_SCALE); + vertices[v].position[1] = local.y / SDL_max(scale.y, MIN_SCALE); + vertices[v].position[2] = local.z / SDL_max(scale.z, MIN_SCALE); vertices[v].uv[0] = (float)side / (float)ROPE_SIDES; vertices[v].uv[1] = (float)x / (float)segments; vertices[v].weights[0] = 1.0f; @@ -3764,7 +3967,7 @@ bool softNewRope(int32_t node, Vec3T end, int32_t segments, float radius) { uint32_t b = (uint32_t)(x * ROPE_SIDES + (side + 1) % ROPE_SIDES); uint32_t c = a + ROPE_SIDES; uint32_t d = b + ROPE_SIDES; - uint32_t *tri = &indices[(x * ROPE_SIDES + side) * 6]; + uint32_t *tri = &indices[(x * ROPE_SIDES + side) * ROPE_INDICES_PER_QUAD]; tri[0] = a; tri[1] = c; @@ -3774,12 +3977,12 @@ bool softNewRope(int32_t node, Vec3T end, int32_t segments, float radius) { tri[5] = d; } } - sceneComputeNormals(vertices, record->meshVertexCount, indices, segments * ROPE_SIDES * 6); - mesh = meshNewVertices(vertices, record->meshVertexCount, indices, segments * ROPE_SIDES * 6, false); + sceneComputeNormals(vertices, record->meshVertexCount, indices, indexCount); + mesh = meshNewVertices(vertices, record->meshVertexCount, indices, indexCount, false); SDL_free(vertices); SDL_free(indices); SDL_free(rings); - if (mesh < 0) { + if (mesh == NO_HANDLE) { _releaseSoft(record); return false; } @@ -3788,6 +3991,7 @@ bool softNewRope(int32_t node, Vec3T end, int32_t segments, float radius) { record->generation = nodeGetGeneration(node); record->mesh = mesh; record->used = true; + _indexSet(_world->softOfNode, node, (int32_t)(record - _world->softs)); if (!_buildSoft(record)) { _releaseSoft(record); return false; @@ -3832,6 +4036,7 @@ bool softPin(int32_t node, Vec3T point, int32_t follow) { } +// Damping is a live setting of the body; nothing is rebuilt. bool softSetDamping(int32_t node, float damping) { SoftRecordT *record = _findSoft(node); @@ -3839,18 +4044,31 @@ bool softSetDamping(int32_t node, float damping) { return false; } record->damping = SDL_max(0.0f, damping); - return _buildSoft(record); + if (!record->body.IsInvalid()) { + { + JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->body); + + if (lock.Succeeded()) { + lock.GetBody().GetMotionProperties()->SetLinearDamping(record->damping); + } + } + // Outside the lock: activating takes locks of its own. + _world->system->GetBodyInterface().ActivateBody(record->body); + } + return true; } +// Mass is shared out over the particles in place; nothing is rebuilt. bool softSetMass(int32_t node, float kilograms) { SoftRecordT *record = _findSoft(node); if (record == nullptr) { return false; } - record->mass = SDL_max(0.001f, kilograms); - return _buildSoft(record); + record->mass = SDL_max(MIN_MASS, kilograms); + _softSetMasses(record); + return true; } @@ -3885,10 +4103,15 @@ bool softSetStiffness(int32_t node, float stretch, float bend) { } record->stretch = SDL_clamp(stretch, 0.0f, 1.0f); record->bend = SDL_clamp(bend, 0.0f, 1.0f); - return _buildSoft(record); + if (!_buildSoft(record)) { + _releaseSoft(record); + return false; + } + return true; } +// Lets the particle nearest a world point go, giving it its share of the mass back. bool softUnpin(int32_t node, Vec3T point) { SoftRecordT *record = _findSoft(node); int32_t vertex; @@ -3902,7 +4125,8 @@ bool softUnpin(int32_t node, Vec3T point) { if (record->pins[x].vertex == vertex) { record->pins[x] = record->pins[record->pinCount - 1]; record->pinCount--; - return _buildSoft(record); + _softSetMasses(record); + return true; } } return false; diff --git a/src/rotoZoom.c b/src/rotoZoom.c index 7e8377f29..05b4cf761 100644 --- a/src/rotoZoom.c +++ b/src/rotoZoom.c @@ -56,20 +56,31 @@ static void _sampleNearest(const SDL_Surface *source, double sx, double sy, uint // Bilinear blend of the four neighbours. Samples off the edge count as transparent. static void _sampleSmooth(const SDL_Surface *source, double sx, double sy, uint8_t *out) { - int32_t x0 = (int32_t)floor(sx - 0.5); - int32_t y0 = (int32_t)floor(sy - 0.5); - 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]; - int32_t sum = 0; - int32_t c = 0; - int32_t i = 0; + int32_t x0 = (int32_t)floor(sx - 0.5); + int32_t y0 = (int32_t)floor(sy - 0.5); + 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]; + 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; } diff --git a/src/scene.c b/src/scene.c index d20b567c8..c6cc464fc 100644 --- a/src/scene.c +++ b/src/scene.c @@ -27,8 +27,11 @@ // Everything in the scene is a node in one tree (node 0 is the root): a node has a transform, and // optionally a mesh with a material, or a light. Meshes, materials and nodes are addressed from // Lua by integer handles that index the arrays below; a freed slot is reused. Shaders come -// precompiled from sceneShaders.h (see src/shaders/build.sh). +// precompiled from the generated sceneShaders.h (cmake/shaderHeader.cmake builds it from +// src/shaders/scene.hlsl); the limits and codes the uniform blocks share with them live in +// sceneShared.h. +#include #include #include #include @@ -39,48 +42,61 @@ #define COLOUR_MAX 255.0f -#define MAX_LIGHTS 8 #define MAX_ANISOTROPY 8.0f // Texture samples along a grazing surface #define POST_VERTICES 3 // One triangle covers the screen -#define MAX_VIEWS 4 // Cameras rendered to textures besides the main one #define BLOOM_LEVELS 5 // Half-size chain for the glow +#define BLOOM_LEVELS_MIN 2 // Fewer and the up pass has nothing to add (tiny targets) #define BLOOM_MIN_SIZE 8 // Smallest level, pixels #define VIEW_SIZE_MAX 4096 // Pixels per side of a rendered view #define TEXTURE_SIZES_STEP 64 // Growth of the texture size table #define DEFAULT_BLOOM_THRESHOLD 1.0f #define MATERIAL_SAMPLERS 7 // Base, shadows, normal, occlusion, metallic-roughness, emissive, sky +#define FRAME_UNIFORMS 0 // Fragment uniform slots: the pass's FragmentUniformsT ... +#define MATERIAL_UNIFORMS 1 // ... and the batch's MaterialUniformsT +#define DRAW_UNIFORMS 0 // Vertex uniform slots: DrawUniformsT ... +#define SKIN_UNIFORMS 1 // ... and SkinUniformsT #define CUBE_FACE_MIN 16 // Sky cube face sizes, a power of two from the source's height #define CUBE_FACE_MAX 1024 -#define SH_COEFFICIENTS 9 // Spherical harmonics to second order #define SH_SAMPLES_ACROSS 128 // Equirect columns sampled for the harmonics #define HALF_BYTES 2 #define CUBE_CHANNELS 4 -#define TEXTURE_NONE 0.0f // material.w in the fragment uniforms -#define TEXTURE_SRGB 1.0f -#define TEXTURE_FEED 2.0f -#define MAX_JOINTS 128 -#define INITIAL_CAPACITY 64 #define PIPELINE_COUNT 8 // skinned x blend x double sided #define PIPELINE_SKINNED 1 #define PIPELINE_BLEND 2 #define PIPELINE_TWO_SIDED 4 +#define SHADOW_PIPELINES 4 // skinned x double sided +#define SHADOW_PIPELINE_SKINNED 1 +#define SHADOW_PIPELINE_TWO_SIDED 2 +#define SAMPLE_SETS 2 // Pipelines per target sample count ... +#define SAMPLE_SET_SINGLE 0 // ... single sample (views, and the window without antialiasing) ... +#define SAMPLE_SET_MULTI 1 // ... and the window's multisampled targets +#define MESH_ATTRIBUTES 6 // Vertex attributes of a SceneVertexT #define PARTICLE_PIPELINES 2 // PARTICLE_ALPHA, PARTICLE_ADD #define PARTICLE_VERTICES 6 // Two triangles per particle, unindexed #define DYNAMIC_BUFFER_MIN 65536 // Bytes: the smallest per-frame vertex buffer #define PARTICLE_DRAW_MAX 64 // 3D emitters drawn per frame +#define PARTICLE_FRAMES_MAX 16 // Frames (runs) per emitter the run table allows for +#define PARTICLE_RUN_MAX (PARTICLE_DRAW_MAX * PARTICLE_FRAMES_MAX) // Runs beyond this are dropped #define DEFAULT_FOV 60.0f #define DEFAULT_NEAR 0.1f #define DEFAULT_FAR 1000.0f -#define DEFAULT_EYE_Z 5.0f +#define DEFAULT_EYE_Z 5.0f // The default camera, looking at the origin from +Z +#define DEFAULT_ORTHO_HEIGHT 5.0f // World units the default orthographic view spans vertically +#define DEFAULT_ROUGHNESS 0.5f +#define DEFAULT_CONE_INNER 20.0f // A new spot light's cone, degrees +#define DEFAULT_CONE_OUTER 30.0f #define MIN_SEGMENTS 3 #define NO_HANDLE -1 -#define SHADOW_PIPELINES 4 // skinned x double sided -#define MAX_SHADOWS MAX_LIGHTS // Every light may cast; the arrays are sized to the lights in use #define SHADOW_NEAR_MIN 0.01f +#define SHADOW_FAR_MIN 0.02f // A point or spot shadow's frustum, however close its casters +#define SHADOW_NEAR_FRACTION 0.5f // Near no more than this far along a point or spot shadow's frustum +#define SPOT_SHADOW_FOV_MAX 170.0f // A spot shadow's perspective, degrees; wider is unusable +#define SHADOW_DEPTH_BIAS_CONSTANT 2.0f // Rasterizer bias in the shadow passes against self-shadowing +#define SHADOW_DEPTH_BIAS_SLOPE 2.0f +#define UP_PARALLEL_LIMIT 0.99f // |direction.y| above this is straight up or down: use another up +#define BOUNDS_PAD 0.001f // Added to every bounding radius so flat meshes have some +#define SQRT2 1.41421356f #define CUBE_FACES 6 -#define SHADOW_NONE 0 -#define SHADOW_MAP 1 -#define SHADOW_CUBE 2 #define SHADOW_SIZE 1024 #define DEFAULT_AMBIENT 26 // sRGB; the same look as the old 0.1 in gamma space #define MIN_EXPOSURE -10.0f // Stops @@ -90,14 +106,11 @@ #define SHADOW_SIZE_MAX 4096 #define SHADOW_BIAS 0.0015f #define SHADOW_MARGIN 1.05f // The fitted light frustum, a little larger than the scene -#define SHADOW_CASCADE 3 // A directional light's shadow split along the camera's view -#define MAX_CASCADES 4 #define DEFAULT_CASCADES 3 #define DEFAULT_SHADOW_DISTANCE 60.0f // How far from the camera cascaded shadows reach #define CASCADE_LAMBDA 0.7f // The practical split scheme's mix of log and linear splits #define CASCADE_NEAR 0.01f #define SKIN_BOUNDS_GROW 1.5f // A skinned mesh moves beyond its bind pose -#define MAX_MORPHS 8 // Active morph targets per draw (the shader's limit) #define MORPH_FLOATS 8 // Per target per vertex: position delta xyz + pad, normal delta xyz + pad @@ -123,15 +136,11 @@ typedef struct LightUniformS { float cone[4]; } LightUniformT; +// What every draw in a pass shares. typedef struct FragmentUniformsS { float cameraPosition[4]; float cameraForward[4]; float ambient[4]; - float baseColor[4]; - float emissive[4]; - float material[4]; - float maps[4]; - float tiling[4]; // x, y = texture repeats across the surface float counts[4]; float shadowParams[4]; Mat4T shadowMatrix[MAX_SHADOWS * MAX_CASCADES]; // Per slot, one per cascade @@ -144,6 +153,15 @@ typedef struct FragmentUniformsS { float sh[SH_COEFFICIENTS][4]; } FragmentUniformsT; +// Matches MaterialUniforms in scene.hlsl: what changes per draw batch. +typedef struct MaterialUniformsS { + float baseColor[4]; + float emissive[4]; + float material[4]; // x = metallic, y = roughness, z = unlit (1/0), w = TEXTURE_* + float maps[4]; // x = normal map strength (0 = none), y = occlusion strength (0 = none) + float tiling[4]; // x, y = texture repeats across the surface +} MaterialUniformsT; + // One light's shadow for this frame. typedef struct ShadowS { int32_t node; @@ -257,7 +275,6 @@ typedef struct NodeS { // A node showing a picture (or text) on the shared quad: its frames as textures, and a private // material that borrows the current frame. typedef struct SpriteNodeS { - int32_t node; SDL_GPUTexture **frames; int32_t count; int32_t frame; @@ -270,8 +287,9 @@ typedef struct SpriteNodeS { typedef struct DrawS { int32_t node; float depth; // View-space distance, for sorting blended draws - Vec3T centre; // World bounding sphere, filled by _fitShadows + Vec3T centre; // World bounding sphere, filled by _boundDraws float radius; + int32_t skin; // Into the frame's skin matrices (_fillInstances), NO_HANDLE unskinned } DrawT; // Matches SkinUniforms in scene.hlsl. @@ -283,8 +301,7 @@ typedef struct SkinUniformsS { typedef struct ParticleVertexS { float centre[3]; float corner[2]; - float size; - float angle; + float sizeAngle[2]; // Size in world units, rotation in degrees float colour[4]; float uv[2]; } ParticleVertexT; @@ -331,6 +348,7 @@ typedef struct BloomUniformsS { // One camera's render: where it looks from and what it draws into. typedef struct CameraFrameS { Mat4T view; + Mat4T world; // The camera's own transform (view's inverse): its axes and eye Mat4T viewProjection; Vec3T eye; Vec3T right; @@ -338,6 +356,7 @@ typedef struct CameraFrameS { Vec3T forward; int32_t width; int32_t height; + int32_t sampleSet; // SAMPLE_SET_* the pipelines drawing into the targets come from SDL_GPUTexture *colour; // HDR, resolved SDL_GPUTexture *multisampled; // Or NULL SDL_GPUTexture *depth; @@ -389,6 +408,15 @@ typedef struct DepthOrderS { int32_t index; } DepthOrderT; +// A one-off upload in three steps (_stageBegin, _stageCopy, _stageEnd): a transfer buffer mapped +// for filling, a copy pass for the copies out of it, then submit and release. +typedef struct StagingS { + SDL_GPUTransferBuffer *transfer; + SDL_GPUCommandBuffer *commands; + SDL_GPUCopyPass *pass; + void *mapped; +} StagingT; + // What a point light's six faces were last rendered from, so unchanged ones are kept. typedef struct ShadowCacheS { int32_t node; @@ -415,7 +443,7 @@ typedef struct SceneS { SDL_GPUShader *particleFragment; SDL_GPUShader *lineVertex; SDL_GPUShader *lineFragment; - SDL_GPUGraphicsPipeline *linePipeline; + SDL_GPUGraphicsPipeline *linePipeline[SAMPLE_SETS]; SDL_GPUShader *postVertex; SDL_GPUShader *postFragment; SDL_GPUGraphicsPipeline *postPipeline; @@ -427,7 +455,7 @@ typedef struct SceneS { int32_t skyLevels; SDL_GPUSampler *skySampler; // Trilinear, clamped SDL_GPUShader *skyFragment; - SDL_GPUGraphicsPipeline *skyPipeline; + SDL_GPUGraphicsPipeline *skyPipeline[SAMPLE_SETS]; float skyIntensity; Vec3T sh[SH_COEFFICIENTS]; // The sky's diffuse light bool environment; // Light the scene from the sky when there is one @@ -439,12 +467,18 @@ typedef struct SceneS { SDL_GPUTexture *output; // What the post pass writes and the composite wraps float exposure; // In stops SceneTonemapE tonemap; - SDL_GPUGraphicsPipeline *particlePipelines[PARTICLE_PIPELINES]; + SDL_GPUGraphicsPipeline *particlePipelines[SAMPLE_SETS][PARTICLE_PIPELINES]; InstanceMatricesT *instances; // This frame's matrices, one per draw ... int32_t instanceRoom; // ... how many the array holds ... SDL_GPUBuffer *instanceBuffer; // ... and on the GPU SDL_GPUTransferBuffer *instanceTransfer; uint32_t instanceCapacity; // Bytes the GPU buffers hold + SkinUniformsT *skins; // This frame's joint matrices, one block per skinned draw ... + int32_t skinRoom; // ... and how many the array holds + bool *skip; // Scratch: a flag per draw for culling ... + int32_t skipRoom; // ... and how many it holds + DepthOrderT *particleOrder; // Scratch: one emitter's particles sorted by depth ... + int32_t particleOrderRoom; // ... and how many it holds int32_t statTotal; // Last frame: draws collected ... int32_t statDrawn; // ... inside the view ... int32_t statBatches; // ... and draw calls they became @@ -485,11 +519,11 @@ typedef struct SceneS { ParticleVertexT *particleVertices; // CPU side, grown as needed int32_t particleVertexCapacity; int32_t particleVertexCount; - ParticleRunT particleRuns[PARTICLE_DRAW_MAX * 16]; + ParticleRunT particleRuns[PARTICLE_RUN_MAX]; int32_t particleRunCount; ParticleTexturesT *particleTextures; int32_t particleTextureCount; - SDL_GPUGraphicsPipeline *pipelines[PIPELINE_COUNT]; + SDL_GPUGraphicsPipeline *pipelines[SAMPLE_SETS][PIPELINE_COUNT]; SDL_GPUGraphicsPipeline *shadowPipelines[SHADOW_PIPELINES]; ShadowCacheT shadowCache[MAX_SHADOWS]; uint32_t shadowMapsVersion; // Bumped whenever the map array is (re)made @@ -536,88 +570,103 @@ static int32_t _addMesh(const SceneVertexT *vertices, int32_t vertexCou static int32_t _allocFeed(int32_t player); static int32_t _allocMaterial(void); static int32_t _allocNode(void); +static void _alphaBlendState(SDL_GPUColorTargetDescription *colour, bool additive); +static void _attach(int32_t node, int32_t parent); static void _boundDraws(int32_t drawCount); static void _cameraFrame(int32_t camera, int32_t width, int32_t height, CameraFrameT *frame); -static void _attach(int32_t node, int32_t parent); +static int32_t _compareDepth(float a, float b); static int32_t _compareDepthOrder(const void *a, const void *b); static int32_t _compareDraws(const void *a, const void *b); static int32_t _compareOpaque(const void *a, const void *b); -static bool _createLinePipeline(void); -static bool _createParticlePipeline(int32_t blend); -static bool _createPipeline(int32_t variant); +static void _computeSh(const float *rgb, int32_t width, int32_t height); static bool _createBloomPipelines(void); static bool _createBloomTargets(int32_t width, int32_t height); +static bool _createLinePipeline(int32_t sampleSet); +static bool _createParticlePipeline(int32_t sampleSet, int32_t blend); +static bool _createPipeline(int32_t sampleSet, int32_t variant); static bool _createPostPipeline(void); -static void _computeSh(const float *rgb, int32_t width, int32_t height); -static bool _createSkyPipeline(void); static SDL_GPUShader *_createShader(const SceneShaderT *shader, SDL_GPUShaderStage stage, uint32_t samplers, uint32_t uniforms, uint32_t storageBuffers); static bool _createShaders(void); static SDL_GPUTexture *_createShadowArray(SDL_GPUTextureType type, int32_t layers, int32_t size); static bool _createShadowMaps(int32_t layers); static bool _createShadowPipeline(int32_t variant); -static SDL_GPUTextureFormat _depthFormat(void); +static bool _createSkyPipeline(int32_t sampleSet); +static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount); static void _cullCascade(const ShadowT *shadow, int32_t cascade, int32_t drawCount, bool *skip); static void _cullDraws(const Mat4T *viewProjection, int32_t drawCount, bool *skip); static void _cullFace(const ShadowT *shadow, int32_t face, int32_t drawCount, bool *skip); -static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount); +static SDL_GPUTextureFormat _depthFormat(void); +static void _describeMeshVertex(SDL_GPUVertexBufferDescription *buffer, SDL_GPUVertexAttribute *attributes); +static void _destroyBloomTargets(void); static void _destroyPipelines(void); static void _destroyShadowMaps(void); static void _destroyTargets(void); static void _detach(int32_t node); -static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, bool shadowPass, bool twoSided, const bool *skip, FragmentUniformsT *fragmentUniforms); -static void _destroyBloomTargets(void); static void _drawBloom(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame); static void _drawLines(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame); +static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, bool shadowPass, bool twoSided, const bool *skip, const FragmentUniformsT *fragmentUniforms, int32_t sampleSet); static void _drawParticles(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame, const FragmentUniformsT *fragmentUniforms); static void _drawPost(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame); static void _drawSky(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame); -static Vec3T _eyeOf(int32_t camera); static Vec3T _faceDirection(int32_t face, float s, float t); static void _fillInstances(int32_t drawCount, const CameraFrameT *frame); static void _fillLights(FragmentUniformsT *uniforms); static void _fillSkin(const NodeT *node, SkinUniformsT *uniforms); -static void _fitShadows(int32_t drawCount); +static void _fitShadows(int32_t drawCount, const CameraFrameT *camera); static void _freeFeed(FeedT *feed); -static void _freeSpriteNode(int32_t node); -static void _freeView(ViewT *view); static void _freeMaterialTexture(MaterialT *material); static void _freeMorphs(MeshT *mesh); static void _freeMorphWeights(NodeT *node); static void _freeSkin(NodeT *node); +static void _freeSpriteNode(int32_t node); +static void _freeView(ViewT *view); static void _gatherParticles(Vec3T eye, Vec3T forward); +static int32_t _gridMesh(const float *heights, int32_t columns, int32_t rows, float sizeX, float sizeY, float sizeZ, bool firstRowFar); static uint16_t _half(float value); static bool _hasMorphs(const NodeT *node, const MeshT *mesh); static SDL_GPUTextureFormat _hdrFormat(void); +static bool _isSkinned(const NodeT *node, const MeshT *mesh); static void _lathe(SceneVertexT **vertices, int32_t *vertexCount, uint32_t **indices, int32_t *indexCount, float bottomRadius, float topRadius, float height, int32_t segments); static float _linear(uint8_t value); static float _linearF(float value); -static int32_t _lookupPipeline(int32_t node); +static bool _mapIsColour(MaterialMapE map); static void _matchMorphWeights(NodeT *node); +static void _materialDefaults(MaterialT *material); +static void _materialPlace(MaterialT *material, MaterialMapE map, SDL_GPUTexture *texture, float strength); static SDL_GPUTexture *_materialTexture(const MaterialT *material); static uint32_t _mipLevels(int32_t width, int32_t height); static ParticleTexturesT *_particleTextures(const EmitterViewT *view); +static SDL_GPUTextureFormat _pickDepthFormat(const SDL_GPUTextureFormat *wanted, int32_t count, SDL_GPUTextureUsageFlags usage); +static int32_t _pipelineVariant(int32_t node); static Mat4T _projectionFor(int32_t width, int32_t height); -static void _releaseParticleTextures(bool all); -static void _releaseMaterialBase(MaterialT *material); +static int32_t _quadMesh(float width, float height, Vec3T down, Vec3T normal); static void _recordTexture(SDL_GPUTexture *texture, size_t bytes); +static void _releaseMaterialBase(MaterialT *material); +static void _releaseParticleTextures(bool all); +static void _releasePipeline(SDL_GPUGraphicsPipeline **pipeline); static void _releaseTexture(SDL_GPUTexture **texture); static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame, FragmentUniformsT *uniforms, int32_t drawCount); static void _ribbon(const EmitterViewT *view, int32_t index, Vec3T eye); static bool _sameBatch(int32_t a, int32_t b, bool shadowPass, const bool *skip); +static SDL_GPUSampleCount _sampleCountOf(int32_t sampleSet); static Vec3T _sampleEquirect(const float *rgb, int32_t width, int32_t height, Vec3T direction); -static bool _setMap(SDL_GPUTexture **slot, SDL_Surface *image, bool srgb); -static SDL_GPUTexture *_solidTexture(uint8_t r, uint8_t g, uint8_t b); -static SDL_GPUTexture *_uploadCompressed(const Ktx2ImageT *image, bool srgb); -static SDL_GPUTexture *_uploadCube(const uint16_t *pixels, int32_t face); -static void _uploadInstances(SDL_GPUCommandBuffer *commands, int32_t drawCount); static SDL_GPUTextureFormat _shadowFormat(void); +static bool *_skipScratch(int32_t drawCount); +static SDL_GPUTexture *_solidTexture(uint8_t r, uint8_t g, uint8_t b); +static bool _stageBegin(StagingT *staging, uint32_t bytes); +static bool _stageCopy(StagingT *staging); +static void _stageEnd(StagingT *staging, SDL_GPUTexture *mipmaps); +static void _stageTexture(const StagingT *staging, uint32_t offset, SDL_GPUTexture *texture, uint32_t level, uint32_t layer, uint32_t width, uint32_t height); static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible); static SDL_GPUBuffer *_uploadBuffer(SDL_GPUBufferUsageFlags usage, const void *data, uint32_t size); +static SDL_GPUTexture *_uploadCompressed(const Ktx2ImageT *image, bool srgb); +static SDL_GPUTexture *_uploadCube(const uint16_t *pixels, int32_t face); static bool _uploadDynamic(SDL_GPUCommandBuffer *commands, SDL_GPUBufferUsageFlags usage, SDL_GPUBuffer **buffer, SDL_GPUTransferBuffer **transfer, uint32_t *capacity, const void *data, uint32_t bytes, const char *what); +static void _uploadInstances(SDL_GPUCommandBuffer *commands, int32_t drawCount); static void _uploadLines(SDL_GPUCommandBuffer *commands); static void _uploadParticles(SDL_GPUCommandBuffer *commands); static SDL_GPUTexture *_uploadTexture(SDL_Surface *image, bool srgb); -static SceneVertexT _vertex(float x, float y, float z, float nx, float ny, float nz, float u, float v); +static SceneVertexT _vertex(float x, float y, float z, float nx, float ny, float nz, float u, float v); static Mat4T _viewOf(int32_t camera); @@ -667,7 +716,7 @@ static int32_t _addMesh(const SceneVertexT *vertices, int32_t vertexCount, const mesh->indexBuffer = _uploadBuffer(SDL_GPU_BUFFERUSAGE_INDEX, indices, (uint32_t)(sizeof(uint32_t) * (size_t)indexCount)); if ((mesh->vertexBuffer == NULL) || (mesh->indexBuffer == NULL)) { mesh->used = true; - meshDelete(x); + meshDelete((int32_t)(mesh - _scene.meshes)); return NO_HANDLE; } mesh->indexCount = (uint32_t)indexCount; @@ -751,17 +800,8 @@ static int32_t _allocMaterial(void) { _scene.materialCount++; } material = &_scene.materials[x]; - memset(material, 0, sizeof(*material)); - material->baseColor.x = 1.0f; - material->baseColor.y = 1.0f; - material->baseColor.z = 1.0f; - material->baseColor.w = 1.0f; - material->roughness = 0.5f; - material->tilingU = 1.0f; - material->tilingV = 1.0f; - material->feed = NO_HANDLE; - material->view = NO_HANDLE; - material->used = true; + _materialDefaults(material); + material->used = true; return x; } @@ -782,28 +822,41 @@ static int32_t _allocNode(void) { if (_scene.nodes == NULL) { utilDie("Out of memory allocating a scene node."); } + memset(&_scene.nodes[x], 0, sizeof(NodeT)); _scene.nodeCount++; } - node = &_scene.nodes[x]; + node = &_scene.nodes[x]; generation = node->generation + 1; memset(node, 0, sizeof(*node)); - node->generation = generation; - node->parent = NO_HANDLE; - node->spriteSlot = NO_HANDLE; - node->firstChild = NO_HANDLE; - node->nextSibling = NO_HANDLE; - node->rotation = quatIdentity(); - node->scale = vec3(1.0f, 1.0f, 1.0f); - node->world = mat4Identity(); - node->mesh = NO_HANDLE; - node->material = NO_HANDLE; - node->visible = true; + node->generation = generation; + node->parent = NO_HANDLE; + node->spriteSlot = NO_HANDLE; + node->firstChild = NO_HANDLE; + node->nextSibling = NO_HANDLE; + node->rotation = quatIdentity(); + node->scale = vec3(1.0f, 1.0f, 1.0f); + node->world = mat4Identity(); + node->mesh = NO_HANDLE; + node->material = NO_HANDLE; + node->visible = true; node->shadowCaster = true; - node->used = true; + node->used = true; return x; } +// Source-alpha blending over what is there (or, additive, added to it), alpha kept as coverage. +static void _alphaBlendState(SDL_GPUColorTargetDescription *colour, bool additive) { + colour->blend_state.enable_blend = true; + colour->blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; + colour->blend_state.dst_color_blendfactor = additive ? SDL_GPU_BLENDFACTOR_ONE : SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + colour->blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; + colour->blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; + colour->blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + colour->blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; +} + + // A world bounding sphere per draw, for culling and for fitting shadows. static void _boundDraws(int32_t drawCount) { int32_t x; @@ -828,29 +881,29 @@ static void _boundDraws(int32_t drawCount) { } } _scene.draws[x].centre = vec3Scale(vec3Add(drawMin, drawMax), 0.5f); - _scene.draws[x].radius = vec3Length(vec3Subtract(drawMax, _scene.draws[x].centre)) + 0.001f; + _scene.draws[x].radius = vec3Length(vec3Subtract(drawMax, _scene.draws[x].centre)) + BOUNDS_PAD; } } -// A camera's view, projection and axes for a target of the given size. +// A camera's view, projection and axes for a target of the given size (single sample until the +// caller says otherwise). The axes and eye are the columns of the camera's own world matrix, so +// only the view itself costs an inversion. static void _cameraFrame(int32_t camera, int32_t width, int32_t height, CameraFrameT *frame) { - Mat4T inverse; + const float *m; memset(frame, 0, sizeof(*frame)); frame->view = _viewOf(camera); - frame->eye = _eyeOf(camera); + frame->world = nodeValid(camera) ? _scene.nodes[camera].world : mat4Compose(vec3(0.0f, 0.0f, DEFAULT_EYE_Z), quatIdentity(), vec3(1.0f, 1.0f, 1.0f)); frame->viewProjection = mat4Multiply(_projectionFor(width, height), frame->view); frame->width = width; frame->height = height; - frame->right = vec3(1.0f, 0.0f, 0.0f); - frame->up = vec3(0.0f, 1.0f, 0.0f); - frame->forward = vec3(0.0f, 0.0f, -1.0f); - if (mat4Invert(frame->view, &inverse)) { - frame->right = vec3Normalize(mat4TransformVector(inverse, vec3(1.0f, 0.0f, 0.0f))); - frame->up = vec3Normalize(mat4TransformVector(inverse, vec3(0.0f, 1.0f, 0.0f))); - frame->forward = vec3Normalize(mat4TransformVector(inverse, vec3(0.0f, 0.0f, -1.0f))); - } + frame->sampleSet = SAMPLE_SET_SINGLE; + m = frame->world.m; + frame->right = vec3Normalize(vec3(m[0], m[1], m[2])); + frame->up = vec3Normalize(vec3(m[4], m[5], m[6])); + frame->forward = vec3Normalize(vec3(-m[8], -m[9], -m[10])); + frame->eye = vec3(m[12], m[13], m[14]); } @@ -872,33 +925,27 @@ static void _attach(int32_t node, int32_t parent) { } -// Blended draws go back to front; opaque ones keep their order. // Far to near. -static int32_t _compareDepthOrder(const void *a, const void *b) { - const DepthOrderT *x = a; - const DepthOrderT *y = b; - - if (x->depth > y->depth) { +static int32_t _compareDepth(float a, float b) { + if (a > b) { return -1; } - if (x->depth < y->depth) { + if (a < b) { return 1; } return 0; } -static int32_t _compareDraws(const void *a, const void *b) { - const DrawT *da = a; - const DrawT *db = b; +// Particles and emitters: far to near. +static int32_t _compareDepthOrder(const void *a, const void *b) { + return _compareDepth(((const DepthOrderT *)a)->depth, ((const DepthOrderT *)b)->depth); +} - if (da->depth > db->depth) { - return -1; - } - if (da->depth < db->depth) { - return 1; - } - return 0; + +// Blended draws go back to front; opaque ones keep their order. +static int32_t _compareDraws(const void *a, const void *b) { + return _compareDepth(((const DrawT *)a)->depth, ((const DrawT *)b)->depth); } @@ -917,9 +964,8 @@ static int32_t _compareOpaque(const void *a, const void *b) { } -// Billboard pipeline for one blend: camera-facing quads, depth tested, never written, two-sided. // Debug lines: unlit, alpha blended, depth tested against the scene but never writing it. -static bool _createLinePipeline(void) { +static bool _createLinePipeline(int32_t sampleSet) { SDL_GPUGraphicsPipelineCreateInfo info; SDL_GPUVertexBufferDescription buffers[1]; SDL_GPUVertexAttribute attributes[2]; @@ -938,14 +984,8 @@ static bool _createLinePipeline(void) { attributes[1].location = 1; attributes[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; attributes[1].offset = offsetof(LineVertexT, colour); - colour.format = _scene.hdrFormat; - colour.blend_state.enable_blend = true; - colour.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; - colour.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - colour.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; - colour.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; - colour.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - colour.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + colour.format = _scene.hdrFormat; + _alphaBlendState(&colour, false); info.vertex_shader = _scene.lineVertex; info.fragment_shader = _scene.lineFragment; info.vertex_input_state.vertex_buffer_descriptions = buffers; @@ -956,7 +996,7 @@ static bool _createLinePipeline(void) { info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; - info.multisample_state.sample_count = _scene.sampleCount; + info.multisample_state.sample_count = _sampleCountOf(sampleSet); info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL; info.depth_stencil_state.enable_depth_test = true; info.depth_stencil_state.enable_depth_write = false; @@ -964,8 +1004,8 @@ static bool _createLinePipeline(void) { info.target_info.num_color_targets = 1; info.target_info.depth_stencil_format = _scene.depthFormat; info.target_info.has_depth_stencil_target = true; - _scene.linePipeline = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); - if (_scene.linePipeline == NULL) { + _scene.linePipeline[sampleSet] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); + if (_scene.linePipeline[sampleSet] == NULL) { utilTrace("Scene: line pipeline: %s", SDL_GetError()); return false; } @@ -973,7 +1013,8 @@ static bool _createLinePipeline(void) { } -static bool _createParticlePipeline(int32_t blend) { +// Billboard pipeline for one blend: camera-facing quads, depth tested, never written, two-sided. +static bool _createParticlePipeline(int32_t sampleSet, int32_t blend) { SDL_GPUGraphicsPipelineCreateInfo info; SDL_GPUVertexBufferDescription buffers[1]; SDL_GPUVertexAttribute attributes[5]; @@ -994,21 +1035,15 @@ static bool _createParticlePipeline(int32_t blend) { attributes[1].offset = offsetof(ParticleVertexT, corner); attributes[2].location = 2; attributes[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; - attributes[2].offset = offsetof(ParticleVertexT, size); + attributes[2].offset = offsetof(ParticleVertexT, sizeAngle); attributes[3].location = 3; attributes[3].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; attributes[3].offset = offsetof(ParticleVertexT, colour); attributes[4].location = 4; attributes[4].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; attributes[4].offset = offsetof(ParticleVertexT, uv); - colour.format = _scene.hdrFormat; - colour.blend_state.enable_blend = true; - colour.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; - colour.blend_state.dst_color_blendfactor = (blend == PARTICLE_ADD) ? SDL_GPU_BLENDFACTOR_ONE : SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - colour.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; - colour.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; - colour.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - colour.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + colour.format = _scene.hdrFormat; + _alphaBlendState(&colour, blend == PARTICLE_ADD); info.vertex_shader = _scene.particleVertex; info.fragment_shader = _scene.particleFragment; info.vertex_input_state.vertex_buffer_descriptions = buffers; @@ -1019,7 +1054,7 @@ static bool _createParticlePipeline(int32_t blend) { info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; - info.multisample_state.sample_count = _scene.sampleCount; + info.multisample_state.sample_count = _sampleCountOf(sampleSet); info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL; info.depth_stencil_state.enable_depth_test = true; info.depth_stencil_state.enable_depth_write = false; @@ -1027,8 +1062,8 @@ static bool _createParticlePipeline(int32_t blend) { info.target_info.num_color_targets = 1; info.target_info.depth_stencil_format = _scene.depthFormat; info.target_info.has_depth_stencil_target = true; - _scene.particlePipelines[blend] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); - if (_scene.particlePipelines[blend] == NULL) { + _scene.particlePipelines[sampleSet][blend] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); + if (_scene.particlePipelines[sampleSet][blend] == NULL) { utilTrace("Scene: particle pipeline %d: %s", blend, SDL_GetError()); return false; } @@ -1036,67 +1071,40 @@ static bool _createParticlePipeline(int32_t blend) { } -static bool _createPipeline(int32_t variant) { +// The main pass pipeline for one PIPELINE_* variant, for targets of the sample set's count. +static bool _createPipeline(int32_t sampleSet, int32_t variant) { SDL_GPUGraphicsPipelineCreateInfo info; - SDL_GPUVertexBufferDescription buffers[1]; - SDL_GPUVertexAttribute attributes[6]; + SDL_GPUVertexBufferDescription buffer; + SDL_GPUVertexAttribute attributes[MESH_ATTRIBUTES]; SDL_GPUColorTargetDescription colour; memset(&info, 0, sizeof(info)); - memset(buffers, 0, sizeof(buffers)); - memset(attributes, 0, sizeof(attributes)); memset(&colour, 0, sizeof(colour)); - buffers[0].slot = 0; - buffers[0].pitch = sizeof(SceneVertexT); - buffers[0].input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; - attributes[0].location = 0; - attributes[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3; - attributes[0].offset = offsetof(SceneVertexT, position); - attributes[1].location = 1; - attributes[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3; - attributes[1].offset = offsetof(SceneVertexT, normal); - attributes[2].location = 2; - attributes[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; - attributes[2].offset = offsetof(SceneVertexT, uv); - attributes[3].location = 3; - attributes[3].format = SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4; - attributes[3].offset = offsetof(SceneVertexT, joints); - attributes[4].location = 4; - attributes[4].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; - attributes[4].offset = offsetof(SceneVertexT, weights); - attributes[5].location = 5; - attributes[5].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; - attributes[5].offset = offsetof(SceneVertexT, tangent); - colour.format = _scene.hdrFormat; + _describeMeshVertex(&buffer, attributes); + colour.format = _scene.hdrFormat; if (variant & PIPELINE_BLEND) { - colour.blend_state.enable_blend = true; - colour.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; - colour.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - colour.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; - colour.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE; - colour.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - colour.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + _alphaBlendState(&colour, false); } info.vertex_shader = (variant & PIPELINE_SKINNED) ? _scene.vertexSkinned : _scene.vertexStatic; info.fragment_shader = _scene.fragment; - info.vertex_input_state.vertex_buffer_descriptions = buffers; + info.vertex_input_state.vertex_buffer_descriptions = &buffer; info.vertex_input_state.num_vertex_buffers = 1; info.vertex_input_state.vertex_attributes = attributes; - info.vertex_input_state.num_vertex_attributes = 6; + info.vertex_input_state.num_vertex_attributes = MESH_ATTRIBUTES; info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; info.rasterizer_state.cull_mode = (variant & PIPELINE_TWO_SIDED) ? SDL_GPU_CULLMODE_NONE : SDL_GPU_CULLMODE_BACK; info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; - info.multisample_state.sample_count = _scene.sampleCount; + info.multisample_state.sample_count = _sampleCountOf(sampleSet); info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL; info.depth_stencil_state.enable_depth_test = true; - info.depth_stencil_state.enable_depth_write = (variant & PIPELINE_BLEND) ? false : true; + info.depth_stencil_state.enable_depth_write = !(variant & PIPELINE_BLEND); info.target_info.color_target_descriptions = &colour; info.target_info.num_color_targets = 1; info.target_info.depth_stencil_format = _scene.depthFormat; info.target_info.has_depth_stencil_target = true; - _scene.pipelines[variant] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); - if (_scene.pipelines[variant] == NULL) { + _scene.pipelines[sampleSet][variant] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); + if (_scene.pipelines[sampleSet][variant] == NULL) { utilTrace("Scene: pipeline %d: %s", variant, SDL_GetError()); return false; } @@ -1104,7 +1112,6 @@ static bool _createPipeline(int32_t variant) { } -// Picks the blob for the format the device accepts. // The bloom pipelines: the post vertex shader with the downsample and upsample fragments, into // 16-bit float levels. static bool _createBloomPipelines(void) { @@ -1124,12 +1131,18 @@ static bool _createBloomPipelines(void) { info.target_info.color_target_descriptions = &colour; info.target_info.num_color_targets = 1; _scene.bloomDownPipeline = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); - info.fragment_shader = _scene.bloomUpFragment; - _scene.bloomUpPipeline = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); - if ((_scene.bloomDownPipeline == NULL) || (_scene.bloomUpPipeline == NULL)) { + if (_scene.bloomDownPipeline == NULL) { utilTrace("Scene: bloom pipeline: %s", SDL_GetError()); return false; } + info.fragment_shader = _scene.bloomUpFragment; + _scene.bloomUpPipeline = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); + if (_scene.bloomUpPipeline == NULL) { + utilTrace("Scene: bloom pipeline: %s", SDL_GetError()); + SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.bloomDownPipeline); + _scene.bloomDownPipeline = NULL; + return false; + } return true; } @@ -1205,7 +1218,7 @@ static bool _createPostPipeline(void) { // The sky pipeline: the post pass's screen triangle with the sky fragment shader, drawn first // into the main pass under everything (no depth test or write), so it must match the pass's // multisampling and depth format. -static bool _createSkyPipeline(void) { +static bool _createSkyPipeline(int32_t sampleSet) { SDL_GPUGraphicsPipelineCreateInfo info; SDL_GPUColorTargetDescription colour; @@ -1218,14 +1231,14 @@ static bool _createSkyPipeline(void) { info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE; info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; - info.multisample_state.sample_count = _scene.sampleCount; + info.multisample_state.sample_count = _sampleCountOf(sampleSet); info.depth_stencil_state.enable_depth_test = false; info.target_info.color_target_descriptions = &colour; info.target_info.num_color_targets = 1; info.target_info.depth_stencil_format = _scene.depthFormat; info.target_info.has_depth_stencil_target = true; - _scene.skyPipeline = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); - if (_scene.skyPipeline == NULL) { + _scene.skyPipeline[sampleSet] = SDL_CreateGPUGraphicsPipeline(_scene.device, &info); + if (_scene.skyPipeline[sampleSet] == NULL) { utilTrace("Scene: sky pipeline: %s", SDL_GetError()); return false; } @@ -1273,6 +1286,7 @@ static void _computeSh(const float *rgb, int32_t width, int32_t height) { } +// Picks the blob for the format the device accepts. static SDL_GPUShader *_createShader(const SceneShaderT *shader, SDL_GPUShaderStage stage, uint32_t samplers, uint32_t uniforms, uint32_t storageBuffers) { SDL_GPUShaderCreateInfo info; SDL_GPUShaderFormat formats = SDL_GetGPUShaderFormats(_scene.device); @@ -1311,7 +1325,7 @@ static SDL_GPUShader *_createShader(const SceneShaderT *shader, SDL_GPUShaderSta static bool _createShaders(void) { _scene.vertexStatic = _createShader(&sceneShaderVertexStatic, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1, 2); _scene.vertexSkinned = _createShader(&sceneShaderVertexSkinned, SDL_GPU_SHADERSTAGE_VERTEX, 0, 2, 2); - _scene.fragment = _createShader(&sceneShaderFragmentMain, SDL_GPU_SHADERSTAGE_FRAGMENT, MATERIAL_SAMPLERS, 1, 0); + _scene.fragment = _createShader(&sceneShaderFragmentMain, SDL_GPU_SHADERSTAGE_FRAGMENT, MATERIAL_SAMPLERS, 2, 0); _scene.depthFragment = _createShader(&sceneShaderDepthMain, SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 0, 0); _scene.particleVertex = _createShader(&sceneShaderParticleVertex, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1, 0); _scene.particleFragment = _createShader(&sceneShaderParticleFragment, SDL_GPU_SHADERSTAGE_FRAGMENT, 2, 2, 0); @@ -1367,46 +1381,24 @@ static bool _createShadowMaps(int32_t layers) { // shader, no colour target, and a depth bias against self-shadowing. static bool _createShadowPipeline(int32_t variant) { SDL_GPUGraphicsPipelineCreateInfo info; - SDL_GPUVertexBufferDescription buffers[1]; - SDL_GPUVertexAttribute attributes[6]; + SDL_GPUVertexBufferDescription buffer; + SDL_GPUVertexAttribute attributes[MESH_ATTRIBUTES]; memset(&info, 0, sizeof(info)); - memset(buffers, 0, sizeof(buffers)); - memset(attributes, 0, sizeof(attributes)); - buffers[0].slot = 0; - buffers[0].pitch = sizeof(SceneVertexT); - buffers[0].input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; - attributes[0].location = 0; - attributes[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3; - attributes[0].offset = offsetof(SceneVertexT, position); - attributes[1].location = 1; - attributes[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3; - attributes[1].offset = offsetof(SceneVertexT, normal); - attributes[2].location = 2; - attributes[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; - attributes[2].offset = offsetof(SceneVertexT, uv); - attributes[3].location = 3; - attributes[3].format = SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4; - attributes[3].offset = offsetof(SceneVertexT, joints); - attributes[4].location = 4; - attributes[4].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; - attributes[4].offset = offsetof(SceneVertexT, weights); - attributes[5].location = 5; - attributes[5].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; - attributes[5].offset = offsetof(SceneVertexT, tangent); - info.vertex_shader = (variant & PIPELINE_SKINNED) ? _scene.vertexSkinned : _scene.vertexStatic; + _describeMeshVertex(&buffer, attributes); + info.vertex_shader = (variant & SHADOW_PIPELINE_SKINNED) ? _scene.vertexSkinned : _scene.vertexStatic; info.fragment_shader = _scene.depthFragment; - info.vertex_input_state.vertex_buffer_descriptions = buffers; + info.vertex_input_state.vertex_buffer_descriptions = &buffer; info.vertex_input_state.num_vertex_buffers = 1; info.vertex_input_state.vertex_attributes = attributes; - info.vertex_input_state.num_vertex_attributes = 6; + info.vertex_input_state.num_vertex_attributes = MESH_ATTRIBUTES; info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL; - info.rasterizer_state.cull_mode = (variant & PIPELINE_TWO_SIDED) ? SDL_GPU_CULLMODE_NONE : SDL_GPU_CULLMODE_BACK; + info.rasterizer_state.cull_mode = (variant & SHADOW_PIPELINE_TWO_SIDED) ? SDL_GPU_CULLMODE_NONE : SDL_GPU_CULLMODE_BACK; info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; info.rasterizer_state.enable_depth_bias = true; - info.rasterizer_state.depth_bias_constant_factor = 2.0f; - info.rasterizer_state.depth_bias_slope_factor = 2.0f; + info.rasterizer_state.depth_bias_constant_factor = SHADOW_DEPTH_BIAS_CONSTANT; + info.rasterizer_state.depth_bias_slope_factor = SHADOW_DEPTH_BIAS_SLOPE; info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1; info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL; info.depth_stencil_state.enable_depth_test = true; @@ -1423,22 +1415,42 @@ static bool _createShadowPipeline(int32_t variant) { } -// Picks the best depth format the device offers. +// The best depth format the device offers for the camera's depth target. static SDL_GPUTextureFormat _depthFormat(void) { - SDL_GPUTextureFormat wanted[] = { SDL_GPU_TEXTUREFORMAT_D32_FLOAT, SDL_GPU_TEXTUREFORMAT_D24_UNORM, SDL_GPU_TEXTUREFORMAT_D16_UNORM }; - int32_t x; + static const SDL_GPUTextureFormat wanted[] = { SDL_GPU_TEXTUREFORMAT_D32_FLOAT, SDL_GPU_TEXTUREFORMAT_D24_UNORM, SDL_GPU_TEXTUREFORMAT_D16_UNORM }; - for (x = 0; x < (int32_t)SDL_arraysize(wanted); x++) { - if (SDL_GPUTextureSupportsFormat(_scene.device, wanted[x], SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET)) { - return wanted[x]; - } - } - return SDL_GPU_TEXTUREFORMAT_D16_UNORM; + return _pickDepthFormat(wanted, (int32_t)SDL_arraysize(wanted), SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET); +} + + +// The vertex buffer and attribute layout of a SceneVertexT, for the pipelines that draw meshes. +static void _describeMeshVertex(SDL_GPUVertexBufferDescription *buffer, SDL_GPUVertexAttribute *attributes) { + memset(buffer, 0, sizeof(*buffer)); + memset(attributes, 0, sizeof(*attributes) * MESH_ATTRIBUTES); + buffer->slot = 0; + buffer->pitch = sizeof(SceneVertexT); + buffer->input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; + attributes[0].location = 0; + attributes[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3; + attributes[0].offset = offsetof(SceneVertexT, position); + attributes[1].location = 1; + attributes[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3; + attributes[1].offset = offsetof(SceneVertexT, normal); + attributes[2].location = 2; + attributes[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; + attributes[2].offset = offsetof(SceneVertexT, uv); + attributes[3].location = 3; + attributes[3].format = SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4; + attributes[3].offset = offsetof(SceneVertexT, joints); + attributes[4].location = 4; + attributes[4].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; + attributes[4].offset = offsetof(SceneVertexT, weights); + attributes[5].location = 5; + attributes[5].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; + attributes[5].offset = offsetof(SceneVertexT, tangent); } -// Pipelines bake in the sample count, so a change in antialiasing drops them; they come back on -// first use. static void _destroyBloomTargets(void) { int32_t level; @@ -1452,24 +1464,25 @@ static void _destroyBloomTargets(void) { } +// Pipelines bake in the sample count, so a change in antialiasing drops every one that draws into +// the camera targets (meshes, sky, particles, lines) along with the shadow pipelines; they come +// back on first use. static void _destroyPipelines(void) { + int32_t set; int32_t x; - for (x = 0; x < PIPELINE_COUNT; x++) { - if (_scene.pipelines[x] != NULL) { - SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.pipelines[x]); - _scene.pipelines[x] = NULL; + for (set = 0; set < SAMPLE_SETS; set++) { + for (x = 0; x < PIPELINE_COUNT; x++) { + _releasePipeline(&_scene.pipelines[set][x]); } + for (x = 0; x < PARTICLE_PIPELINES; x++) { + _releasePipeline(&_scene.particlePipelines[set][x]); + } + _releasePipeline(&_scene.skyPipeline[set]); + _releasePipeline(&_scene.linePipeline[set]); } for (x = 0; x < SHADOW_PIPELINES; x++) { - if (_scene.shadowPipelines[x] != NULL) { - SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.shadowPipelines[x]); - _scene.shadowPipelines[x] = NULL; - } - } - if (_scene.skyPipeline != NULL) { - SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.skyPipeline); - _scene.skyPipeline = NULL; + _releasePipeline(&_scene.shadowPipelines[x]); } } @@ -1523,7 +1536,7 @@ static void _drawBloom(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame int32_t w; int32_t h; - if (!_createBloomTargets(frame->width, frame->height)) { + if (!_createBloomTargets(frame->width, frame->height) || (_scene.bloomLevels < BLOOM_LEVELS_MIN)) { return; } if (((_scene.bloomDownPipeline == NULL) || (_scene.bloomUpPipeline == NULL)) && !_createBloomPipelines()) { @@ -1578,17 +1591,20 @@ static void _drawBloom(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame // Issues the collected draws into a pass: the shadow pass with the light's view-projection and // depth-only pipelines (blended meshes cast nothing), or the main pass with the camera's and the -// full material. -static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, bool shadowPass, bool twoSided, const bool *skip, FragmentUniformsT *fragmentUniforms) { +// full material, from the pipelines of the target's sample set. The frame's fragment uniforms go +// up once per pipeline, the material's per batch. +static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, int32_t drawCount, const Mat4T *viewProjection, bool shadowPass, bool twoSided, const bool *skip, const FragmentUniformsT *fragmentUniforms, int32_t sampleSet) { SDL_GPUBufferBinding binding; SDL_GPUTextureSamplerBinding samplerBindings[MATERIAL_SAMPLERS]; - SDL_GPUSampler *materialSampler = NULL; + SDL_GPUSampler *materialSampler; + SDL_GPUTexture *baseTexture; DrawUniformsT drawUniforms; - SkinUniformsT *skinUniforms = NULL; + MaterialUniformsT materialUniforms; SDL_GPUBuffer *storage[2]; int32_t x; int32_t end; int32_t lastPipeline = NO_HANDLE; + int32_t lastMesh = NO_HANDLE; int32_t variant; NodeT *node; MeshT *mesh; @@ -1596,50 +1612,45 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i MaterialT defaultMaterial; SDL_GPUGraphicsPipeline *pipeline; - memset(&defaultMaterial, 0, sizeof(defaultMaterial)); - defaultMaterial.baseColor.x = 1.0f; - defaultMaterial.baseColor.y = 1.0f; - defaultMaterial.baseColor.z = 1.0f; - defaultMaterial.baseColor.w = 1.0f; - defaultMaterial.roughness = 0.5f; - defaultMaterial.tilingU = 1.0f; - defaultMaterial.tilingV = 1.0f; - defaultMaterial.feed = NO_HANDLE; + _materialDefaults(&defaultMaterial); for (x = 0; x < drawCount; x = end) { end = x + 1; node = &_scene.nodes[_scene.draws[x].node]; mesh = &_scene.meshes[node->mesh]; material = (node->material != NO_HANDLE) ? &_scene.materials[node->material] : &defaultMaterial; - variant = _lookupPipeline(_scene.draws[x].node); - if ((variant == NO_HANDLE) || ((skip != NULL) && skip[x])) { + variant = _pipelineVariant(_scene.draws[x].node); + if ((skip != NULL) && skip[x]) { continue; } if (shadowPass) { if (material->blend || (!node->shadowCaster && !_scene.depthPrepass)) { continue; } - if (twoSided) { - // A bulb inside a closed mesh sees only its back faces; they must still cast. - variant |= PIPELINE_TWO_SIDED; - } - variant &= PIPELINE_SKINNED | PIPELINE_TWO_SIDED; - variant = (variant & PIPELINE_SKINNED ? 1 : 0) | (variant & PIPELINE_TWO_SIDED ? 2 : 0); + // A bulb inside a closed mesh sees only its back faces; they must still cast. + variant = ((variant & PIPELINE_SKINNED) ? SHADOW_PIPELINE_SKINNED : 0) | ((twoSided || (variant & PIPELINE_TWO_SIDED)) ? SHADOW_PIPELINE_TWO_SIDED : 0); if ((_scene.shadowPipelines[variant] == NULL) && !_createShadowPipeline(variant)) { continue; } pipeline = _scene.shadowPipelines[variant]; } else { - pipeline = _scene.pipelines[variant]; + if ((_scene.pipelines[sampleSet][variant] == NULL) && !_createPipeline(sampleSet, variant)) { + continue; + } + pipeline = _scene.pipelines[sampleSet][variant]; } if (variant != lastPipeline) { SDL_BindGPUGraphicsPipeline(pass, pipeline); + if (!shadowPass) { + SDL_PushGPUFragmentUniformData(commands, FRAME_UNIFORMS, fragmentUniforms, sizeof(FragmentUniformsT)); + } lastPipeline = variant; + lastMesh = NO_HANDLE; } memset(&drawUniforms, 0, sizeof(drawUniforms)); drawUniforms.viewProjection = *viewProjection; drawUniforms.morphInfo[2] = x; // Copies of the same thing after this one ride along as instances. - if (!(mesh->skinned && (node->skinCount > 0)) && !_hasMorphs(node, mesh)) { + if (!_isSkinned(node, mesh) && !_hasMorphs(node, mesh)) { while ((end < drawCount) && _sameBatch(x, end, shadowPass, skip)) { end++; } @@ -1676,42 +1687,45 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i drawUniforms.morphInfo[0] = active; drawUniforms.morphInfo[1] = mesh->vertexCount; } - SDL_PushGPUVertexUniformData(commands, 0, &drawUniforms, sizeof(drawUniforms)); - storage[0] = (mesh->morphBuffer != NULL) ? mesh->morphBuffer : _scene.noMorphs; - storage[1] = _scene.instanceBuffer; - SDL_BindGPUVertexStorageBuffers(pass, 0, storage, 2); - if (mesh->skinned && (node->skinCount > 0)) { - // 8 KB per skinned draw; allocated once per pass that needs it. - if (skinUniforms == NULL) { - skinUniforms = SDL_malloc(sizeof(SkinUniformsT)); - if (skinUniforms == NULL) { - utilDie("Out of memory posing a skin."); - } - } - _fillSkin(node, skinUniforms); - SDL_PushGPUVertexUniformData(commands, 1, skinUniforms, sizeof(SkinUniformsT)); + SDL_PushGPUVertexUniformData(commands, DRAW_UNIFORMS, &drawUniforms, sizeof(drawUniforms)); + // Opaque draws are sorted by mesh, so runs of one mesh keep their buffers bound. + if (node->mesh != lastMesh) { + storage[0] = (mesh->morphBuffer != NULL) ? mesh->morphBuffer : _scene.noMorphs; + storage[1] = _scene.instanceBuffer; + SDL_BindGPUVertexStorageBuffers(pass, 0, storage, 2); + memset(&binding, 0, sizeof(binding)); + binding.buffer = mesh->vertexBuffer; + SDL_BindGPUVertexBuffers(pass, 0, &binding, 1); + binding.buffer = mesh->indexBuffer; + SDL_BindGPUIndexBuffer(pass, &binding, SDL_GPU_INDEXELEMENTSIZE_32BIT); + lastMesh = node->mesh; + } + if (_scene.draws[x].skin != NO_HANDLE) { + SDL_PushGPUVertexUniformData(commands, SKIN_UNIFORMS, &_scene.skins[_scene.draws[x].skin], sizeof(SkinUniformsT)); } if (!shadowPass) { - fragmentUniforms->baseColor[0] = material->baseColor.x; - fragmentUniforms->baseColor[1] = material->baseColor.y; - fragmentUniforms->baseColor[2] = material->baseColor.z; - fragmentUniforms->baseColor[3] = material->baseColor.w; - fragmentUniforms->emissive[0] = material->emissive.x; - fragmentUniforms->emissive[1] = material->emissive.y; - fragmentUniforms->emissive[2] = material->emissive.z; - fragmentUniforms->emissive[3] = 1.0f; - fragmentUniforms->material[0] = material->metallic; - fragmentUniforms->material[1] = material->roughness; - fragmentUniforms->material[2] = material->unlit ? 1.0f : 0.0f; - fragmentUniforms->material[3] = (_materialTexture(material) == NULL) ? TEXTURE_NONE : (((material->feed != NO_HANDLE) || (material->view != NO_HANDLE)) ? TEXTURE_FEED : TEXTURE_SRGB); - fragmentUniforms->maps[0] = (material->normalMap != NULL) ? material->normalStrength : 0.0f; - fragmentUniforms->maps[1] = (material->occlusionMap != NULL) ? material->occlusionStrength : 0.0f; - fragmentUniforms->tiling[0] = material->tilingU; - fragmentUniforms->tiling[1] = material->tilingV; - SDL_PushGPUFragmentUniformData(commands, 0, fragmentUniforms, sizeof(FragmentUniformsT)); + baseTexture = _materialTexture(material); + memset(&materialUniforms, 0, sizeof(materialUniforms)); + materialUniforms.baseColor[0] = material->baseColor.x; + materialUniforms.baseColor[1] = material->baseColor.y; + materialUniforms.baseColor[2] = material->baseColor.z; + materialUniforms.baseColor[3] = material->baseColor.w; + materialUniforms.emissive[0] = material->emissive.x; + materialUniforms.emissive[1] = material->emissive.y; + materialUniforms.emissive[2] = material->emissive.z; + materialUniforms.emissive[3] = 1.0f; + materialUniforms.material[0] = material->metallic; + materialUniforms.material[1] = material->roughness; + materialUniforms.material[2] = material->unlit ? 1.0f : 0.0f; + materialUniforms.material[3] = (float)((baseTexture == NULL) ? TEXTURE_NONE : (((material->feed != NO_HANDLE) || (material->view != NO_HANDLE)) ? TEXTURE_FEED : TEXTURE_SRGB)); + materialUniforms.maps[0] = (material->normalMap != NULL) ? material->normalStrength : 0.0f; + materialUniforms.maps[1] = (material->occlusionMap != NULL) ? material->occlusionStrength : 0.0f; + materialUniforms.tiling[0] = material->tilingU; + materialUniforms.tiling[1] = material->tilingV; + SDL_PushGPUFragmentUniformData(commands, MATERIAL_UNIFORMS, &materialUniforms, sizeof(materialUniforms)); memset(samplerBindings, 0, sizeof(samplerBindings)); materialSampler = (material->filter == FILTER_NEAREST) ? _scene.nearestSampler : _scene.sampler; - samplerBindings[0].texture = (_materialTexture(material) != NULL) ? _materialTexture(material) : _scene.white; + samplerBindings[0].texture = (baseTexture != NULL) ? baseTexture : _scene.white; samplerBindings[0].sampler = materialSampler; samplerBindings[1].texture = (_scene.shadowMaps != NULL) ? _scene.shadowMaps : _scene.shadowMapsNone; samplerBindings[1].sampler = _scene.shadowSampler; @@ -1726,22 +1740,14 @@ static void _drawList(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, i samplerBindings[6].texture = (_scene.skyCube != NULL) ? _scene.skyCube : _scene.blackCube; samplerBindings[6].sampler = _scene.skySampler; SDL_BindGPUFragmentSamplers(pass, 0, samplerBindings, MATERIAL_SAMPLERS); - } - memset(&binding, 0, sizeof(binding)); - binding.buffer = mesh->vertexBuffer; - SDL_BindGPUVertexBuffers(pass, 0, &binding, 1); - binding.buffer = mesh->indexBuffer; - SDL_BindGPUIndexBuffer(pass, &binding, SDL_GPU_INDEXELEMENTSIZE_32BIT); - SDL_DrawGPUIndexedPrimitives(pass, mesh->indexCount, (Uint32)(end - x), 0, 0, 0); - if (!shadowPass) { _scene.statBatches++; } + SDL_DrawGPUIndexedPrimitives(pass, mesh->indexCount, (Uint32)(end - x), 0, 0, 0); } - SDL_free(skinUniforms); } -// Draws this frame's particle runs, after every mesh, with the camera's axes for the billboards. +// Draws this frame's debug lines, after everything else in the pass. static void _drawLines(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame) { SDL_GPUBufferBinding binding; LineUniformsT uniforms; @@ -1749,22 +1755,24 @@ static void _drawLines(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, if ((_scene.lineVertexCount == 0) || (_scene.lineBuffer == NULL)) { return; } - if ((_scene.linePipeline == NULL) && !_createLinePipeline()) { + if ((_scene.linePipeline[frame->sampleSet] == NULL) && !_createLinePipeline(frame->sampleSet)) { return; } uniforms.viewProjection = frame->viewProjection; memset(&binding, 0, sizeof(binding)); binding.buffer = _scene.lineBuffer; - SDL_BindGPUGraphicsPipeline(pass, _scene.linePipeline); + SDL_BindGPUGraphicsPipeline(pass, _scene.linePipeline[frame->sampleSet]); SDL_PushGPUVertexUniformData(commands, 0, &uniforms, sizeof(uniforms)); SDL_BindGPUVertexBuffers(pass, 0, &binding, 1); SDL_DrawGPUPrimitives(pass, (Uint32)_scene.lineVertexCount, 1, 0, 0); } +// Draws this frame's particle runs, after every mesh, with the camera's axes for the billboards. static void _drawParticles(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, const CameraFrameT *frame, const FragmentUniformsT *fragmentUniforms) { SDL_GPUBufferBinding binding; SDL_GPUTextureSamplerBinding samplers[2]; + SDL_GPUGraphicsPipeline **pipelines = _scene.particlePipelines[frame->sampleSet]; ParticleUniformsT uniforms; ParticleParamsT params; int32_t x; @@ -1806,13 +1814,13 @@ static void _drawParticles(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pa for (x = 0; x < _scene.particleRunCount; x++) { ParticleRunT *run = &_scene.particleRuns[x]; - if ((_scene.particlePipelines[run->blend] == NULL) && !_createParticlePipeline(run->blend)) { + if ((pipelines[run->blend] == NULL) && !_createParticlePipeline(frame->sampleSet, run->blend)) { continue; } if ((int32_t)run->blend != lastBlend) { - SDL_BindGPUGraphicsPipeline(pass, _scene.particlePipelines[run->blend]); + SDL_BindGPUGraphicsPipeline(pass, pipelines[run->blend]); SDL_PushGPUVertexUniformData(commands, 0, &uniforms, sizeof(uniforms)); - SDL_PushGPUFragmentUniformData(commands, 0, fragmentUniforms, sizeof(FragmentUniformsT)); + SDL_PushGPUFragmentUniformData(commands, FRAME_UNIFORMS, fragmentUniforms, sizeof(FragmentUniformsT)); lastBlend = run->blend; } params.flags[0] = (run->blend == PARTICLE_ADD) ? 1.0f : 0.0f; @@ -1826,8 +1834,6 @@ static void _drawParticles(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pa } -// A fingerprint of everything a point light's faces depend on: the light, its range, and every -// caster's transform; skinned and morphing casters count as always changed. // The post pass: exposure, bloom (the main camera's, when on), the tone curve and the sRGB // encode, from the camera's HDR target into its display texture. static void _drawPost(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame) { @@ -1841,8 +1847,9 @@ static void _drawPost(SDL_GPUCommandBuffer *commands, const CameraFrameT *frame) return; } if (bloom) { + // Under BLOOM_LEVELS_MIN levels (a tiny target) nothing was rendered into bloomUp[0]. _drawBloom(commands, frame); - bloom = _scene.bloomLevels > 0; + bloom = _scene.bloomLevels >= BLOOM_LEVELS_MIN; } memset(&colour, 0, sizeof(colour)); colour.texture = frame->output; @@ -1875,7 +1882,7 @@ static void _drawSky(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, co if (_scene.skyCube == NULL) { return; } - if ((_scene.skyPipeline == NULL) && !_createSkyPipeline()) { + if ((_scene.skyPipeline[frame->sampleSet] == NULL) && !_createSkyPipeline(frame->sampleSet)) { return; } memset(&uniforms, 0, sizeof(uniforms)); @@ -1886,7 +1893,7 @@ static void _drawSky(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, co uniforms.eye[1] = frame->eye.y; uniforms.eye[2] = frame->eye.z; uniforms.params[0] = _scene.skyIntensity; - SDL_BindGPUGraphicsPipeline(pass, _scene.skyPipeline); + SDL_BindGPUGraphicsPipeline(pass, _scene.skyPipeline[frame->sampleSet]); memset(&sampler, 0, sizeof(sampler)); sampler.texture = _scene.skyCube; sampler.sampler = _scene.skySampler; @@ -1896,14 +1903,6 @@ static void _drawSky(SDL_GPUCommandBuffer *commands, SDL_GPURenderPass *pass, co } -static Vec3T _eyeOf(int32_t camera) { - if ((camera == NO_HANDLE) || !nodeValid(camera)) { - return vec3(0.0f, 0.0f, DEFAULT_EYE_Z); - } - return mat4TransformPoint(_scene.nodes[camera].world, vec3(0.0f, 0.0f, 0.0f)); -} - - // The world direction through a cube face at s, t (-1 to 1, t down), in the usual cube map // convention (+X right, +Y up, +Z toward the viewer of a face looking down -Z). static Vec3T _faceDirection(int32_t face, float s, float t) { @@ -1924,6 +1923,8 @@ static Vec3T _faceDirection(int32_t face, float s, float t) { } +// A fingerprint of everything a point light's faces depend on: the light, its range, and every +// caster's transform; skinned and morphing casters count as always changed. static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount) { uint64_t hash = 1469598103934665603ULL; int32_t x; @@ -1945,7 +1946,7 @@ static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount) { if (!node->shadowCaster) { continue; } - if ((mesh->skinned && (node->skinCount > 0)) || ((mesh->morphBuffer != NULL) && (node->morphCount > 0))) { + if (_isSkinned(node, mesh) || ((mesh->morphBuffer != NULL) && (node->morphCount > 0))) { return 0; } hash = (hash ^ (uint64_t)(uint32_t)_scene.draws[x].node) * 1099511628211ULL; @@ -1958,7 +1959,6 @@ static uint64_t _cubeHash(const ShadowT *shadow, int32_t drawCount) { } -// Marks the draws a point light's face cannot see: bounding sphere against the 90 degree frustum. // Draws outside one cascade's box (in the cascade's light view, looking down -Z) are skipped. static void _cullCascade(const ShadowT *shadow, int32_t cascade, int32_t drawCount, bool *skip) { int32_t x; @@ -2012,6 +2012,8 @@ static void _cullDraws(const Mat4T *viewProjection, int32_t drawCount, bool *ski } +// Marks the draws a point light's face cannot see: bounding sphere against the 90 degree frustum +// (whose side planes are at 45 degrees, hence the root two on the radius). static void _cullFace(const ShadowT *shadow, int32_t face, int32_t drawCount, bool *skip) { int32_t x; @@ -2020,7 +2022,7 @@ static void _cullFace(const ShadowT *shadow, int32_t face, int32_t drawCount, bo float radius = _scene.draws[x].radius; float ahead = -local.z; - skip[x] = ((ahead + radius < shadow->near) || (ahead - radius > shadow->far) || (fabsf(local.x) - radius * 1.4143f > ahead + radius) || (fabsf(local.y) - radius * 1.4143f > ahead + radius)); + skip[x] = ((ahead + radius < shadow->near) || (ahead - radius > shadow->far) || (fabsf(local.x) - radius * SQRT2 > ahead + radius) || (fabsf(local.y) - radius * SQRT2 > ahead + radius)); } } @@ -2049,11 +2051,11 @@ static void _detach(int32_t node) { } -// The first MAX_LIGHTS visible lights, in world space, and the shadow slots: every one of them -// flagged to cast gets a slot (a map for directional and spot lights, a cube for point lights). -// The frame's model and normal matrices, one pair per draw in draw order. +// The frame's model and normal matrices, one pair per draw in draw order, and the joint matrices +// of every skinned draw, posed once here for every pass that draws it. static void _fillInstances(int32_t drawCount, const CameraFrameT *frame) { int32_t x; + int32_t skins = 0; if (_scene.instanceRoom < drawCount) { SDL_free(_scene.instances); @@ -2066,7 +2068,6 @@ static void _fillInstances(int32_t drawCount, const CameraFrameT *frame) { for (x = 0; x < drawCount; x++) { const NodeT *node = &_scene.nodes[_scene.draws[x].node]; Mat4T model = node->world; - Mat4T inverse; // A billboard keeps its position and scale but takes its axes from the camera: squarely, // or turning about its own up only. @@ -2084,7 +2085,7 @@ static void _fillInstances(int32_t drawCount, const CameraFrameT *frame) { } else { toward = vec3Subtract(frame->eye, position); toward.y = 0.0f; - if (vec3Length(toward) < 0.0001f) { + if (vec3Length(toward) < MATH_EPSILON) { toward = vec3(0.0f, 0.0f, 1.0f); } toward = vec3Normalize(toward); @@ -2111,16 +2112,27 @@ static void _fillInstances(int32_t drawCount, const CameraFrameT *frame) { model = mat4Multiply(model, mat4Compose(vec3(0.0f, 0.0f, 0.0f), quatIdentity(), vec3(sprite->width, sprite->height, 1.0f))); } - _scene.instances[x].model = model; - if (mat4Invert(model, &inverse)) { - _scene.instances[x].normal = mat4Transpose(inverse); - } else { - _scene.instances[x].normal = mat4Identity(); + _scene.instances[x].model = model; + _scene.instances[x].normal = mat4NormalMatrix(model); + _scene.draws[x].skin = NO_HANDLE; + if (_isSkinned(node, &_scene.meshes[node->mesh])) { + if (skins == _scene.skinRoom) { + _scene.skinRoom = SDL_max(skins + 1, _scene.skinRoom * 2); + _scene.skins = SDL_realloc(_scene.skins, sizeof(SkinUniformsT) * (size_t)_scene.skinRoom); + if (_scene.skins == NULL) { + utilDie("Out of memory posing skins."); + } + } + _fillSkin(node, &_scene.skins[skins]); + _scene.draws[x].skin = skins; + skins++; } } } +// The first MAX_LIGHTS visible lights, in world space, and the shadow slots: every one of them +// flagged to cast gets a slot (a map for directional and spot lights, a cube for point lights). static void _fillLights(FragmentUniformsT *uniforms) { int32_t x; int32_t count = 0; @@ -2150,8 +2162,8 @@ static void _fillLights(FragmentUniformsT *uniforms) { light->color[1] = node->light.color.y * node->light.intensity; light->color[2] = node->light.color.z * node->light.intensity; light->color[3] = 1.0f; - light->cone[0] = cosf(node->light.innerDegrees * PI / 180.0f); - light->cone[1] = cosf(node->light.outerDegrees * PI / 180.0f); + light->cone[0] = cosf(DEGREES_TO_RADIANS(node->light.innerDegrees)); + light->cone[1] = cosf(DEGREES_TO_RADIANS(node->light.outerDegrees)); light->cone[2] = 0.0f; light->cone[3] = 0.0f; if (node->castsShadow && (_scene.shadowCount < MAX_SHADOWS)) { @@ -2208,7 +2220,7 @@ static void _fillSkin(const NodeT *node, SkinUniformsT *uniforms) { // Every shadow's projection, fitted to what is drawn: a directional light gets a parallel box // round the scene's bounds, a spot light its own cone, a point light six 90 degree faces out to // its range or the far edge of the scene. -static void _fitShadows(int32_t drawCount) { +static void _fitShadows(int32_t drawCount, const CameraFrameT *camera) { Vec3T corners[8]; Vec3T boundsMin = vec3(0.0f, 0.0f, 0.0f); Vec3T boundsMax = vec3(0.0f, 0.0f, 0.0f); @@ -2240,7 +2252,7 @@ static void _fitShadows(int32_t drawCount) { } } centre = vec3Scale(vec3Add(boundsMin, boundsMax), 0.5f); - radius = vec3Length(vec3Subtract(boundsMax, centre)) * SHADOW_MARGIN + 0.001f; + radius = vec3Length(vec3Subtract(boundsMax, centre)) * SHADOW_MARGIN + BOUNDS_PAD; for (c = 0; c < 8; c++) { corners[c] = vec3((c & 1) ? boundsMax.x : boundsMin.x, (c & 2) ? boundsMax.y : boundsMin.y, (c & 4) ? boundsMax.z : boundsMin.z); } @@ -2249,7 +2261,7 @@ static void _fitShadows(int32_t drawCount) { NodeT *light = &_scene.nodes[shadow->node]; Vec3T direction = vec3Normalize(mat4TransformVector(light->world, vec3(0.0f, 0.0f, -1.0f))); Vec3T position = mat4TransformPoint(light->world, vec3(0.0f, 0.0f, 0.0f)); - Vec3T up = (fabsf(direction.y) < 0.99f) ? vec3(0.0f, 1.0f, 0.0f) : vec3(0.0f, 0.0f, 1.0f); + Vec3T up = (fabsf(direction.y) < UP_PARALLEL_LIMIT) ? vec3(0.0f, 1.0f, 0.0f) : vec3(0.0f, 0.0f, 1.0f); Mat4T view; float far; float minX = 0.0f; @@ -2264,18 +2276,13 @@ static void _fitShadows(int32_t drawCount) { // slice gets a bounding sphere (a stable size as the camera turns), an orthographic box // round it looking along the light and reaching back past the scene for casters behind, // snapped to whole texels so edges hold still as the camera moves. - Mat4T cameraView = _viewOf(_scene.cameraNode); - Mat4T cameraInverse; - float aspect = (_scene.height > 0) ? (float)_scene.width / (float)_scene.height : 1.0f; - float tanHalf = tanf(_scene.fov * 0.5f * PI / 180.0f); - float nearPlane = _scene.near; - float farPlane = SDL_min(_scene.shadowDistance, _scene.far); - float sliceNear = nearPlane; + float aspect = (_scene.height > 0) ? (float)_scene.width / (float)_scene.height : 1.0f; + float tanHalf = tanf(DEGREES_TO_RADIANS(_scene.fov) * 0.5f); + float nearPlane = _scene.near; + float farPlane = SDL_min(_scene.shadowDistance, _scene.far); + float sliceNear = nearPlane; int32_t k; - if (!mat4Invert(cameraView, &cameraInverse)) { - cameraInverse = mat4Identity(); - } for (k = 0; k < shadow->cascades; k++) { float fraction = (float)(k + 1) / (float)shadow->cascades; float logSplit = nearPlane * powf(farPlane / nearPlane, fraction); @@ -2293,7 +2300,7 @@ static void _fitShadows(int32_t drawCount) { for (c = 0; c < 8; c++) { float depth = (c & 4) ? sliceFar : sliceNear; - slice[c] = mat4TransformPoint(cameraInverse, vec3(((c & 1) ? 1.0f : -1.0f) * depth * tanHalf * aspect, ((c & 2) ? 1.0f : -1.0f) * depth * tanHalf, -depth)); + slice[c] = mat4TransformPoint(camera->world, vec3(((c & 1) ? 1.0f : -1.0f) * depth * tanHalf * aspect, ((c & 2) ? 1.0f : -1.0f) * depth * tanHalf, -depth)); sliceCentre = vec3Add(sliceCentre, slice[c]); } sliceCentre = vec3Scale(sliceCentre, 1.0f / 8.0f); @@ -2336,11 +2343,11 @@ static void _fitShadows(int32_t drawCount) { } } // View space looks down -Z: the nearest point has the largest z. - shadow->matrix = mat4Multiply(mat4OrthographicBounds(minX * SHADOW_MARGIN, maxX * SHADOW_MARGIN, minY * SHADOW_MARGIN, maxY * SHADOW_MARGIN, SDL_max(-maxZ / SHADOW_MARGIN, 0.01f), -minZ * SHADOW_MARGIN), view); + shadow->matrix = mat4Multiply(mat4OrthographicBounds(minX * SHADOW_MARGIN, maxX * SHADOW_MARGIN, minY * SHADOW_MARGIN, maxY * SHADOW_MARGIN, SDL_max(-maxZ / SHADOW_MARGIN, SHADOW_NEAR_MIN), -minZ * SHADOW_MARGIN), view); } else { // Near and far from the casters themselves: the nearest caster surface (a shade round the // bulb) sets near, the range or the farthest caster sets far. - float nearest = 1.0e30f; + float nearest = FLT_MAX; float farthest = 0.0f; for (x = 0; x < drawCount; x++) { @@ -2353,12 +2360,12 @@ static void _fitShadows(int32_t drawCount) { farthest = SDL_max(farthest, distance + _scene.draws[x].radius); } far = (light->light.range > 0.0f) ? light->light.range : farthest; - far = SDL_max(far, 0.02f); - shadow->near = SDL_clamp(nearest, SHADOW_NEAR_MIN, far * 0.5f); + far = SDL_max(far, SHADOW_FAR_MIN); + shadow->near = SDL_clamp(nearest, SHADOW_NEAR_MIN, far * SHADOW_NEAR_FRACTION); shadow->far = far; if (light->light.type == LIGHT_SPOT) { view = mat4LookAt(position, vec3Add(position, direction), up); - shadow->matrix = mat4Multiply(mat4Perspective(SDL_min(light->light.outerDegrees * 2.0f * SHADOW_MARGIN, 170.0f), 1.0f, shadow->near, far), view); + shadow->matrix = mat4Multiply(mat4Perspective(SDL_min(light->light.outerDegrees * 2.0f * SHADOW_MARGIN, SPOT_SHADOW_FOV_MAX), 1.0f, shadow->near, far), view); continue; } for (c = 0; c < CUBE_FACES; c++) { @@ -2373,22 +2380,22 @@ static void _fitShadows(int32_t drawCount) { // Collects every 3D emitter's live particles into this frame's vertex list: emitters far to near, // alpha-blended particles far to near within each, one run per frame texture. static void _gatherParticles(Vec3T eye, Vec3T forward) { - EmitterViewT views[PARTICLE_DRAW_MAX]; - DepthOrderT order[PARTICLE_DRAW_MAX]; - DepthOrderT *particleOrder = NULL; + EmitterViewT views[PARTICLE_DRAW_MAX]; + DepthOrderT order[PARTICLE_DRAW_MAX]; + DepthOrderT *particleOrder; ParticleTexturesT *textures; - ParticleViewT *particle; - ParticleVertexT *vertex; - Vec3T origin; - int32_t emitters = 0; - int32_t total = 0; - int32_t needed; - int32_t e; - int32_t i; - int32_t k; - int32_t frame; - int32_t first; - int32_t c; + ParticleViewT *particle; + ParticleVertexT *vertex; + Vec3T origin; + int32_t emitters; + int32_t total = 0; + int32_t needed; + int32_t e; + int32_t i; + int32_t k; + int32_t frame; + int32_t first; + int32_t c; // The two triangles of a quad as corner offsets. static const float corners[PARTICLE_VERTICES][2] = { { -1.0f, -1.0f }, { 1.0f, -1.0f }, { 1.0f, 1.0f }, { -1.0f, -1.0f }, { 1.0f, 1.0f }, { -1.0f, 1.0f } }; @@ -2396,18 +2403,15 @@ static void _gatherParticles(Vec3T eye, Vec3T forward) { _scene.particleVertexCount = 0; _scene.particleSoft = false; _releaseParticleTextures(false); - for (i = 0; (i < particlesCount()) && (emitters < PARTICLE_DRAW_MAX); i++) { - if (!particlesView(i, &views[emitters]) || (views[emitters].node < 0) || (views[emitters].count == 0)) { - continue; - } - origin = nodeValid(views[emitters].node) ? nodeGetWorldPosition(views[emitters].node) : eye; - order[emitters].depth = vec3Dot(vec3Subtract(origin, eye), forward); - order[emitters].index = emitters; - total += views[emitters].count * (1 + SDL_max(views[emitters].trailLength - 1, 0)); - if (views[emitters].softness > 0.0f) { + emitters = particlesView3D(views, PARTICLE_DRAW_MAX); + for (e = 0; e < emitters; e++) { + origin = nodeValid(views[e].node) ? nodeGetWorldPosition(views[e].node) : eye; + order[e].depth = vec3Dot(vec3Subtract(origin, eye), forward); + order[e].index = e; + total += views[e].count * (1 + SDL_max(views[e].trailLength - 1, 0)); + if (views[e].softness > 0.0f) { _scene.particleSoft = true; } - emitters++; } if (total == 0) { return; @@ -2428,10 +2432,14 @@ static void _gatherParticles(Vec3T eye, Vec3T forward) { if (textures == NULL) { continue; } - particleOrder = SDL_malloc(sizeof(DepthOrderT) * (size_t)view->count); - if (particleOrder == NULL) { - utilDie("Out of memory sorting particles."); + if (_scene.particleOrderRoom < view->count) { + _scene.particleOrderRoom = SDL_max(view->count, _scene.particleOrderRoom * 2); + _scene.particleOrder = SDL_realloc(_scene.particleOrder, sizeof(DepthOrderT) * (size_t)_scene.particleOrderRoom); + if (_scene.particleOrder == NULL) { + utilDie("Out of memory sorting particles."); + } } + particleOrder = _scene.particleOrder; for (i = 0; i < view->count; i++) { particleOrder[i].depth = vec3Dot(vec3Subtract(view->particles[i].position, eye), forward); particleOrder[i].index = i; @@ -2447,24 +2455,24 @@ static void _gatherParticles(Vec3T eye, Vec3T forward) { continue; } for (c = 0; c < PARTICLE_VERTICES; c++) { - vertex = &_scene.particleVertices[_scene.particleVertexCount++]; - vertex->centre[0] = particle->position.x; - vertex->centre[1] = particle->position.y; - vertex->centre[2] = particle->position.z; - vertex->corner[0] = corners[c][0]; - vertex->corner[1] = corners[c][1]; - vertex->size = particle->size; - vertex->angle = particle->angle; - vertex->colour[0] = _linearF(particle->colour[0]); - vertex->colour[1] = _linearF(particle->colour[1]); - vertex->colour[2] = _linearF(particle->colour[2]); - vertex->colour[3] = particle->colour[3]; - vertex->uv[0] = corners[c][0] * 0.5f + 0.5f; - vertex->uv[1] = 0.5f - corners[c][1] * 0.5f; + vertex = &_scene.particleVertices[_scene.particleVertexCount++]; + vertex->centre[0] = particle->position.x; + vertex->centre[1] = particle->position.y; + vertex->centre[2] = particle->position.z; + vertex->corner[0] = corners[c][0]; + vertex->corner[1] = corners[c][1]; + vertex->sizeAngle[0] = particle->size; + vertex->sizeAngle[1] = particle->angle; + vertex->colour[0] = _linearF(particle->colour[0]); + vertex->colour[1] = _linearF(particle->colour[1]); + vertex->colour[2] = _linearF(particle->colour[2]); + vertex->colour[3] = particle->colour[3]; + vertex->uv[0] = corners[c][0] * 0.5f + 0.5f; + vertex->uv[1] = 0.5f - corners[c][1] * 0.5f; } _ribbon(view, particleOrder[k].index, eye); } - if ((_scene.particleVertexCount > first) && (_scene.particleRunCount < (int32_t)(sizeof(_scene.particleRuns) / sizeof(_scene.particleRuns[0])))) { + if ((_scene.particleVertexCount > first) && (_scene.particleRunCount < PARTICLE_RUN_MAX)) { _scene.particleRuns[_scene.particleRunCount].first = first; _scene.particleRuns[_scene.particleRunCount].count = _scene.particleVertexCount - first; _scene.particleRuns[_scene.particleRunCount].lit = view->lit; @@ -2474,11 +2482,70 @@ static void _gatherParticles(Vec3T eye, Vec3T forward) { _scene.particleRunCount++; } } - SDL_free(particleOrder); } } +// A grid of columns x rows cells across sizeX by sizeZ, centred on the origin, each vertex lifted +// by its sample (0 to 1) times sizeY, or flat at y = 0 with NULL heights. Rows run from the far +// (-Z) edge or the near one, with UVs 0 to 1 across the whole either way (v = 1 at +Z, as +// meshPlane has it) and the triangles wound to face +Y. Normals come from the slopes. +static int32_t _gridMesh(const float *heights, int32_t columns, int32_t rows, float sizeX, float sizeY, float sizeZ, bool firstRowFar) { + SceneVertexT *vertices; + uint32_t *indices; + int32_t vertexCount = (columns + 1) * (rows + 1); + int32_t indexCount = columns * rows * 6; + float zStart = firstRowFar ? -sizeZ / 2.0f : sizeZ / 2.0f; + float zStep = firstRowFar ? sizeZ : -sizeZ; + int32_t x; + int32_t y; + int32_t mesh; + + if ((columns < 1) || (rows < 1)) { + return NO_HANDLE; + } + vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT)); + indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t)); + if ((vertices == NULL) || (indices == NULL)) { + utilDie("Out of memory making a grid."); + } + for (y = 0; y <= rows; y++) { + for (x = 0; x <= columns; x++) { + float u = (float)x / (float)columns; + float v = (float)y / (float)rows; + float z = zStart + zStep * v; + float h = (heights != NULL) ? heights[y * (columns + 1) + x] * sizeY : 0.0f; + + vertices[y * (columns + 1) + x] = _vertex(-sizeX / 2.0f + sizeX * u, h, z, 0.0f, 1.0f, 0.0f, u, 0.5f + z / sizeZ); + } + } + for (y = 0; y < rows; y++) { + for (x = 0; x < columns; x++) { + uint32_t a = (uint32_t)(y * (columns + 1) + x); + uint32_t b = a + 1; + uint32_t c = a + (uint32_t)(columns + 1); + uint32_t d = c + 1; + uint32_t *tri = &indices[(y * columns + x) * 6]; + + // Counter-clockwise seen from above: the winding flips with the row direction. + tri[0] = a; + tri[1] = firstRowFar ? d : b; + tri[2] = firstRowFar ? b : d; + tri[3] = a; + tri[4] = firstRowFar ? c : d; + tri[5] = firstRowFar ? d : c; + } + } + if (heights != NULL) { + sceneComputeNormals(vertices, vertexCount, indices, indexCount); + } + mesh = _addMesh(vertices, vertexCount, indices, indexCount, false); + SDL_free(vertices); + SDL_free(indices); + return mesh; +} + + static void _freeFeed(FeedT *feed) { if (feed->target != NULL) { SDL_DestroyTexture(feed->target); @@ -2514,6 +2581,7 @@ static void _freeSpriteNode(int32_t node) { } } + static void _freeView(ViewT *view) { _releaseTexture(&view->colour); _releaseTexture(&view->depth); @@ -2564,8 +2632,6 @@ static void _freeSkin(NodeT *node) { } -// A surface of revolution around Y with flat caps: cylinders and cones. Sides get their own -// vertices so the caps can have flat normals. // A float as a 16-bit float (round toward zero; denormals flush to zero). static uint16_t _half(float value) { uint32_t bits; @@ -2613,20 +2679,28 @@ static SDL_GPUTextureFormat _hdrFormat(void) { } +// Whether a draw is posed by a skin this frame (which keeps it out of instanced batches). +static bool _isSkinned(const NodeT *node, const MeshT *mesh) { + return mesh->skinned && (node->skinCount > 0); +} + + +// A surface of revolution around Y with flat caps: cylinders and cones. Sides get their own +// vertices so the caps can have flat normals. static void _lathe(SceneVertexT **vertices, int32_t *vertexCount, uint32_t **indices, int32_t *indexCount, float bottomRadius, float topRadius, float height, int32_t segments) { - int32_t x; - int32_t v = 0; - int32_t i = 0; - float angle; - float c; - float s; - float half = height / 2.0f; - float slope; - float slopeLength; - int32_t bottomCentre; - int32_t topCentre; + int32_t x; + int32_t v = 0; + int32_t i = 0; + float angle; + float c; + float s; + float half = height / 2.0f; + float slope; + float slopeLength; + int32_t bottomCentre; + int32_t topCentre; SceneVertexT *verts; - uint32_t *idx; + uint32_t *idx; // Sides: two rings of segments + 1 vertices (the seam is doubled for the uv wrap), then a // centre and ring per cap. @@ -2637,8 +2711,9 @@ static void _lathe(SceneVertexT **vertices, int32_t *vertexCount, uint32_t **ind if ((verts == NULL) || (idx == NULL)) { utilDie("Out of memory building a mesh."); } + // A zero-height, equal-radius lathe has no slope to take a normal from; keep the division finite. slope = bottomRadius - topRadius; - slopeLength = sqrtf(slope * slope + height * height); + slopeLength = SDL_max(sqrtf(slope * slope + height * height), MATH_EPSILON); for (x = 0; x <= segments; x++) { angle = (float)x / (float)segments * 2.0f * PI; c = cosf(angle); @@ -2684,7 +2759,7 @@ static void _lathe(SceneVertexT **vertices, int32_t *vertexCount, uint32_t **ind // Sizes the node's weight list to its mesh's targets (weights start at 0). static void _matchMorphWeights(NodeT *node) { - int32_t count = ((node->mesh != NO_HANDLE) && meshValid(node->mesh)) ? _scene.meshes[node->mesh].morphCount : 0; + int32_t count = meshValid(node->mesh) ? _scene.meshes[node->mesh].morphCount : 0; if (count == node->morphCount) { return; @@ -2700,7 +2775,57 @@ static void _matchMorphWeights(NodeT *node) { } -// What the fragment shader samples for a material: its video feed, its image, or NULL. +// White, half rough, no texture: the look of a new material and of a node without one. +static void _materialDefaults(MaterialT *material) { + memset(material, 0, sizeof(*material)); + material->baseColor.x = 1.0f; + material->baseColor.y = 1.0f; + material->baseColor.z = 1.0f; + material->baseColor.w = 1.0f; + material->roughness = DEFAULT_ROUGHNESS; + material->tilingU = 1.0f; + material->tilingV = 1.0f; + material->feed = NO_HANDLE; + material->view = NO_HANDLE; +} + + +// Puts an uploaded texture (or NULL, clearing) in one of a material's map slots, releasing what +// was there, with the strength that goes with the map: the normal map's bump scale or the +// occlusion map's blend. A base texture also drops any video feed or view the material showed. +static void _materialPlace(MaterialT *material, MaterialMapE map, SDL_GPUTexture *texture, float strength) { + SDL_GPUTexture **slot; + + switch (map) { + case MAP_NORMAL: + slot = &material->normalMap; + material->normalStrength = SDL_max(strength, 0.0f); + break; + case MAP_OCCLUSION: + slot = &material->occlusionMap; + material->occlusionStrength = SDL_clamp(strength, 0.0f, 1.0f); + break; + case MAP_METALLIC_ROUGHNESS: + slot = &material->metallicRoughnessMap; + break; + case MAP_EMISSIVE: + slot = &material->emissiveMap; + break; + default: + slot = &material->texture; + break; + } + if (map == MAP_BASE) { + _releaseMaterialBase(material); + material->feed = NO_HANDLE; + material->view = NO_HANDLE; + } else { + _releaseTexture(slot); + } + *slot = texture; +} + + // Levels in a full mipmap chain down to 1x1. static uint32_t _mipLevels(int32_t width, int32_t height) { uint32_t levels = 1; @@ -2714,6 +2839,8 @@ static uint32_t _mipLevels(int32_t width, int32_t height) { } +// What the fragment shader samples for a material: its video feed, its rendered view, its image, +// or NULL. static SDL_GPUTexture *_materialTexture(const MaterialT *material) { if ((material->feed != NO_HANDLE) && (material->feed < _scene.feedCount) && _scene.feeds[material->feed].used) { return _scene.feeds[material->feed].gpu; @@ -2740,27 +2867,9 @@ static float _linearF(float value) { } -// The pipeline variant a node's mesh and material call for, created on first use. -static int32_t _lookupPipeline(int32_t node) { - int32_t variant = 0; - MaterialT *material; - - if (_scene.meshes[_scene.nodes[node].mesh].skinned && (_scene.nodes[node].skinCount > 0)) { - variant |= PIPELINE_SKINNED; - } - if (_scene.nodes[node].material != NO_HANDLE) { - material = &_scene.materials[_scene.nodes[node].material]; - if (material->blend) { - variant |= PIPELINE_BLEND; - } - if (material->doubleSided) { - variant |= PIPELINE_TWO_SIDED; - } - } - if ((_scene.pipelines[variant] == NULL) && !_createPipeline(variant)) { - return NO_HANDLE; - } - return variant; +// Colour maps are sRGB (the sampler decodes them); data maps are not. +static bool _mapIsColour(MaterialMapE map) { + return (map == MAP_BASE) || (map == MAP_EMISSIVE); } @@ -2818,6 +2927,40 @@ static ParticleTexturesT *_particleTextures(const EmitterViewT *view) { } +// The first of the wanted depth formats the device offers for the usage, else 16-bit. +static SDL_GPUTextureFormat _pickDepthFormat(const SDL_GPUTextureFormat *wanted, int32_t count, SDL_GPUTextureUsageFlags usage) { + int32_t x; + + for (x = 0; x < count; x++) { + if (SDL_GPUTextureSupportsFormat(_scene.device, wanted[x], SDL_GPU_TEXTURETYPE_2D, usage)) { + return wanted[x]; + } + } + return SDL_GPU_TEXTUREFORMAT_D16_UNORM; +} + + +// The PIPELINE_* variant a node's mesh and material call for. +static int32_t _pipelineVariant(int32_t node) { + int32_t variant = 0; + MaterialT *material; + + if (_isSkinned(&_scene.nodes[node], &_scene.meshes[_scene.nodes[node].mesh])) { + variant |= PIPELINE_SKINNED; + } + if (_scene.nodes[node].material != NO_HANDLE) { + material = &_scene.materials[_scene.nodes[node].material]; + if (material->blend) { + variant |= PIPELINE_BLEND; + } + if (material->doubleSided) { + variant |= PIPELINE_TWO_SIDED; + } + } + return variant; +} + + // The camera's projection for a target of the given size. static Mat4T _projectionFor(int32_t width, int32_t height) { float aspect = (height > 0) ? (float)width / (float)height : 1.0f; @@ -2829,7 +2972,26 @@ static Mat4T _projectionFor(int32_t width, int32_t height) { } -// A depth format the shadow map can be both rendered into and sampled from. +// A two-triangle quad centred on the origin: width along +X, height along down (the edge where +// v = 1, so a picture on it reads upright), facing normal. +static int32_t _quadMesh(float width, float height, Vec3T down, Vec3T normal) { + SceneVertexT vertices[4]; + uint32_t indices[6] = { 0, 1, 2, 0, 2, 3 }; + float w = width / 2.0f; + float h = height / 2.0f; + Vec3T corner; + int32_t x; + // Counter-clockwise seen from the front: each corner's (right, down) signs and its uv. + static const float corners[4][4] = { { -1.0f, 1.0f, 0.0f, 1.0f }, { 1.0f, 1.0f, 1.0f, 1.0f }, { 1.0f, -1.0f, 1.0f, 0.0f }, { -1.0f, -1.0f, 0.0f, 0.0f } }; + + for (x = 0; x < 4; x++) { + corner = vec3Add(vec3(corners[x][0] * w, 0.0f, 0.0f), vec3Scale(down, corners[x][1] * h)); + vertices[x] = _vertex(corner.x, corner.y, corner.z, normal.x, normal.y, normal.z, corners[x][2], corners[x][3]); + } + return _addMesh(vertices, 4, indices, 6, false); +} + + // Drops the textures of emitters that no longer exist, or all of them. static void _releaseParticleTextures(bool all) { int32_t x = 0; @@ -2887,6 +3049,14 @@ static void _recordTexture(SDL_GPUTexture *texture, size_t bytes) { } +static void _releasePipeline(SDL_GPUGraphicsPipeline **pipeline) { + if (*pipeline != NULL) { + SDL_ReleaseGPUGraphicsPipeline(_scene.device, *pipeline); + *pipeline = NULL; + } +} + + static void _releaseTexture(SDL_GPUTexture **texture) { int32_t x; @@ -2914,7 +3084,7 @@ static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *fr SDL_GPUColorTargetInfo colour; SDL_GPUDepthStencilTargetInfo depth; SDL_GPURenderPass *pass; - bool *culled; + bool *culled = _skipScratch(drawCount); int32_t x; uniforms->cameraPosition[0] = frame->eye.x; @@ -2926,10 +3096,6 @@ static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *fr uniforms->cameraForward[2] = frame->forward.z; _gatherParticles(frame->eye, frame->forward); _uploadParticles(commands); - culled = SDL_calloc((size_t)SDL_max(drawCount, 1), sizeof(bool)); - if (culled == NULL) { - utilDie("Out of memory culling the scene."); - } _cullDraws(&frame->viewProjection, drawCount, culled); if (frame->main) { _scene.statTotal = drawCount; @@ -2953,7 +3119,7 @@ static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *fr depth.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; _scene.depthPrepass = true; pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth); - _drawList(commands, pass, drawCount, &frame->viewProjection, true, false, culled, NULL); + _drawList(commands, pass, drawCount, &frame->viewProjection, true, false, culled, NULL, SAMPLE_SET_SINGLE); SDL_EndGPURenderPass(pass); _scene.depthPrepass = false; } @@ -2978,12 +3144,11 @@ static void _renderCamera(SDL_GPUCommandBuffer *commands, const CameraFrameT *fr depth.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; pass = SDL_BeginGPURenderPass(commands, &colour, 1, &depth); _drawSky(commands, pass, frame); - _drawList(commands, pass, drawCount, &frame->viewProjection, false, false, culled, uniforms); + _drawList(commands, pass, drawCount, &frame->viewProjection, false, false, culled, uniforms, frame->sampleSet); _drawParticles(commands, pass, frame, uniforms); _drawLines(commands, pass, frame); SDL_EndGPURenderPass(pass); _drawPost(commands, frame); - SDL_free(culled); } @@ -3032,19 +3197,19 @@ static void _ribbon(const EmitterViewT *view, int32_t index, Vec3T eye) { Vec3T edge = quad[c][0] ? side : sidePrevious; Vec3T world = vec3Add(point, vec3Scale(edge, (float)quad[c][1])); - vertex->centre[0] = world.x; - vertex->centre[1] = world.y; - vertex->centre[2] = world.z; - vertex->corner[0] = 0.0f; - vertex->corner[1] = 0.0f; - vertex->size = 0.0f; - vertex->angle = 0.0f; - vertex->colour[0] = _linearF(particle->colour[0]); - vertex->colour[1] = _linearF(particle->colour[1]); - vertex->colour[2] = _linearF(particle->colour[2]); - vertex->colour[3] = particle->colour[3] * (quad[c][0] ? fade1 : fade0); - vertex->uv[0] = 0.5f; - vertex->uv[1] = (quad[c][1] < 0) ? 0.0f : 1.0f; + vertex->centre[0] = world.x; + vertex->centre[1] = world.y; + vertex->centre[2] = world.z; + vertex->corner[0] = 0.0f; + vertex->corner[1] = 0.0f; + vertex->sizeAngle[0] = 0.0f; + vertex->sizeAngle[1] = 0.0f; + vertex->colour[0] = _linearF(particle->colour[0]); + vertex->colour[1] = _linearF(particle->colour[1]); + vertex->colour[2] = _linearF(particle->colour[2]); + vertex->colour[3] = particle->colour[3] * (quad[c][0] ? fade1 : fade0); + vertex->uv[0] = 0.5f; + vertex->uv[1] = (quad[c][1] < 0) ? 0.0f : 1.0f; } previous = current; sidePrevious = side; @@ -3064,7 +3229,7 @@ static bool _sameBatch(int32_t a, int32_t b, bool shadowPass, const bool *skip) return false; } mesh = &_scene.meshes[nb->mesh]; - if ((mesh->skinned && (nb->skinCount > 0)) || _hasMorphs(nb, mesh)) { + if (_isSkinned(nb, mesh) || _hasMorphs(nb, mesh)) { return false; } if (shadowPass) { @@ -3077,6 +3242,13 @@ static bool _sameBatch(int32_t a, int32_t b, bool shadowPass, const bool *skip) } +// The sample count a pipeline set draws with: the window's targets may be multisampled, a view's +// never are. +static SDL_GPUSampleCount _sampleCountOf(int32_t sampleSet) { + return (sampleSet == SAMPLE_SET_MULTI) ? _scene.sampleCount : SDL_GPU_SAMPLECOUNT_1; +} + + // A bilinear sample of an equirectangular image along a direction (the image's centre column // faces -Z, its top is +Y). static Vec3T _sampleEquirect(const float *rgb, int32_t width, int32_t height, Vec3T direction) { @@ -3117,36 +3289,29 @@ static Vec3T _sampleEquirect(const float *rgb, int32_t width, int32_t height, Ve } -// Replaces one of a material's maps; NULL clears it. -static bool _setMap(SDL_GPUTexture **slot, SDL_Surface *image, bool srgb) { - SDL_GPUTexture *texture = NULL; - - if (image != NULL) { - texture = _uploadTexture(image, srgb); - if (texture == NULL) { - return false; - } - } - _releaseTexture(slot); - *slot = texture; - return true; -} - - +// A depth format the shadow map can be both rendered into and sampled from. 16-bit comes before +// 24-bit here: the shadow compare needs no more, and the smaller map samples faster. static SDL_GPUTextureFormat _shadowFormat(void) { - SDL_GPUTextureFormat wanted[] = { SDL_GPU_TEXTUREFORMAT_D32_FLOAT, SDL_GPU_TEXTUREFORMAT_D16_UNORM, SDL_GPU_TEXTUREFORMAT_D24_UNORM }; - int32_t x; + static const SDL_GPUTextureFormat wanted[] = { SDL_GPU_TEXTUREFORMAT_D32_FLOAT, SDL_GPU_TEXTUREFORMAT_D16_UNORM, SDL_GPU_TEXTUREFORMAT_D24_UNORM }; - for (x = 0; x < (int32_t)SDL_arraysize(wanted); x++) { - if (SDL_GPUTextureSupportsFormat(_scene.device, wanted[x], SDL_GPU_TEXTURETYPE_2D, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER)) { - return wanted[x]; - } - } - return SDL_GPU_TEXTUREFORMAT_D16_UNORM; + return _pickDepthFormat(wanted, (int32_t)SDL_arraysize(wanted), SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER); +} + + +// The frame's culling flags, one per draw, cleared; grown to the most draws seen. +static bool *_skipScratch(int32_t drawCount) { + if (_scene.skipRoom < drawCount) { + _scene.skipRoom = SDL_max(drawCount, _scene.skipRoom * 2); + _scene.skip = SDL_realloc(_scene.skip, sizeof(bool) * (size_t)_scene.skipRoom); + if (_scene.skip == NULL) { + utilDie("Out of memory culling the scene."); + } + } + memset(_scene.skip, 0, sizeof(bool) * (size_t)drawCount); + return _scene.skip; } -// World matrices and inherited visibility, depth first. // A 1x1 data texture of one colour, for the maps a material does not have. static SDL_GPUTexture *_solidTexture(uint8_t r, uint8_t g, uint8_t b) { SDL_Surface *pixel = SDL_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32); @@ -3162,23 +3327,88 @@ static SDL_GPUTexture *_solidTexture(uint8_t r, uint8_t g, uint8_t b) { } -// A 16-bit float RGBA cube texture from six face-sized images of pixels in +X, -X, +Y, -Y, +Z, -// -Z order, with its mip chain generated on the GPU. +// Opens a staged upload of bytes: a transfer buffer mapped at staging->mapped for the caller to +// fill. False, with the error traced, when the GPU refuses. +static bool _stageBegin(StagingT *staging, uint32_t bytes) { + SDL_GPUTransferBufferCreateInfo info; + + memset(staging, 0, sizeof(*staging)); + memset(&info, 0, sizeof(info)); + info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + info.size = bytes; + staging->transfer = SDL_CreateGPUTransferBuffer(_scene.device, &info); + if (staging->transfer == NULL) { + utilTrace("Scene: %s", SDL_GetError()); + return false; + } + staging->mapped = SDL_MapGPUTransferBuffer(_scene.device, staging->transfer, false); + if (staging->mapped == NULL) { + utilTrace("Scene: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(_scene.device, staging->transfer); + staging->transfer = NULL; + return false; + } + return true; +} + + +// Unmaps the filled transfer buffer and opens the copy pass (staging->pass) the caller issues its +// uploads into. False, with everything released, when no command buffer could be had. +static bool _stageCopy(StagingT *staging) { + SDL_UnmapGPUTransferBuffer(_scene.device, staging->transfer); + staging->mapped = NULL; + staging->commands = SDL_AcquireGPUCommandBuffer(_scene.device); + if (staging->commands == NULL) { + utilTrace("Scene: %s", SDL_GetError()); + SDL_ReleaseGPUTransferBuffer(_scene.device, staging->transfer); + staging->transfer = NULL; + return false; + } + staging->pass = SDL_BeginGPUCopyPass(staging->commands); + return true; +} + + +// Ends the copy pass, generates the mip chain of mipmaps (when given), submits and releases. +static void _stageEnd(StagingT *staging, SDL_GPUTexture *mipmaps) { + SDL_EndGPUCopyPass(staging->pass); + if (mipmaps != NULL) { + SDL_GenerateMipmapsForGPUTexture(staging->commands, mipmaps); + } + SDL_SubmitGPUCommandBuffer(staging->commands); + SDL_ReleaseGPUTransferBuffer(_scene.device, staging->transfer); + memset(staging, 0, sizeof(*staging)); +} + + +// One upload out of the staged bytes at offset into a level and layer of a texture. +static void _stageTexture(const StagingT *staging, uint32_t offset, SDL_GPUTexture *texture, uint32_t level, uint32_t layer, uint32_t width, uint32_t height) { + SDL_GPUTextureTransferInfo source; + SDL_GPUTextureRegion region; + + memset(&source, 0, sizeof(source)); + memset(®ion, 0, sizeof(region)); + source.transfer_buffer = staging->transfer; + source.offset = offset; + region.texture = texture; + region.mip_level = level; + region.layer = layer; + region.w = width; + region.h = height; + region.d = 1; + SDL_UploadToGPUTexture(staging->pass, &source, ®ion, false); +} + + // A block-compressed (or, as the fallback, RGBA) texture from a transcoded KTX2 image, every mip // level uploaded as it came (no generation: compressed formats cannot be rendered into). static SDL_GPUTexture *_uploadCompressed(const Ktx2ImageT *image, bool srgb) { - SDL_GPUTextureCreateInfo info; - SDL_GPUTransferBufferCreateInfo transferInfo; - SDL_GPUTextureTransferInfo source; - SDL_GPUTextureRegion region; - SDL_GPUTexture *texture; - SDL_GPUTransferBuffer *transfer; - SDL_GPUCommandBuffer *commands; - SDL_GPUCopyPass *pass; - uint8_t *mapped; - size_t total = 0; - size_t offset = 0; - int32_t level; + SDL_GPUTextureCreateInfo info; + SDL_GPUTexture *texture; + StagingT staging; + size_t total = 0; + size_t offset = 0; + int32_t level; for (level = 0; level < image->levelCount; level++) { total += image->levels[level].size; @@ -3210,58 +3440,38 @@ static SDL_GPUTexture *_uploadCompressed(const Ktx2ImageT *image, bool srgb) { utilTrace("Scene: compressed texture: %s", SDL_GetError()); return NULL; } - memset(&transferInfo, 0, sizeof(transferInfo)); - transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; - transferInfo.size = (Uint32)total; - transfer = SDL_CreateGPUTransferBuffer(_scene.device, &transferInfo); - if (transfer == NULL) { - utilTrace("Scene: %s", SDL_GetError()); + if (!_stageBegin(&staging, (uint32_t)total)) { SDL_ReleaseGPUTexture(_scene.device, texture); return NULL; } - mapped = SDL_MapGPUTransferBuffer(_scene.device, transfer, false); for (level = 0; level < image->levelCount; level++) { - memcpy(mapped + offset, image->levels[level].data, image->levels[level].size); + memcpy((uint8_t *)staging.mapped + offset, image->levels[level].data, image->levels[level].size); offset += image->levels[level].size; } - SDL_UnmapGPUTransferBuffer(_scene.device, transfer); - commands = SDL_AcquireGPUCommandBuffer(_scene.device); - pass = SDL_BeginGPUCopyPass(commands); - offset = 0; + if (!_stageCopy(&staging)) { + SDL_ReleaseGPUTexture(_scene.device, texture); + return NULL; + } + offset = 0; for (level = 0; level < image->levelCount; level++) { - memset(&source, 0, sizeof(source)); - memset(®ion, 0, sizeof(region)); - source.transfer_buffer = transfer; - source.offset = (Uint32)offset; - region.texture = texture; - region.mip_level = (Uint32)level; - region.w = (Uint32)image->levels[level].width; - region.h = (Uint32)image->levels[level].height; - region.d = 1; - SDL_UploadToGPUTexture(pass, &source, ®ion, false); + _stageTexture(&staging, (uint32_t)offset, texture, (uint32_t)level, 0, (uint32_t)image->levels[level].width, (uint32_t)image->levels[level].height); offset += image->levels[level].size; } - SDL_EndGPUCopyPass(pass); - SDL_SubmitGPUCommandBuffer(commands); - SDL_ReleaseGPUTransferBuffer(_scene.device, transfer); + _stageEnd(&staging, NULL); _recordTexture(texture, total); return texture; } +// A 16-bit float RGBA cube texture from six face-sized images of pixels in +X, -X, +Y, -Y, +Z, +// -Z order, with its mip chain generated on the GPU. static SDL_GPUTexture *_uploadCube(const uint16_t *pixels, int32_t face) { - SDL_GPUTextureCreateInfo info; - SDL_GPUTransferBufferCreateInfo transferInfo; - SDL_GPUTextureTransferInfo source; - SDL_GPUTextureRegion region; - SDL_GPUTexture *texture; - SDL_GPUTransferBuffer *transfer; - SDL_GPUCommandBuffer *commands; - SDL_GPUCopyPass *pass; - void *mapped; - uint32_t faceBytes = (uint32_t)face * (uint32_t)face * CUBE_CHANNELS * HALF_BYTES; - uint32_t levels = _mipLevels(face, face); - int32_t f; + SDL_GPUTextureCreateInfo info; + SDL_GPUTexture *texture; + StagingT staging; + uint32_t faceBytes = (uint32_t)face * (uint32_t)face * CUBE_CHANNELS * HALF_BYTES; + uint32_t levels = _mipLevels(face, face); + int32_t f; memset(&info, 0, sizeof(info)); info.type = SDL_GPU_TEXTURETYPE_CUBE; @@ -3277,43 +3487,25 @@ static SDL_GPUTexture *_uploadCube(const uint16_t *pixels, int32_t face) { utilTrace("Scene: cube texture: %s", SDL_GetError()); return NULL; } - memset(&transferInfo, 0, sizeof(transferInfo)); - transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; - transferInfo.size = faceBytes * CUBE_FACES; - transfer = SDL_CreateGPUTransferBuffer(_scene.device, &transferInfo); - if (transfer == NULL) { - utilTrace("Scene: %s", SDL_GetError()); + if (!_stageBegin(&staging, faceBytes * CUBE_FACES)) { + SDL_ReleaseGPUTexture(_scene.device, texture); + return NULL; + } + memcpy(staging.mapped, pixels, faceBytes * CUBE_FACES); + if (!_stageCopy(&staging)) { SDL_ReleaseGPUTexture(_scene.device, texture); return NULL; } - mapped = SDL_MapGPUTransferBuffer(_scene.device, transfer, false); - memcpy(mapped, pixels, faceBytes * CUBE_FACES); - SDL_UnmapGPUTransferBuffer(_scene.device, transfer); - commands = SDL_AcquireGPUCommandBuffer(_scene.device); - pass = SDL_BeginGPUCopyPass(commands); for (f = 0; f < CUBE_FACES; f++) { - memset(&source, 0, sizeof(source)); - memset(®ion, 0, sizeof(region)); - source.transfer_buffer = transfer; - source.offset = faceBytes * (uint32_t)f; - region.texture = texture; - region.layer = (Uint32)f; - region.w = (Uint32)face; - region.h = (Uint32)face; - region.d = 1; - SDL_UploadToGPUTexture(pass, &source, ®ion, false); + _stageTexture(&staging, faceBytes * (uint32_t)f, texture, 0, (uint32_t)f, (uint32_t)face, (uint32_t)face); } - SDL_EndGPUCopyPass(pass); - if (levels > 1) { - SDL_GenerateMipmapsForGPUTexture(commands, texture); - } - SDL_SubmitGPUCommandBuffer(commands); - SDL_ReleaseGPUTransferBuffer(_scene.device, transfer); + _stageEnd(&staging, (levels > 1) ? texture : NULL); _recordTexture(texture, (size_t)faceBytes * CUBE_FACES * ((levels > 1) ? 4 : 3) / 3); return texture; } +// World matrices and inherited visibility, depth first. static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisible) { NodeT *n = &_scene.nodes[node]; int32_t child; @@ -3328,15 +3520,11 @@ static void _updateWorld(int32_t node, const Mat4T *parentWorld, bool parentVisi // Copies data into a new GPU buffer through a transfer buffer. static SDL_GPUBuffer *_uploadBuffer(SDL_GPUBufferUsageFlags usage, const void *data, uint32_t size) { - SDL_GPUBufferCreateInfo info; - SDL_GPUTransferBufferCreateInfo transferInfo; - SDL_GPUTransferBufferLocation source; - SDL_GPUBufferRegion region; - SDL_GPUBuffer *buffer; - SDL_GPUTransferBuffer *transfer; - SDL_GPUCommandBuffer *commands; - SDL_GPUCopyPass *pass; - void *mapped; + SDL_GPUBufferCreateInfo info; + SDL_GPUTransferBufferLocation source; + SDL_GPUBufferRegion region; + SDL_GPUBuffer *buffer; + StagingT staging; memset(&info, 0, sizeof(info)); info.usage = usage; @@ -3346,29 +3534,22 @@ static SDL_GPUBuffer *_uploadBuffer(SDL_GPUBufferUsageFlags usage, const void *d utilTrace("Scene: %s", SDL_GetError()); return NULL; } - memset(&transferInfo, 0, sizeof(transferInfo)); - transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; - transferInfo.size = size; - transfer = SDL_CreateGPUTransferBuffer(_scene.device, &transferInfo); - if (transfer == NULL) { - utilTrace("Scene: %s", SDL_GetError()); + if (!_stageBegin(&staging, size)) { + SDL_ReleaseGPUBuffer(_scene.device, buffer); + return NULL; + } + memcpy(staging.mapped, data, size); + if (!_stageCopy(&staging)) { SDL_ReleaseGPUBuffer(_scene.device, buffer); return NULL; } - mapped = SDL_MapGPUTransferBuffer(_scene.device, transfer, false); - memcpy(mapped, data, size); - SDL_UnmapGPUTransferBuffer(_scene.device, transfer); - commands = SDL_AcquireGPUCommandBuffer(_scene.device); - pass = SDL_BeginGPUCopyPass(commands); memset(&source, 0, sizeof(source)); memset(®ion, 0, sizeof(region)); - source.transfer_buffer = transfer; + source.transfer_buffer = staging.transfer; region.buffer = buffer; region.size = size; - SDL_UploadToGPUBuffer(pass, &source, ®ion, false); - SDL_EndGPUCopyPass(pass); - SDL_SubmitGPUCommandBuffer(commands); - SDL_ReleaseGPUTransferBuffer(_scene.device, transfer); + SDL_UploadToGPUBuffer(staging.pass, &source, ®ion, false); + _stageEnd(&staging, NULL); return buffer; } @@ -3411,11 +3592,25 @@ static bool _uploadDynamic(SDL_GPUCommandBuffer *commands, SDL_GPUBufferUsageFla transferInfo.size = *capacity; *transfer = SDL_CreateGPUTransferBuffer(_scene.device, &transferInfo); if ((*buffer == NULL) || (*transfer == NULL)) { + // Nothing kept, so the next frame tries again rather than mapping a buffer it has not got. utilTrace("Scene: %s buffer: %s", what, SDL_GetError()); + if (*buffer != NULL) { + SDL_ReleaseGPUBuffer(_scene.device, *buffer); + } + if (*transfer != NULL) { + SDL_ReleaseGPUTransferBuffer(_scene.device, *transfer); + } + *buffer = NULL; + *transfer = NULL; + *capacity = 0; return false; } } mapped = SDL_MapGPUTransferBuffer(_scene.device, *transfer, true); + if (mapped == NULL) { + utilTrace("Scene: %s buffer: %s", what, SDL_GetError()); + return false; + } memcpy(mapped, data, bytes); SDL_UnmapGPUTransferBuffer(_scene.device, *transfer); pass = SDL_BeginGPUCopyPass(commands); @@ -3448,18 +3643,12 @@ static void _uploadParticles(SDL_GPUCommandBuffer *commands) { // backend that refuses a texture usable as a render target gets a single level instead). Colour // textures are sRGB, so the sampler hands the shader linear light; data textures are not. static SDL_GPUTexture *_uploadTexture(SDL_Surface *image, bool srgb) { - SDL_Surface *rgba; - SDL_GPUTextureCreateInfo info; - SDL_GPUTransferBufferCreateInfo transferInfo; - SDL_GPUTextureTransferInfo source; - SDL_GPUTextureRegion region; - SDL_GPUTexture *texture; - SDL_GPUTransferBuffer *transfer; - SDL_GPUCommandBuffer *commands; - SDL_GPUCopyPass *pass; - void *mapped; - uint32_t size; - uint32_t levels; + SDL_Surface *rgba; + SDL_GPUTextureCreateInfo info; + SDL_GPUTexture *texture; + StagingT staging; + uint32_t size; + uint32_t levels; rgba = SDL_ConvertSurface(image, SDL_PIXELFORMAT_RGBA32); if (rgba == NULL) { @@ -3490,49 +3679,34 @@ static SDL_GPUTexture *_uploadTexture(SDL_Surface *image, bool srgb) { SDL_DestroySurface(rgba); return NULL; } - memset(&transferInfo, 0, sizeof(transferInfo)); - transferInfo.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; - transferInfo.size = size; - transfer = SDL_CreateGPUTransferBuffer(_scene.device, &transferInfo); - if (transfer == NULL) { - utilTrace("Scene: %s", SDL_GetError()); + if (!_stageBegin(&staging, size)) { SDL_ReleaseGPUTexture(_scene.device, texture); SDL_DestroySurface(rgba); return NULL; } - mapped = SDL_MapGPUTransferBuffer(_scene.device, transfer, false); if (rgba->pitch == rgba->w * 4) { - memcpy(mapped, rgba->pixels, size); + memcpy(staging.mapped, rgba->pixels, size); } else { int32_t y; for (y = 0; y < rgba->h; y++) { - memcpy((uint8_t *)mapped + y * rgba->w * 4, (uint8_t *)rgba->pixels + y * rgba->pitch, (size_t)rgba->w * 4); + memcpy((uint8_t *)staging.mapped + y * rgba->w * 4, (uint8_t *)rgba->pixels + y * rgba->pitch, (size_t)rgba->w * 4); } } - SDL_UnmapGPUTransferBuffer(_scene.device, transfer); - commands = SDL_AcquireGPUCommandBuffer(_scene.device); - pass = SDL_BeginGPUCopyPass(commands); - memset(&source, 0, sizeof(source)); - memset(®ion, 0, sizeof(region)); - source.transfer_buffer = transfer; - region.texture = texture; - region.w = (Uint32)rgba->w; - region.h = (Uint32)rgba->h; - region.d = 1; - SDL_UploadToGPUTexture(pass, &source, ®ion, false); - SDL_EndGPUCopyPass(pass); - if (levels > 1) { - SDL_GenerateMipmapsForGPUTexture(commands, texture); + if (!_stageCopy(&staging)) { + SDL_ReleaseGPUTexture(_scene.device, texture); + SDL_DestroySurface(rgba); + return NULL; } - SDL_SubmitGPUCommandBuffer(commands); - SDL_ReleaseGPUTransferBuffer(_scene.device, transfer); + _stageTexture(&staging, 0, texture, 0, 0, (uint32_t)rgba->w, (uint32_t)rgba->h); + _stageEnd(&staging, (levels > 1) ? texture : NULL); SDL_DestroySurface(rgba); _recordTexture(texture, (levels > 1) ? (size_t)size * 4 / 3 : (size_t)size); return texture; } +// A vertex with no tangent (computed on upload) and one full weight on joint 0. static SceneVertexT _vertex(float x, float y, float z, float nx, float ny, float nz, float u, float v) { SceneVertexT out; @@ -3550,12 +3724,12 @@ static SceneVertexT _vertex(float x, float y, float z, float nx, float ny, float } -// The view matrix: the inverse of the camera node's world matrix, or the default view. -// A camera node's view matrix (the default view when there is none). +// The view matrix: the inverse of the camera node's world matrix, or the default view (looking at +// the origin from DEFAULT_EYE_Z) when there is no camera. static Mat4T _viewOf(int32_t camera) { Mat4T view; - if ((camera == NO_HANDLE) || !nodeValid(camera)) { + if (!nodeValid(camera)) { return mat4LookAt(vec3(0.0f, 0.0f, DEFAULT_EYE_Z), vec3(0.0f, 0.0f, 0.0f), vec3(0.0f, 1.0f, 0.0f)); } if (!mat4Invert(_scene.nodes[camera].world, &view)) { @@ -3605,8 +3779,8 @@ bool lightAttach(int32_t node, LightTypeE type) { _scene.nodes[node].light.color = vec3(1.0f, 1.0f, 1.0f); _scene.nodes[node].light.intensity = 1.0f; _scene.nodes[node].light.range = 0.0f; - _scene.nodes[node].light.innerDegrees = 20.0f; - _scene.nodes[node].light.outerDegrees = 30.0f; + _scene.nodes[node].light.innerDegrees = DEFAULT_CONE_INNER; + _scene.nodes[node].light.outerDegrees = DEFAULT_CONE_OUTER; return true; } @@ -3631,12 +3805,24 @@ bool lightSetColor(int32_t node, uint8_t r, uint8_t g, uint8_t b) { } +// Linear components, already in the space the shader wants (glTF light colours are linear). +bool lightSetColorLinear(int32_t node, float r, float g, float b) { + if (!nodeValid(node) || !_scene.nodes[node].hasLight) { + return false; + } + _scene.nodes[node].light.color = vec3(r, g, b); + return true; +} + + +// A spot light's full brightness inside the inner angle, fading to nothing at the outer, which is +// never narrower than the inner (the shader's edge runs from one to the other). bool lightSetCone(int32_t node, float innerDegrees, float outerDegrees) { if (!nodeValid(node) || !_scene.nodes[node].hasLight) { return false; } _scene.nodes[node].light.innerDegrees = innerDegrees; - _scene.nodes[node].light.outerDegrees = outerDegrees; + _scene.nodes[node].light.outerDegrees = SDL_max(outerDegrees, innerDegrees); return true; } @@ -3673,10 +3859,11 @@ bool lightSetShadow(int32_t node, bool shadow) { // ===== Materials ===== +// A sprite node's private material (its base texture is the sprite's) is the node's to delete. bool materialDelete(int32_t material) { int32_t x; - if (!materialValid(material)) { + if (!materialValid(material) || _scene.materials[material].textureBorrowed) { return false; } _freeMaterialTexture(&_scene.materials[material]); @@ -3758,14 +3945,6 @@ bool materialSetEmissiveLinear(int32_t material, float r, float g, float b) { } -bool materialSetEmissiveMap(int32_t material, SDL_Surface *image) { - if (!materialValid(material)) { - return false; - } - return _setMap(&_scene.materials[material].emissiveMap, image, true); -} - - bool materialSetFilter(int32_t material, MaterialFilterE filter) { if (!materialValid(material)) { return false; @@ -3778,48 +3957,38 @@ bool materialSetFilter(int32_t material, MaterialFilterE filter) { // One of a material's textures from a transcoded KTX2 image (colour maps sRGB, data maps not); // NULL clears it. Strength is the normal map's bump scale or the occlusion map's blend. bool materialSetMap(int32_t material, MaterialMapE map, const Ktx2ImageT *image, float strength) { - MaterialT *m; - SDL_GPUTexture **slot; - SDL_GPUTexture *texture = NULL; - bool srgb = (map == MAP_BASE) || (map == MAP_EMISSIVE); + SDL_GPUTexture *texture = NULL; if (!materialValid(material)) { return false; } - m = &_scene.materials[material]; - switch (map) { - case MAP_NORMAL: - slot = &m->normalMap; - m->normalStrength = SDL_max(strength, 0.0f); - break; - case MAP_OCCLUSION: - slot = &m->occlusionMap; - m->occlusionStrength = SDL_clamp(strength, 0.0f, 1.0f); - break; - case MAP_METALLIC_ROUGHNESS: - slot = &m->metallicRoughnessMap; - break; - case MAP_EMISSIVE: - slot = &m->emissiveMap; - break; - default: - slot = &m->texture; - break; - } if (image != NULL) { - texture = _uploadCompressed(image, srgb); + texture = _uploadCompressed(image, _mapIsColour(map)); if (texture == NULL) { return false; } } - if (map == MAP_BASE) { - _releaseMaterialBase(m); - m->feed = NO_HANDLE; - m->view = NO_HANDLE; - } else { - _releaseTexture(slot); + _materialPlace(&_scene.materials[material], map, texture, strength); + return true; +} + + +// The same from a surface, copied into a mipmapped texture. The base map is what an untextured +// material shows plain; a normal map is tangent space (flat is 128, 128, 255); occlusion sits in R +// and metallic-roughness in B and G (glTF's packing); the emissive map multiplies the emissive colour. +bool materialSetMapSurface(int32_t material, MaterialMapE map, SDL_Surface *image, float strength) { + SDL_GPUTexture *texture = NULL; + + if (!materialValid(material)) { + return false; } - *slot = texture; + if (image != NULL) { + texture = _uploadTexture(image, _mapIsColour(map)); + if (texture == NULL) { + return false; + } + } + _materialPlace(&_scene.materials[material], map, texture, strength); return true; } @@ -3833,35 +4002,6 @@ bool materialSetMetallic(int32_t material, float metallic) { } -// glTF's packing: roughness in G, metallic in B, multiplied into the factors. -bool materialSetMetallicRoughnessMap(int32_t material, SDL_Surface *image) { - if (!materialValid(material)) { - return false; - } - return _setMap(&_scene.materials[material].metallicRoughnessMap, image, false); -} - - -// A tangent-space normal map (flat is 128, 128, 255); strength scales the bumps, 1 as authored. -bool materialSetNormalMap(int32_t material, SDL_Surface *image, float strength) { - if (!materialValid(material)) { - return false; - } - _scene.materials[material].normalStrength = SDL_max(strength, 0.0f); - return _setMap(&_scene.materials[material].normalMap, image, false); -} - - -// Ambient occlusion in R, darkening the ambient and environment light only; strength 1 as authored. -bool materialSetOcclusionMap(int32_t material, SDL_Surface *image, float strength) { - if (!materialValid(material)) { - return false; - } - _scene.materials[material].occlusionStrength = SDL_clamp(strength, 0.0f, 1.0f); - return _setMap(&_scene.materials[material].occlusionMap, image, false); -} - - bool materialSetRoughness(int32_t material, float roughness) { if (!materialValid(material)) { return false; @@ -3871,27 +4011,6 @@ bool materialSetRoughness(int32_t material, float roughness) { } -// Copies the image into a GPU texture; NULL removes the texture. -bool materialSetTexture(int32_t material, SDL_Surface *image) { - SDL_GPUTexture *texture = NULL; - - if (!materialValid(material)) { - return false; - } - if (image != NULL) { - texture = _uploadTexture(image, true); - if (texture == NULL) { - return false; - } - } - _releaseMaterialBase(&_scene.materials[material]); - _scene.materials[material].texture = texture; - _scene.materials[material].feed = NO_HANDLE; - _scene.materials[material].view = NO_HANDLE; - return true; -} - - // How many times the material's textures repeat across a surface's 0 to 1 UV range. bool materialSetTiling(int32_t material, float u, float v) { if (!materialValid(material)) { @@ -3912,8 +4031,6 @@ bool materialSetUnlit(int32_t material, bool unlit) { } -// A video player's frames as the base colour texture; replaces any image. The frames arrive -// through sceneUpdateVideo each frame. // A rendered view as the base colour texture; NO_HANDLE goes back to the material's own texture. bool materialSetView(int32_t material, int32_t view) { if (!materialValid(material) || ((view != NO_HANDLE) && !viewValid(view))) { @@ -3927,6 +4044,8 @@ bool materialSetView(int32_t material, int32_t view) { } +// A video player's frames as the base colour texture; replaces any image. The frames arrive +// through sceneUpdateVideo each frame. bool materialSetVideo(int32_t material, int32_t player) { if (!materialValid(material)) { return false; @@ -3947,16 +4066,16 @@ bool materialValid(int32_t material) { // Six faces with their own vertices so each has a flat normal. Centred on the origin. int32_t meshBox(float width, float height, float depth) { - SceneVertexT vertices[24]; - uint32_t indices[36]; - float w = width / 2.0f; - float h = height / 2.0f; - float d = depth / 2.0f; - int32_t face; - int32_t v = 0; - int32_t i = 0; + SceneVertexT vertices[24]; + uint32_t indices[36]; + float w = width / 2.0f; + float h = height / 2.0f; + float d = depth / 2.0f; + int32_t face; + int32_t v = 0; + int32_t i = 0; // Per face: normal, then the four corners counter-clockwise seen from outside. - float faces[6][5][3] = { + float faces[6][5][3] = { { { 0.0f, 0.0f, 1.0f }, { -w, -h, d }, { w, -h, d }, { w, h, d }, { -w, h, d } }, // Front (+Z) { { 0.0f, 0.0f, -1.0f }, { w, -h, -d }, { -w, -h, -d }, { -w, h, -d }, { w, h, -d } }, // Back (-Z) { { 1.0f, 0.0f, 0.0f }, { w, -h, d }, { w, -h, -d }, { w, h, -d }, { w, h, d } }, // Right (+X) @@ -3964,7 +4083,7 @@ int32_t meshBox(float width, float height, float depth) { { { 0.0f, 1.0f, 0.0f }, { -w, h, d }, { w, h, d }, { w, h, -d }, { -w, h, -d } }, // Top (+Y) { { 0.0f, -1.0f, 0.0f }, { -w, -h, -d }, { w, -h, -d }, { w, -h, d }, { -w, -h, d } }, // Bottom (-Y) }; - float uvs[4][2] = { { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f } }; + float uvs[4][2] = { { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f } }; for (face = 0; face < 6; face++) { int32_t corner; @@ -3984,11 +4103,11 @@ int32_t meshBox(float width, float height, float depth) { int32_t meshCone(float radius, float height, int32_t segments) { - SceneVertexT *vertices; - uint32_t *indices; - int32_t vertexCount; - int32_t indexCount; - int32_t mesh; + SceneVertexT *vertices; + uint32_t *indices; + int32_t vertexCount; + int32_t indexCount; + int32_t mesh; _lathe(&vertices, &vertexCount, &indices, &indexCount, radius, 0.0f, height, SDL_max(segments, MIN_SEGMENTS)); mesh = _addMesh(vertices, vertexCount, indices, indexCount, false); @@ -3999,11 +4118,11 @@ int32_t meshCone(float radius, float height, int32_t segments) { int32_t meshCylinder(float radius, float height, int32_t segments) { - SceneVertexT *vertices; - uint32_t *indices; - int32_t vertexCount; - int32_t indexCount; - int32_t mesh; + SceneVertexT *vertices; + uint32_t *indices; + int32_t vertexCount; + int32_t indexCount; + int32_t mesh; _lathe(&vertices, &vertexCount, &indices, &indexCount, radius, radius, height, SDL_max(segments, MIN_SEGMENTS)); mesh = _addMesh(vertices, vertexCount, indices, indexCount, false); @@ -4060,7 +4179,6 @@ int32_t meshFindMorph(int32_t mesh, const char *name) { } -// The mesh's geometry as kept on the CPU: x, y, z per vertex and triangle indices. // Rewrites a mesh's vertex positions (x, y, z per vertex, in the mesh's space), recomputing normals // and bounds and uploading in place, for meshes a soft body drives. bool meshSetPositions(int32_t mesh, const float *positions) { @@ -4107,10 +4225,15 @@ bool meshSetPositions(int32_t mesh, const float *positions) { } } mapped = SDL_MapGPUTransferBuffer(_scene.device, m->transfer, true); + if (mapped == NULL) { + utilTrace("Scene: %s", SDL_GetError()); + return false; + } memcpy(mapped, m->vertices, size); SDL_UnmapGPUTransferBuffer(_scene.device, m->transfer); commands = SDL_AcquireGPUCommandBuffer(_scene.device); if (commands == NULL) { + utilTrace("Scene: %s", SDL_GetError()); return false; } pass = SDL_BeginGPUCopyPass(commands); @@ -4126,6 +4249,7 @@ bool meshSetPositions(int32_t mesh, const float *positions) { } +// The mesh's geometry as kept on the CPU: x, y, z per vertex and triangle indices. bool meshGetGeometry(int32_t mesh, const float **positions, int32_t *vertexCount, const uint32_t **indices, int32_t *indexCount) { if (!meshValid(mesh)) { return false; @@ -4173,8 +4297,8 @@ const char *meshGetMorphName(int32_t mesh, int32_t target) { // shading computed here), uvs (2, may be NULL), and triangle indices. int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount) { SceneVertexT *vertices; - int32_t x; - int32_t mesh; + int32_t x; + int32_t mesh; if ((positions == NULL) || (indices == NULL) || (vertexCount <= 0) || (indexCount < 3)) { return NO_HANDLE; @@ -4221,53 +4345,10 @@ int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const } -// A quad in the XZ plane facing +Y. -// A plane of columns x rows quads, for cloth and terrain that bends. +// A plane of columns x rows quads, for cloth and terrain that bends; the first row is the near +// (+Z) edge. int32_t meshGrid(float width, float depth, int32_t columns, int32_t rows) { - SceneVertexT *vertices; - uint32_t *indices; - int32_t vertexCount = (columns + 1) * (rows + 1); - int32_t indexCount = columns * rows * 6; - int32_t x; - int32_t y; - int32_t mesh; - - if ((columns < 1) || (rows < 1)) { - return NO_HANDLE; - } - vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT)); - indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t)); - if ((vertices == NULL) || (indices == NULL)) { - utilDie("Out of memory making a grid."); - } - for (y = 0; y <= rows; y++) { - for (x = 0; x <= columns; x++) { - float u = (float)x / (float)columns; - float v = (float)y / (float)rows; - - vertices[y * (columns + 1) + x] = _vertex(-width / 2.0f + width * u, 0.0f, depth / 2.0f - depth * v, 0.0f, 1.0f, 0.0f, u, 1.0f - v); - } - } - for (y = 0; y < rows; y++) { - for (x = 0; x < columns; x++) { - uint32_t a = (uint32_t)(y * (columns + 1) + x); - uint32_t b = a + 1; - uint32_t c = a + (uint32_t)(columns + 1); - uint32_t d = c + 1; - uint32_t *tri = &indices[(y * columns + x) * 6]; - - tri[0] = a; - tri[1] = b; - tri[2] = d; - tri[3] = a; - tri[4] = d; - tri[5] = c; - } - } - mesh = _addMesh(vertices, vertexCount, indices, indexCount, false); - SDL_free(vertices); - SDL_free(indices); - return mesh; + return _gridMesh(NULL, columns, rows, width, 0.0f, depth, false); } @@ -4276,79 +4357,35 @@ int32_t meshGrid(float width, float depth, int32_t columns, int32_t rows) { // 0 to 1 across the whole (materialSetTiling repeats a texture over it). Normals come from the // slopes. The samples are kept for the height field body and terrainGetHeight. int32_t meshHeightmap(const float *heights, int32_t columns, int32_t rows, float sizeX, float sizeY, float sizeZ) { - int32_t vertexCount = (columns + 1) * (rows + 1); - int32_t indexCount = columns * rows * 6; - SceneVertexT *vertices; - uint32_t *indices; - int32_t x; - int32_t y; - int32_t mesh; + int32_t vertexCount = (columns + 1) * (rows + 1); + int32_t mesh; + MeshT *m; - if ((heights == NULL) || (columns < 1) || (rows < 1)) { + if (heights == NULL) { return NO_HANDLE; } - vertices = SDL_calloc((size_t)vertexCount, sizeof(SceneVertexT)); - indices = SDL_calloc((size_t)indexCount, sizeof(uint32_t)); - if ((vertices == NULL) || (indices == NULL)) { - utilDie("Out of memory making a heightmap."); + mesh = _gridMesh(heights, columns, rows, sizeX, sizeY, sizeZ, true); + if (mesh == NO_HANDLE) { + return NO_HANDLE; } - for (y = 0; y <= rows; y++) { - for (x = 0; x <= columns; x++) { - float u = (float)x / (float)columns; - float v = (float)y / (float)rows; - - vertices[y * (columns + 1) + x] = _vertex(-sizeX / 2.0f + sizeX * u, heights[y * (columns + 1) + x] * sizeY, -sizeZ / 2.0f + sizeZ * v, 0.0f, 1.0f, 0.0f, u, v); - } - } - for (y = 0; y < rows; y++) { - for (x = 0; x < columns; x++) { - uint32_t a = (uint32_t)(y * (columns + 1) + x); - uint32_t b = a + 1; - uint32_t c = a + (uint32_t)(columns + 1); - uint32_t d = c + 1; - uint32_t *tri = &indices[(y * columns + x) * 6]; - - tri[0] = a; - tri[1] = d; - tri[2] = b; - tri[3] = a; - tri[4] = c; - tri[5] = d; - } - } - sceneComputeNormals(vertices, vertexCount, indices, indexCount); - mesh = _addMesh(vertices, vertexCount, indices, indexCount, false); - SDL_free(vertices); - SDL_free(indices); - if (mesh != NO_HANDLE) { - MeshT *m = &_scene.meshes[mesh]; - - m->heights = SDL_malloc(sizeof(float) * (size_t)vertexCount); - if (m->heights == NULL) { - utilDie("Out of memory keeping a heightmap."); - } - memcpy(m->heights, heights, sizeof(float) * (size_t)vertexCount); - m->heightColumns = columns; - m->heightRows = rows; - m->sizeX = sizeX; - m->sizeY = sizeY; - m->sizeZ = sizeZ; + m = &_scene.meshes[mesh]; + m->heights = SDL_malloc(sizeof(float) * (size_t)vertexCount); + if (m->heights == NULL) { + utilDie("Out of memory keeping a heightmap."); } + memcpy(m->heights, heights, sizeof(float) * (size_t)vertexCount); + m->heightColumns = columns; + m->heightRows = rows; + m->sizeX = sizeX; + m->sizeY = sizeY; + m->sizeZ = sizeZ; return mesh; } +// A quad in the XZ plane facing +Y. int32_t meshPlane(float width, float depth) { - SceneVertexT vertices[4]; - uint32_t indices[6] = { 0, 1, 2, 0, 2, 3 }; - float w = width / 2.0f; - float d = depth / 2.0f; - - vertices[0] = _vertex(-w, 0.0f, d, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f); - vertices[1] = _vertex( w, 0.0f, d, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f); - vertices[2] = _vertex( w, 0.0f, -d, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f); - vertices[3] = _vertex(-w, 0.0f, -d, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f); - return _addMesh(vertices, 4, indices, 6, false); + return _quadMesh(width, depth, vec3(0.0f, 0.0f, 1.0f), vec3(0.0f, 1.0f, 0.0f)); } @@ -4404,16 +4441,16 @@ bool meshSetMorphTargets(int32_t mesh, const float *deltas, int32_t targetCount, // Latitude/longitude sphere; segments around, half as many from pole to pole. int32_t meshSphere(float radius, int32_t segments) { - SceneVertexT *vertices; - uint32_t *indices; - int32_t rings; - int32_t ring; - int32_t seg; - int32_t v = 0; - int32_t i = 0; - int32_t vertexCount; - int32_t indexCount; - int32_t mesh; + SceneVertexT *vertices; + uint32_t *indices; + int32_t rings; + int32_t ring; + int32_t seg; + int32_t v = 0; + int32_t i = 0; + int32_t vertexCount; + int32_t indexCount; + int32_t mesh; segments = SDL_max(segments, MIN_SEGMENTS); rings = SDL_max(segments / 2, 2); @@ -4459,16 +4496,16 @@ int32_t meshSphere(float radius, int32_t segments) { // A ring around Y. int32_t meshTorus(float radius, float tubeRadius, int32_t segments) { - SceneVertexT *vertices; - uint32_t *indices; - int32_t tubeSegments; - int32_t x; - int32_t y; - int32_t v = 0; - int32_t i = 0; - int32_t vertexCount; - int32_t indexCount; - int32_t mesh; + SceneVertexT *vertices; + uint32_t *indices; + int32_t tubeSegments; + int32_t x; + int32_t y; + int32_t v = 0; + int32_t i = 0; + int32_t vertexCount; + int32_t indexCount; + int32_t mesh; segments = SDL_max(segments, MIN_SEGMENTS); tubeSegments = SDL_max(segments / 2, MIN_SEGMENTS); @@ -4604,12 +4641,13 @@ uint32_t nodeGetGeneration(int32_t node) { } -// The node's mesh handle, or -1. +// The node's material handle, or -1. int32_t nodeGetMaterial(int32_t node) { return nodeValid(node) ? _scene.nodes[node].material : NO_HANDLE; } +// The node's mesh handle, or -1. int32_t nodeGetMesh(int32_t node) { if (!nodeValid(node)) { return NO_HANDLE; @@ -4711,7 +4749,7 @@ bool nodeLookAt(int32_t node, Vec3T target) { localTarget = target; } forward = vec3Subtract(localTarget, n->translation); - if (vec3Length(forward) < 1e-6f) { + if (vec3Length(forward) < MATH_EPSILON) { return true; } n->rotation = quatLookRotation(forward, vec3(0.0f, 1.0f, 0.0f)); @@ -4754,8 +4792,6 @@ bool nodeRotate(int32_t node, QuatT delta) { } -// mesh NO_HANDLE clears; material NO_HANDLE means the default look. -// Changes the material and keeps the mesh. bool nodeSetBillboard(int32_t node, BillboardE mode) { if (!nodeValid(node)) { return false; @@ -4765,6 +4801,7 @@ bool nodeSetBillboard(int32_t node, BillboardE mode) { } +// Changes the material and keeps the mesh. bool nodeSetMaterial(int32_t node, int32_t material) { if (!nodeValid(node)) { return false; @@ -4774,6 +4811,7 @@ bool nodeSetMaterial(int32_t node, int32_t material) { } +// mesh NO_HANDLE clears; material NO_HANDLE means the default look. bool nodeSetMesh(int32_t node, int32_t mesh, int32_t material) { if (!nodeValid(node)) { return false; @@ -4863,8 +4901,6 @@ bool nodeSetScale(int32_t node, Vec3T scale) { } -// Drives the node's skinned mesh from other nodes: joints (up to 128) and their inverse bind -// matrices, copied. count 0 removes the skin and the mesh draws unskinned. // The joint nodes of a skinned node's skin, or 0 with none. int32_t nodeGetSkinJoints(int32_t node, const int32_t **joints) { if (!nodeValid(node) || (_scene.nodes[node].skinCount == 0)) { @@ -4877,6 +4913,8 @@ int32_t nodeGetSkinJoints(int32_t node, const int32_t **joints) { } +// Drives the node's skinned mesh from other nodes: joints (up to MAX_JOINTS) and their inverse +// bind matrices, copied. count 0 removes the skin and the mesh draws unskinned. bool nodeSetSkin(int32_t node, const int32_t *joints, const Mat4T *inverseBind, int32_t count) { NodeT *n; @@ -4900,7 +4938,6 @@ bool nodeSetSkin(int32_t node, const int32_t *joints, const Mat4T *inverseBind, } -// Hides the node and everything under it. // Whether the node's mesh is drawn into shadow maps; a bulb's own mesh or a glowing sign is not. bool nodeSetShadow(int32_t node, bool casts) { if (!nodeValid(node)) { @@ -4928,14 +4965,8 @@ bool nodeSetSprite(int32_t node, SDL_Surface **frames, int32_t count, float widt return true; } if (_scene.quadMesh == NO_HANDLE) { - SceneVertexT vertices[4]; - uint32_t indices[6] = { 0, 1, 2, 0, 2, 3 }; - - vertices[0] = _vertex(-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f); - vertices[1] = _vertex(0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f); - vertices[2] = _vertex(0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f); - vertices[3] = _vertex(-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f); - _scene.quadMesh = _addMesh(vertices, 4, indices, 6, false); + // A unit quad in the XY plane facing +Z. + _scene.quadMesh = _quadMesh(1.0f, 1.0f, vec3(0.0f, -1.0f, 0.0f), vec3(0.0f, 0.0f, 1.0f)); if (_scene.quadMesh == NO_HANDLE) { return false; } @@ -4984,7 +5015,6 @@ bool nodeSetSprite(int32_t node, SDL_Surface **frames, int32_t count, float widt _scene.materials[sprite->material].doubleSided = true; _scene.materials[sprite->material].unlit = !lit; _scene.materials[sprite->material].roughness = 1.0f; - sprite->node = node; sprite->count = count; sprite->width = width; sprite->height = height; @@ -5013,6 +5043,8 @@ bool nodeSetSpriteFrame(int32_t node, int32_t frame) { return true; } + +// Hides the node and everything under it. bool nodeSetVisible(int32_t node, bool visible) { if (!nodeValid(node)) { return false; @@ -5030,7 +5062,6 @@ bool nodeSetWorldTransform(int32_t node, Vec3T position, QuatT rotation) { Vec3T parentPosition; QuatT parentRotation; Vec3T parentScale; - QuatT parentInverseRotation; if (!nodeValid(node)) { return false; @@ -5042,12 +5073,8 @@ bool nodeSetWorldTransform(int32_t node, Vec3T position, QuatT rotation) { return true; } mat4Decompose(_scene.nodes[n->parent].world, &parentPosition, &parentRotation, &parentScale); - parentInverseRotation = parentRotation; - parentInverseRotation.x = -parentInverseRotation.x; - parentInverseRotation.y = -parentInverseRotation.y; - parentInverseRotation.z = -parentInverseRotation.z; n->translation = mat4TransformPoint(parentInverse, position); - n->rotation = quatNormalize(quatMultiply(parentInverseRotation, rotation)); + n->rotation = quatNormalize(quatMultiply(quatInverse(parentRotation), rotation)); return true; } @@ -5177,10 +5204,6 @@ void sceneGetSize(int32_t *width, int32_t *height) { } -// device may be NULL (no GPU backend on this machine); the scene then refuses to be enabled. -// Creates the root node, the shaders, the sampler and the white stand-in texture. -// The camera's view matrix: world to a frame looking down -Z with +X right and +Y up, which is -// also the listener frame positional sound wants. Ktx2FormatE sceneCompressedFormat(void) { return _scene.compressedFormat; } @@ -5196,32 +5219,36 @@ void sceneGetStats(int32_t *total, int32_t *drawn, int32_t *batches, int64_t *te } +// The camera's view matrix: world to a frame looking down -Z with +X right and +Y up, which is +// also the listener frame positional sound wants. Mat4T sceneGetView(void) { return _viewOf(_scene.cameraNode); } +// device may be NULL (no GPU backend on this machine); the scene then refuses to be enabled. +// Creates the root node, the shaders, the samplers and the stand-in textures. bool sceneInit(SDL_GPUDevice *device, SDL_Renderer *renderer) { SDL_GPUSamplerCreateInfo samplerInfo; memset(&_scene, 0, sizeof(_scene)); - _scene.device = device; - _scene.renderer = renderer; - _scene.cameraNode = NO_HANDLE; - _scene.perspective = true; - _scene.fov = DEFAULT_FOV; - _scene.near = DEFAULT_NEAR; - _scene.far = DEFAULT_FAR; - _scene.orthoHeight = DEFAULT_EYE_Z; - _scene.ambient = vec3(_linear(DEFAULT_AMBIENT), _linear(DEFAULT_AMBIENT), _linear(DEFAULT_AMBIENT)); - _scene.tonemap = TONEMAP_NEUTRAL; - _scene.skyIntensity = 1.0f; - _scene.environment = true; + _scene.device = device; + _scene.renderer = renderer; + _scene.cameraNode = NO_HANDLE; + _scene.perspective = true; + _scene.fov = DEFAULT_FOV; + _scene.near = DEFAULT_NEAR; + _scene.far = DEFAULT_FAR; + _scene.orthoHeight = DEFAULT_ORTHO_HEIGHT; + _scene.ambient = vec3(_linear(DEFAULT_AMBIENT), _linear(DEFAULT_AMBIENT), _linear(DEFAULT_AMBIENT)); + _scene.tonemap = TONEMAP_NEUTRAL; + _scene.skyIntensity = 1.0f; + _scene.environment = true; _scene.bloomThreshold = DEFAULT_BLOOM_THRESHOLD; _scene.quadMesh = NO_HANDLE; - _scene.antialias = true; - _scene.sampleCount = SDL_GPU_SAMPLECOUNT_1; - _scene.shadowSize = SHADOW_SIZE; + _scene.antialias = true; + _scene.sampleCount = SDL_GPU_SAMPLECOUNT_1; + _scene.shadowSize = SHADOW_SIZE; _scene.shadowCascades = DEFAULT_CASCADES; _scene.shadowDistance = DEFAULT_SHADOW_DISTANCE; // The node tree is plain data and exists on every machine (physics bodies live on nodes); @@ -5311,10 +5338,9 @@ bool sceneIsEnabled(void) { // World point to overlay coordinates using the last rendered frame's camera. Returns false when // the point is behind the camera (x and y are still filled in). bool sceneProject(Vec3T world, float *x, float *y, float *depth) { - Vec3T clip; - float w = _scene.viewProjection.m[3] * world.x + _scene.viewProjection.m[7] * world.y + _scene.viewProjection.m[11] * world.z + _scene.viewProjection.m[15]; + float w; + Vec3T clip = mat4Project(_scene.viewProjection, world, &w); - clip = mat4TransformPoint(_scene.viewProjection, world); *x = (clip.x + 1.0f) / 2.0f * (float)_scene.width; *y = (1.0f - clip.y) / 2.0f * (float)_scene.height; *depth = clip.z; @@ -5340,11 +5366,6 @@ void sceneQuit(void) { } } _destroyPipelines(); - for (x = 0; x < PARTICLE_PIPELINES; x++) { - if (_scene.particlePipelines[x] != NULL) { - SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.particlePipelines[x]); - } - } if (_scene.instanceBuffer != NULL) { SDL_ReleaseGPUBuffer(_scene.device, _scene.instanceBuffer); } @@ -5363,9 +5384,7 @@ void sceneQuit(void) { if (_scene.lineFragment != NULL) { SDL_ReleaseGPUShader(_scene.device, _scene.lineFragment); } - if (_scene.postPipeline != NULL) { - SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.postPipeline); - } + _releasePipeline(&_scene.postPipeline); if (_scene.postVertex != NULL) { SDL_ReleaseGPUShader(_scene.device, _scene.postVertex); } @@ -5387,12 +5406,8 @@ void sceneQuit(void) { if (_scene.bloomUpFragment != NULL) { SDL_ReleaseGPUShader(_scene.device, _scene.bloomUpFragment); } - if (_scene.bloomDownPipeline != NULL) { - SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.bloomDownPipeline); - } - if (_scene.bloomUpPipeline != NULL) { - SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.bloomUpPipeline); - } + _releasePipeline(&_scene.bloomDownPipeline); + _releasePipeline(&_scene.bloomUpPipeline); _destroyBloomTargets(); for (x = 0; x < MAX_VIEWS; x++) { _freeView(&_scene.views[x]); @@ -5416,9 +5431,6 @@ void sceneQuit(void) { if (_scene.lineTransfer != NULL) { SDL_ReleaseGPUTransferBuffer(_scene.device, _scene.lineTransfer); } - if (_scene.linePipeline != NULL) { - SDL_ReleaseGPUGraphicsPipeline(_scene.device, _scene.linePipeline); - } _releaseParticleTextures(true); if (_scene.vertexStatic != NULL) { SDL_ReleaseGPUShader(_scene.device, _scene.vertexStatic); @@ -5467,6 +5479,9 @@ void sceneQuit(void) { SDL_free(_scene.particleVertices); SDL_free(_scene.lineVertices); SDL_free(_scene.instances); + SDL_free(_scene.skins); + SDL_free(_scene.skip); + SDL_free(_scene.particleOrder); SDL_free(_scene.spriteNodes); SDL_free(_scene.sizedTextures); SDL_free(_scene.sizedBytes); @@ -5485,13 +5500,14 @@ SDL_Texture *sceneRender(void) { CameraFrameT main; CameraFrameT frame; Mat4T identity = mat4Identity(); - bool *skip = NULL; + bool *skip; int32_t x; - int32_t drawCount = 0; - int32_t opaqueCount = 0; + int32_t drawCount; + int32_t opaqueCount = 0; + int32_t blendedStart; int32_t slot; int32_t face; - int32_t layers = 0; + int32_t layers = 0; NodeT *node; if (!_scene.enabled || (_scene.colour == NULL)) { @@ -5506,6 +5522,7 @@ SDL_Texture *sceneRender(void) { main.softDepth = _scene.softDepth; main.output = _scene.output; main.main = true; + main.sampleSet = (_scene.sampleCount == SDL_GPU_SAMPLECOUNT_1) ? SAMPLE_SET_SINGLE : SAMPLE_SET_MULTI; _scene.viewProjection = main.viewProjection; memset(&fragmentUniforms, 0, sizeof(fragmentUniforms)); fragmentUniforms.ambient[0] = _scene.ambient.x; @@ -5528,8 +5545,9 @@ SDL_Texture *sceneRender(void) { } _scene.shadowCount = 0; _fillLights(&fragmentUniforms); - // Collect what to draw: opaque sorted for batching, then blended back to front from the - // window's camera (a view sees them in that order too). + // Collect what to draw in one walk: opaque draws from the front of the array, sorted for + // batching, blended ones from the back, then moved up behind them and sorted back to front + // from the window's camera (a view sees them in that order too). if (_scene.drawCapacity < _scene.nodeCount) { _scene.draws = SDL_realloc(_scene.draws, sizeof(DrawT) * (size_t)_scene.nodeCount); if (_scene.draws == NULL) { @@ -5537,31 +5555,26 @@ SDL_Texture *sceneRender(void) { } _scene.drawCapacity = _scene.nodeCount; } + blendedStart = _scene.nodeCount; for (x = 0; x < _scene.nodeCount; x++) { node = &_scene.nodes[x]; - if (!node->used || !node->worldVisible || (node->mesh == NO_HANDLE) || !meshValid(node->mesh)) { + if (!node->used || !node->worldVisible || !meshValid(node->mesh)) { continue; } - if ((node->material == NO_HANDLE) || !_scene.materials[node->material].blend) { + if ((node->material != NO_HANDLE) && _scene.materials[node->material].blend) { + blendedStart--; + _scene.draws[blendedStart].node = x; + _scene.draws[blendedStart].depth = vec3Length(vec3Subtract(mat4TransformPoint(node->world, vec3(0.0f, 0.0f, 0.0f)), main.eye)); + } else { _scene.draws[opaqueCount].node = x; _scene.draws[opaqueCount].depth = 0.0f; opaqueCount++; } } + drawCount = opaqueCount + (_scene.nodeCount - blendedStart); qsort(_scene.draws, (size_t)opaqueCount, sizeof(DrawT), _compareOpaque); - drawCount = opaqueCount; - for (x = 0; x < _scene.nodeCount; x++) { - node = &_scene.nodes[x]; - if (!node->used || !node->worldVisible || (node->mesh == NO_HANDLE) || !meshValid(node->mesh)) { - continue; - } - if ((node->material != NO_HANDLE) && _scene.materials[node->material].blend) { - _scene.draws[drawCount].node = x; - _scene.draws[drawCount].depth = vec3Length(vec3Subtract(mat4TransformPoint(node->world, vec3(0.0f, 0.0f, 0.0f)), main.eye)); - drawCount++; - } - } if (drawCount > opaqueCount) { + memmove(&_scene.draws[opaqueCount], &_scene.draws[blendedStart], sizeof(DrawT) * (size_t)(drawCount - opaqueCount)); qsort(&_scene.draws[opaqueCount], (size_t)(drawCount - opaqueCount), sizeof(DrawT), _compareDraws); } commands = SDL_AcquireGPUCommandBuffer(_scene.device); @@ -5579,11 +5592,8 @@ SDL_Texture *sceneRender(void) { layers += (_scene.shadows[slot].type == SHADOW_CUBE) ? CUBE_FACES : _scene.shadows[slot].cascades; } if ((_scene.shadowCount > 0) && (drawCount > 0) && _createShadowMaps(layers)) { - skip = SDL_calloc((size_t)drawCount, sizeof(bool)); - if (skip == NULL) { - utilDie("Out of memory culling shadow casters."); - } - _fitShadows(drawCount); + skip = _skipScratch(drawCount); + _fitShadows(drawCount, &main); fragmentUniforms.shadowParams[0] = SHADOW_BIAS; fragmentUniforms.shadowParams[1] = 1.0f / (float)_scene.shadowSize; fragmentUniforms.shadowParams[2] = (float)_scene.shadowCount; @@ -5617,7 +5627,7 @@ SDL_Texture *sceneRender(void) { depth.texture = _scene.shadowMaps; depth.layer = (Uint8)(shadow->layer + face); pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth); - _drawList(commands, pass, drawCount, &shadow->faces[face], true, true, skip, NULL); + _drawList(commands, pass, drawCount, &shadow->faces[face], true, true, skip, NULL, SAMPLE_SET_SINGLE); SDL_EndGPURenderPass(pass); } } else if (shadow->type == SHADOW_CASCADE) { @@ -5631,7 +5641,7 @@ SDL_Texture *sceneRender(void) { depth.texture = _scene.shadowMaps; depth.layer = (Uint8)(shadow->layer + k); pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth); - _drawList(commands, pass, drawCount, &shadow->faces[k], true, false, skip, NULL); + _drawList(commands, pass, drawCount, &shadow->faces[k], true, false, skip, NULL, SAMPLE_SET_SINGLE); SDL_EndGPURenderPass(pass); } } else { @@ -5639,11 +5649,10 @@ SDL_Texture *sceneRender(void) { depth.texture = _scene.shadowMaps; depth.layer = (Uint8)shadow->layer; pass = SDL_BeginGPURenderPass(commands, NULL, 0, &depth); - _drawList(commands, pass, drawCount, &shadow->matrix, true, false, NULL, NULL); + _drawList(commands, pass, drawCount, &shadow->matrix, true, false, NULL, NULL, SAMPLE_SET_SINGLE); SDL_EndGPURenderPass(pass); } } - SDL_free(skip); } else { // Lights flagged to cast but nothing to draw into: no slots this frame. for (x = 0; x < MAX_LIGHTS; x++) { @@ -5815,10 +5824,6 @@ void sceneSetBackground(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { } -// The shadow map's size in texels per side (default 1024, clamped to 256..4096); larger is -// sharper and slower. The map is rebuilt on the next frame that needs it. -// Whether the sky lights the scene (its diffuse light replacing the flat ambient, its reflections -// on metals and glossy surfaces); on by default when a sky is set. // The glow of everything brighter than the threshold, added back at the given strength (0 off). void sceneSetBloom(float threshold, float strength) { _scene.bloomThreshold = SDL_max(threshold, 0.0f); @@ -5826,6 +5831,8 @@ void sceneSetBloom(float threshold, float strength) { } +// Whether the sky lights the scene (its diffuse light replacing the flat ambient, its reflections +// on metals and glossy surfaces); on by default when a sky is set. bool sceneSetEnvironment(bool lit) { _scene.environment = lit; return true; @@ -5858,6 +5865,8 @@ void sceneSetShadowDistance(float distance) { } +// The shadow map's size in texels per side (default 1024, clamped to 256..4096); larger is +// sharper and slower. The map is rebuilt on the next frame that needs it. void sceneSetShadowSize(int32_t size) { _scene.shadowSize = SDL_clamp(size, SHADOW_SIZE_MIN, SHADOW_SIZE_MAX); _destroyShadowMaps(); @@ -5884,8 +5893,6 @@ Vec3T sceneUnproject(float x, float y, float distance) { } -// Rebuilds every node's world matrix now (sceneRender does it too); physics needs them before the -// step, after animation has moved the nodes. // The sky from an equirectangular image of linear RGB floats (NULL removes it): six cube faces // resampled from it, a mip chain for reflections by roughness, and the spherical harmonics of its // diffuse light. @@ -5945,6 +5952,8 @@ void sceneSetTonemap(SceneTonemapE tonemap) { } +// Rebuilds every node's world matrix now (sceneRender does it too); physics needs them before the +// step, after animation has moved the nodes. void sceneUpdateTransforms(void) { Mat4T identity = mat4Identity(); diff --git a/src/scene.h b/src/scene.h index 6acaed816..58e63b3fe 100644 --- a/src/scene.h +++ b/src/scene.h @@ -28,8 +28,10 @@ #include #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); diff --git a/src/shaders/scene.hlsl b/src/shaders/scene.hlsl index d74862f06..20758bfdc 100644 --- a/src/shaders/scene.hlsl +++ b/src/shaders/scene.hlsl @@ -22,39 +22,24 @@ // 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 PI 3.14159265 -#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 +#define CASCADE_BLEND 0.1 // The fraction of each cascade over which the next blends in +#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 // ----- 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 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 baseTexture : register(t0, space2); SamplerState baseSampler : register(s0, space2); Texture2DArray 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 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 postTexture : register(t0, space2); SamplerState postSampler : register(s0, space2); Texture2D 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 bloomSource : register(t0, space2); // The level above (or the frame) SamplerState bloomSourceSampler : register(s0, space2); Texture2D 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 particleTexture : register(t0, space2); SamplerState particleSampler : register(s0, space2); Texture2D 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,28 +798,13 @@ 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); - } - } - light += lights[x].color.rgb * attenuation * (0.5 + 0.5 * dot(normal, toLight)); + 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; diff --git a/src/singe.c b/src/singe.c index 0bd404298..70c025f39 100644 --- a/src/singe.c +++ b/src/singe.c @@ -49,9 +49,6 @@ LUASOCKET_API int luaopen_socket_serial(lua_State *L); int luaopen_luars232(lua_State *L); // Nor for lsqlite3. int luaopen_lsqlite3(lua_State *L); -// The module table below needs this before the prototypes. -static void _createScriptContext(void); -static int32_t _luaopenLfs(lua_State *L); // There is no header for ssl.config binding. Make our own. LSEC_API int luaopen_ssl_config(lua_State *L); @@ -72,7 +69,6 @@ LSEC_API int luaopen_ssl_config(lua_State *L); // We have to do the embedding here so the Lua module // definitions can find their length properly. They // can't be external to this source file. -#define AUDIO_CALIBRATION_FILE "audio.cfg" // Per-machine audio delay, in the data root #define EMBED_HERE #include "embedded.h" @@ -101,6 +97,8 @@ LSEC_API int luaopen_ssl_config(lua_State *L); #define CODE_MOUSE_BUTTON_COUNT 5 // Left, right, middle, X1, X2 #define CODE_MOUSE_WHEEL_UP (CODE_MOUSE_BUTTON_COUNT) #define CODE_MOUSE_WHEEL_DOWN (CODE_MOUSE_BUTTON_COUNT + 1) +// Codes below the gamepad range are keyboard scancodes, so every key SDL defines must sit under it. +SDL_COMPILE_TIME_ASSERT(codeGamepadBase, CODE_GAMEPAD_BASE > SDL_SCANCODE_ENDCALL); #define FRAME_TICK_MS 15 // Minimum time between onOverlayUpdate calls #define IDLE_SLEEP_MS 1 @@ -109,7 +107,7 @@ LSEC_API int luaopen_ssl_config(lua_State *L); #define DEGREES_PER_CIRCLE 360.0 #define ANIMATION_MIN_DELAY_MS 10 // GIFs often carry a zero delay #define SCREENSHOT_MAX 10000 -#define COLOUR_BYTE_MAX 255 +#define COLOR_BYTE_MAX 255 #define NAV_DRAW_VERTICES 4096 // Starting room for navDraw, doubled until the mesh fits #define SOUND_QUEUE_SIZE 64 #define MS_PER_SECOND_NUMBER 1000.0 @@ -119,13 +117,31 @@ LSEC_API int luaopen_ssl_config(lua_State *L); #define SOUND_DEFAULT_FAR 30.0f // ... and silent beyond this #define SOUND_FADE_FRACTION 0.2f // Of the far distance, over which it fades to silence #define SOUND_LOOP_FOREVER -1 +#define SOUND_DIRECTION_EPSILON 0.0001f // Closer than this to the listener has no direction #define HEIGHTMAP_MAX 1025 // Pixels per side of a heightmap image #define WATCH_INTERVAL_MS 1000 // How often --reload checks the script files +#define AUDIO_CALIBRATION_FILE "audio.cfg" // Per-machine audio delay, in the data root +#define CONTROLS_FILE "controls.cfg" // Input mappings, built in and overridden per game +#define SOUND_CHANNEL_NONE -1 // soundPlay's answer when every channel is busy (SOUND_ERROR_INVALID) +#define COLOR_COMPONENTS 4 // r, g, b, a +#define BYTES_PER_KIB 1024 +#define SHAPE_SIZES 3 // a, b, c sizing a primitive shape +#define VEHICLE_DRIVE_INPUTS 4 // forward, right, brake, hand brake +#define MESH_SEGMENTS_DEFAULT 24 // Around a cone or cylinder +#define MESH_SPHERE_SEGMENTS_DEFAULT 32 // Around a sphere or torus +#define EMITTER_DEFAULT_BOUNCE 0.5f // emitterSetCollide's optional arguments +#define EMITTER_DEFAULT_FRICTION 0.2f +#define EMITTER_DEFAULT_FLOOR 0.0f +#define WATER_DEFAULT_LINEAR_DRAG 0.5f // bodySetWater's optional arguments +#define WATER_DEFAULT_ANGULAR_DRAG 0.1f #define LISTENER_CAMERA -1 // soundSetListener's default: the scene camera #define EFFECT_TAG "effects" #define HELD_KEYS_MAX 64 // Keys physically down at once #define PAUSE_TEXT "PAUSED" #define PAUSE_TEXT_SCALE 3 // Console font is small; scale the indicator up +#define QUAD_VERTICES 4 // A textured quad for SDL_RenderGeometry ... +#define QUAD_INDICES 6 // ... as two triangles +#define TRAIL_POINT_FLOATS 3 // x, y, z per recorded trail point #define COLOR_KEY_VALUE 0 #define BLUE_SCREEN_BLUE 255 @@ -174,6 +190,13 @@ typedef enum RenderQualityE { RENDER_SMOOTH = 1 } RenderQualityE; +// How an io function hooked through the vfs uses its file name. +typedef enum IoHookModeE { + IO_HOOK_READ = 0, + IO_HOOK_OPEN = 1, // The mode string decides + IO_HOOK_WRITE = 2 +} IoHookModeE; + typedef enum OverlayResultE { OVERLAY_NOT_UPDATED = 0, OVERLAY_UPDATED = 1 @@ -241,6 +264,7 @@ typedef struct ParticleTexturesS { uint32_t version; int32_t count; SDL_Texture **textures; + ParticleBlendE blend; // The blend mode the textures were last set to UT_hash_handle hh; } ParticleTexturesT; @@ -259,6 +283,7 @@ typedef struct SpriteS { bool animating; uint64_t lastTick; uint64_t ticks; + uint64_t loopMs; // One pass through every frame's (clamped) delay UT_hash_handle hh; } SpriteT; @@ -283,6 +308,7 @@ typedef struct EffectS { float farOff; // ... silent beyond this float relative[3]; // The last position handed to the mixer, listener space float gain; // The last distance gain + float baseGain; // The effects volume the track gain was last built from bool positioned; } EffectT; @@ -381,6 +407,13 @@ typedef struct GlobalS { VideoT *videoList; SpriteT *spriteList; ParticleTexturesT *particleTextures; // 2D particle textures by emitter + Vec3T *navDrawVertices; // navDraw's triangle list, kept between frames + int32_t navDrawCapacity; + SDL_Vertex *quadVertices; // Scratch for 2D particle quads and trails, grown as needed + int32_t *quadIndices; + int32_t quadCapacity; // In quads + int32_t *frameStarts; // Per emitter frame, where its particles' quads begin in the index scratch + int32_t frameStartCapacity; SoundT *soundList; FontT *fontList; FontT *fontCurrent; @@ -389,65 +422,10 @@ typedef struct GlobalS { } GlobalT; -static GlobalT _global; +static GlobalT _global; static MIX_Track *_effectTracks[EFFECT_TRACKS]; static EffectT _effects[EFFECT_TRACKS]; - -#define MODL(name, array) { name, { (const char *)array }, sizeof(array) } -#define MODC(name, openf) { name, { (const char *)openf }, 0 } - - -// Lua Modules -static const LuaModuleT _luaModules[] = { - // LuaFileSystem - MODC("lfs", _luaopenLfs), - // SQLite for script data - MODC("sqlite3", luaopen_lsqlite3), - // LuaSocket - MODC("mime.core", luaopen_mime_core), - MODC("socket.core", luaopen_socket_core), - MODL("ltn12", ltn12_lua), - MODL("mbox", mbox_lua), - MODL("mime", mime_lua), - MODL("socket", socket_lua), - MODL("socket.ftp", ftp_lua), - MODL("socket.headers", headers_lua), - MODL("socket.http", http_lua), - MODL("socket.smtp", smtp_lua), - MODL("socket.tp", tp_lua), - MODL("socket.url", url_lua), -#ifndef _WIN32 - MODC("socket.unix", luaopen_socket_unix), - MODC("socket.serial", luaopen_socket_serial), -#endif - // LuaSec - MODC("ssl.core", luaopen_ssl_core), - MODC("ssl.context", luaopen_ssl_context), - MODC("ssl.x509", luaopen_ssl_x509), - MODC("ssl.config", luaopen_ssl_config), - MODL("ssl.https", https_lua), - MODL("ssl", ssl_lua), - // LuaRS232 - MODC("rs232.core", luaopen_luars232), - MODL("rs232", rs232_lua), - // binaryheap - MODL("binaryheap", binaryheap_lua), - // timerwheel - MODL("timerwheel", timerwheel_lua), - // json - MODL("json", json_lua), - // Copas - MODL("copas", copas_lua), - MODL("copas.ftp", copas_ftp_lua), - MODL("copas.http", copas_http_lua), - MODL("copas.lock", copas_lock_lua), - MODL("copas.queue", copas_queue_lua), - MODL("copas.semaphore", copas_semaphore_lua), - MODL("copas.smtp", copas_smtp_lua), - MODL("copas.timer", copas_timer_lua), -}; - // One entry per InputE, in order. The config name is the controls.cfg table; the switch name is the Lua constant. static const InputNameT _inputNames[INPUT_COUNT] = { { "INPUT_UP", "SWITCH_UP" }, @@ -485,10 +463,12 @@ static int32_t _argAnimation(lua_State *L, const char *method, int32_t mode static int32_t _argAnimationLayer(lua_State *L, const char *method, int32_t index); static bool _argBoolean(lua_State *L, const char *method, int32_t index); static int32_t _argChannel(lua_State *L, const char *method, int32_t index); +static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index); static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t maximum); static QuatT _argEuler(lua_State *L, const char *method, int32_t index); static float *_argFloatTable(lua_State *L, const char *method, int32_t index, int32_t *count); static FontT *_argFont(lua_State *L, const char *method, int32_t index); +static int32_t _argHandle(lua_State *L, const char *method, int32_t index, bool (*valid)(int32_t), const char *noun); static int32_t _argInteger(lua_State *L, const char *method, int32_t index); static int32_t _argBody(lua_State *L, const char *method, int32_t index); static int32_t _argEmitter(lua_State *L, const char *method, int32_t index); @@ -499,7 +479,8 @@ static int32_t _argMesh(lua_State *L, const char *method, int32_t index); static int32_t _argNav(lua_State *L, const char *method, int32_t index); static int32_t _argNavAgent(lua_State *L, const char *method, int32_t index); static int32_t _argNode(lua_State *L, const char *method, int32_t index); -static void _argOptionalColour(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b); +static int32_t _argNodeWith(lua_State *L, const char *method, int32_t index, bool (*exists)(int32_t), const char *noun); +static void _argOptionalColor(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b); static int32_t _argPlayer(lua_State *L, const char *method, int32_t index); static int32_t _argRagdoll(lua_State *L, const char *method, int32_t index); static int32_t _argSoft(lua_State *L, const char *method, int32_t index); @@ -514,6 +495,7 @@ static int32_t _argView(lua_State *L, const char *method, int32_t index); static VideoT *_argVideo(lua_State *L, const char *method, int32_t index); static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base); static void _callLua(const char *func, const char *sig, ...); +static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2); static int32_t _effectTrackFree(void); static float _effectGain(const EffectT *effect, float distance); static void _effectReset(int32_t channel); @@ -521,6 +503,7 @@ static void _effectStopped(void *userdata, MIX_Track *track); static int32_t _controllerSlot(SDL_JoystickID which); static bool _delayAndPump(uint32_t ms); static void _deliverKey(bool down, int32_t keysym, int32_t scancode); +static int64_t _discGetFrame(void); static void _discSeek(int64_t frame); static void _doLogos(void); static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel); @@ -540,21 +523,24 @@ static int32_t _lfsDirIterator(lua_State *L); static int32_t _lfsMkdir(lua_State *L); static int32_t _lfsRmdir(lua_State *L); static int32_t _loadAudioCalibration(void); +static void _loadControlMappings(void); static void _loadControlsFile(const char *path); -static SDL_Surface *_loadEmbeddedPng(const unsigned char *data, unsigned int length); -static SDL_Texture *_loadEmbeddedTexture(const unsigned char *data, unsigned int length, SDL_Surface **surface); +static SDL_Surface *_loadEmbeddedPng(const uint8_t *data, size_t length); +static SDL_Texture *_loadEmbeddedTexture(const uint8_t *data, size_t length, SDL_Surface **surface); +static int32_t _luaCallOriginal(lua_State *L); static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))) __attribute__((noreturn)); static int32_t _luaDofile(lua_State *L); static int32_t _luaFileSearcher(lua_State *L); static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_list args); static int32_t _luaIoHook(lua_State *L); static int32_t _luaLoadfile(lua_State *L); -static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode); +static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode, bool watch); static int32_t _luaopenLfs(lua_State *L); static int32_t _luaPanic(lua_State *L); static int32_t _luaSearcher(lua_State *L); static void _luaTrace(lua_State *L, const char *method, const char *fmt, ...) __attribute__((format(printf, 3, 4))); static int32_t _luaTraceback(lua_State *L); +static int32_t _materialSetMap(lua_State *L, const char *method, MaterialMapE map, bool hasStrength); static float _mixerGain(int32_t effectsVolume); static int32_t _mouseCode(int32_t device, int32_t button); static uint32_t _overlayColor(const SDL_Color *color); @@ -564,12 +550,20 @@ static void _pauseAllVideos(bool pause); static void _processKey(bool down, int32_t keysym, int32_t scancode); static void _progTrace(const char *fmt, ...) __attribute__((format(printf, 1, 2))); static int32_t _pushVec3(lua_State *L, Vec3T v); +static void _quadIndex(int32_t *indices, int32_t slot, int32_t quad); +static void _quadScratch(int32_t quads, int32_t frames); static void _navCallbacks(void); static void _physicsCallbacks(void); +static void _particleTexturesDestroy(ParticleTexturesT *cache); static void _particleTexturesFree(int32_t emitter); +static void _particleTexturesFreeAll(void); +static ParticleTexturesT *_particleTexturesGet(const EmitterViewT *view); static void _registerApi(lua_State *L); static void _reloadScript(void); +static void _resetScriptState(void); +static void _runScript(bool fatal); static SDL_Texture *_sceneVideoSource(int32_t player); +static ConfigT *_scriptConfFromTable(lua_State *L, const char *method); static void _pushConstants(lua_State *L); static void _putPixel(int32_t x, int32_t y, uint32_t pixel); static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha); @@ -580,14 +574,16 @@ static void _selectDefaultAudioTrack(int32_t handle); static void _setMouseCaptured(bool captured); static void _setPause(bool paused, bool fromKey); static void _soundDestroy(SoundT *sound); +static int32_t _soundQueueDrain(int32_t *finished); static void _spriteDestroy(SpriteT *sprite); static void _spriteFreeSurface(SpriteT *sprite); static void _spriteRebuildSurface(SpriteT *sprite); static void _startControllers(void); static void _startLuaContext(lua_State *L); static void _stopControllers(void); +static void _subsystemsInit(void); +static void _subsystemsQuit(void); static void _suppressHeldInput(void); -static SDL_Surface *_surfaceCopy(SDL_Surface *source); static uint32_t _surfaceCornerKey(SDL_Surface *surface); static void _surfaceUnpack(SDL_Surface **surface); static void _takeScreenshot(void); @@ -963,6 +959,61 @@ static int32_t apiVldpGetPixel(lua_State *L); static int32_t apiVldpSetVerbose(lua_State *L); +#define MODL(name, array) { name, { (const char *)array }, sizeof(array) } +#define MODC(name, openf) { name, { (const char *)openf }, 0 } + + +// Lua Modules +static const LuaModuleT _luaModules[] = { + // LuaFileSystem + MODC("lfs", _luaopenLfs), + // SQLite for script data + MODC("sqlite3", luaopen_lsqlite3), + // LuaSocket + MODC("mime.core", luaopen_mime_core), + MODC("socket.core", luaopen_socket_core), + MODL("ltn12", ltn12_lua), + MODL("mbox", mbox_lua), + MODL("mime", mime_lua), + MODL("socket", socket_lua), + MODL("socket.ftp", ftp_lua), + MODL("socket.headers", headers_lua), + MODL("socket.http", http_lua), + MODL("socket.smtp", smtp_lua), + MODL("socket.tp", tp_lua), + MODL("socket.url", url_lua), +#ifndef _WIN32 + MODC("socket.unix", luaopen_socket_unix), + MODC("socket.serial", luaopen_socket_serial), +#endif + // LuaSec + MODC("ssl.core", luaopen_ssl_core), + MODC("ssl.context", luaopen_ssl_context), + MODC("ssl.x509", luaopen_ssl_x509), + MODC("ssl.config", luaopen_ssl_config), + MODL("ssl.https", https_lua), + MODL("ssl", ssl_lua), + // LuaRS232 + MODC("rs232.core", luaopen_luars232), + MODL("rs232", rs232_lua), + // binaryheap + MODL("binaryheap", binaryheap_lua), + // timerwheel + MODL("timerwheel", timerwheel_lua), + // json + MODL("json", json_lua), + // Copas + MODL("copas", copas_lua), + MODL("copas.ftp", copas_ftp_lua), + MODL("copas.http", copas_http_lua), + MODL("copas.lock", copas_lock_lua), + MODL("copas.queue", copas_queue_lua), + MODL("copas.semaphore", copas_semaphore_lua), + MODL("copas.smtp", copas_smtp_lua), + MODL("copas.timer", copas_timer_lua), +}; + + // ===== Internal helpers ===== @@ -1020,6 +1071,12 @@ static int32_t _argChannel(lua_State *L, const char *method, int32_t index) { } +// A colour component argument, clamped to 0..255. +static uint8_t _argColorByte(lua_State *L, const char *method, int32_t index) { + return (uint8_t)SDL_clamp(_argInteger(L, method, index), 0, COLOR_BYTE_MAX); +} + + // Dies unless the argument count is within [minimum, maximum]. static void _argCheck(lua_State *L, const char *method, int32_t minimum, int32_t maximum) { int32_t n = lua_gettop(L); @@ -1082,6 +1139,17 @@ static float *_argFloatTable(lua_State *L, const char *method, int32_t index, in } +// A handle argument the given test accepts, named for the message: "No material 7." +static int32_t _argHandle(lua_State *L, const char *method, int32_t index, bool (*valid)(int32_t), const char *noun) { + int32_t handle = _argInteger(L, method, index); + + if (!valid(handle)) { + _luaDie(L, method, "No %s %d.", noun, handle); + } + return handle; +} + + static int32_t _argInteger(lua_State *L, const char *method, int32_t index) { return (int32_t)_argNumber(L, method, index); } @@ -1108,28 +1176,16 @@ static int32_t _argMorph(lua_State *L, const char *method, int32_t node, int32_t // A node that carries a physics body, checked. static int32_t _argBody(lua_State *L, const char *method, int32_t index) { - int32_t node = _argNode(L, method, index); - - if (!bodyExists(node)) { - _luaDie(L, method, "Node %d has no body.", node); - } - return node; + return _argNodeWith(L, method, index, bodyExists, "body"); } // An emitter handle argument, checked. static int32_t _argEmitter(lua_State *L, const char *method, int32_t index) { - int32_t emitter = _argInteger(L, method, index); - - if (!emitterValid(emitter)) { - _luaDie(L, method, "No emitter %d.", emitter); - } - return emitter; + return _argHandle(L, method, index, emitterValid, "emitter"); } -// A material handle argument, checked. - // A texture argument for a material map: a loaded sprite (its surface comes back), or the name of a // KTX2 file (transcoded into ktx2, true comes back and the caller frees it). static bool _argMapImage(lua_State *L, const char *method, int32_t index, SDL_Surface **surface, Ktx2ImageT *ktx2) { @@ -1161,112 +1217,80 @@ static bool _argMapImage(lua_State *L, const char *method, int32_t index, SDL_Su return true; } -static int32_t _argMaterial(lua_State *L, const char *method, int32_t index) { - int32_t material = _argInteger(L, method, index); - if (!materialValid(material)) { - _luaDie(L, method, "No material %d.", material); - } - return material; +// A material handle argument, checked. +static int32_t _argMaterial(lua_State *L, const char *method, int32_t index) { + return _argHandle(L, method, index, materialValid, "material"); } // A mesh handle argument, checked. static int32_t _argMesh(lua_State *L, const char *method, int32_t index) { - int32_t mesh = _argInteger(L, method, index); - - if (!meshValid(mesh)) { - _luaDie(L, method, "No mesh %d.", mesh); - } - return mesh; + return _argHandle(L, method, index, meshValid, "mesh"); } -// A scene node handle argument, checked. - // A navigation mesh handle from navNew or navLoad. static int32_t _argNav(lua_State *L, const char *method, int32_t index) { - int32_t nav = _argInteger(L, method, index); - - if (!navValid(nav)) { - _luaDie(L, method, "Invalid navigation mesh: %d", nav); - } - return nav; + return _argHandle(L, method, index, navValid, "navigation mesh"); } // An agent handle from navAgentNew. static int32_t _argNavAgent(lua_State *L, const char *method, int32_t index) { - int32_t agent = _argInteger(L, method, index); - - if (!navAgentValid(agent)) { - _luaDie(L, method, "Invalid navigation agent: %d", agent); - } - return agent; + return _argHandle(L, method, index, navAgentValid, "navigation agent"); } -static int32_t _argNode(lua_State *L, const char *method, int32_t index) { - int32_t node = _argInteger(L, method, index); - if (!nodeValid(node)) { - _luaDie(L, method, "No node %d.", node); +// A scene node handle argument, checked. +static int32_t _argNode(lua_State *L, const char *method, int32_t index) { + return _argHandle(L, method, index, nodeValid, "node"); +} + + +// A node argument carrying the given kind of thing: "Node 7 has no body." +static int32_t _argNodeWith(lua_State *L, const char *method, int32_t index, bool (*exists)(int32_t), const char *noun) { + int32_t node = _argNode(L, method, index); + + if (!exists(node)) { + _luaDie(L, method, "Node %d has no %s.", node, noun); } return node; } // r, g, b at index onwards when the script gave them; the caller's defaults otherwise. -static void _argOptionalColour(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b) { +static void _argOptionalColor(lua_State *L, const char *method, int32_t index, uint8_t *r, uint8_t *g, uint8_t *b) { if (lua_gettop(L) < index + 2) { return; } - *r = (uint8_t)_argInteger(L, method, index); - *g = (uint8_t)_argInteger(L, method, index + 1); - *b = (uint8_t)_argInteger(L, method, index + 2); + *r = _argColorByte(L, method, index); + *g = _argColorByte(L, method, index + 1); + *b = _argColorByte(L, method, index + 2); } // A node carrying a player, checked. static int32_t _argPlayer(lua_State *L, const char *method, int32_t index) { - int32_t node = _argNode(L, method, index); - - if (!playerExists(node)) { - _luaDie(L, method, "Node %d has no player.", node); - } - return node; + return _argNodeWith(L, method, index, playerExists, "player"); } // A node carrying a ragdoll, checked. static int32_t _argRagdoll(lua_State *L, const char *method, int32_t index) { - int32_t node = _argNode(L, method, index); - - if (!ragdollExists(node)) { - _luaDie(L, method, "Node %d has no ragdoll.", node); - } - return node; + return _argNodeWith(L, method, index, ragdollExists, "ragdoll"); } // A node carrying a soft body, checked. static int32_t _argSoft(lua_State *L, const char *method, int32_t index) { - int32_t node = _argNode(L, method, index); - - if (!softExists(node)) { - _luaDie(L, method, "Node %d has no soft body.", node); - } - return node; + return _argNodeWith(L, method, index, softExists, "soft body"); } // A node carrying a vehicle, checked. static int32_t _argVehicle(lua_State *L, const char *method, int32_t index) { - int32_t node = _argNode(L, method, index); - - if (!vehicleExists(node)) { - _luaDie(L, method, "Node %d has no vehicle.", node); - } - return node; + return _argNodeWith(L, method, index, vehicleExists, "vehicle"); } @@ -1325,17 +1349,12 @@ static Vec3T _argVec3(lua_State *L, const char *method, int32_t index) { } - // A view handle from viewNew. static int32_t _argView(lua_State *L, const char *method, int32_t index) { - int32_t view = _argInteger(L, method, index); - - if (!viewValid(view)) { - _luaDie(L, method, "Invalid view: %d", view); - } - return view; + return _argHandle(L, method, index, viewValid, "view"); } + static VideoT *_argVideo(lua_State *L, const char *method, int32_t index) { int32_t id = _argInteger(L, method, index); VideoT *video = NULL; @@ -1357,7 +1376,7 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) { int64_t valueNumber = 0; ConfigT *c = NULL; - // Start with the given config, but every entry brings its own video and container. + // Start with the given config, but every entry brings its own video, container and data directory. c = cloneConf(base); free(c->container); c->container = NULL; @@ -1365,6 +1384,8 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) { c->isFrameFile = false; free(c->videoFile); c->videoFile = NULL; + free(c->dataDir); + c->dataDir = NULL; // Update with data in the table on the top of the Lua stack. lua_pushnil(L); @@ -1458,13 +1479,6 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) { utilDie("%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile); } - // Create new data dir location based on script location. - free(c->dataDir); - c->dataDir = createDataDirFor(c); - if (!c->dataDir) { - utilDie("Unable to create data directory for %s.", c->scriptFile); - } - return c; } @@ -1496,7 +1510,8 @@ static void _callLua(const char *func, const char *sig, ...) { lua_insert(_global.luaContext, -2); handler = lua_gettop(_global.luaContext) - 1; - // Push Arguments + // Push Arguments. Room for all of them (the results reuse it) is checked once, up front. + luaL_checkstack(_global.luaContext, (int)strlen(sig) + 1, "Too many arguments"); va_start(vl, sig); while ((*sig) && (!done)) { switch (*sig++) { @@ -1507,6 +1522,7 @@ static void _callLua(const char *func, const char *sig, ...) { case 'i': // Int lua_pushinteger(_global.luaContext, va_arg(vl, int)); // Promoted type for varargs. break; + case 'b': // Boolean (passed as an int) lua_pushboolean(_global.luaContext, va_arg(vl, int32_t)); break; @@ -1524,7 +1540,6 @@ static void _callLua(const char *func, const char *sig, ...) { } if (!done) { narg++; - luaL_checkstack(_global.luaContext, 1, "Too many arguments"); } } @@ -1558,7 +1573,7 @@ static void _callLua(const char *func, const char *sig, ...) { } -// First effect track that is neither playing nor paused, or -1 when all are busy. +// First effect track that is neither playing nor paused, or SOUND_CHANNEL_NONE when all are busy. static int32_t _effectTrackFree(void) { int32_t x = 0; @@ -1568,11 +1583,10 @@ static int32_t _effectTrackFree(void) { } } - return -1; + return SOUND_CHANNEL_NONE; } -// SDL_mixer calls this from its mixing thread. Just queue the channel; the game loop reads it under the mixer lock. // The volume of a positioned sound at a distance: full within near, inverse distance beyond it, // and fading out over the last fifth before far. static float _effectGain(const EffectT *effect, float distance) { @@ -1603,6 +1617,7 @@ static void _effectReset(int32_t channel) { } +// SDL_mixer calls this from its mixing thread. Just queue the channel; the game loop reads it under the mixer lock. static void _effectStopped(void *userdata, MIX_Track *track) { (void)track; @@ -1612,6 +1627,57 @@ static void _effectStopped(void *userdata, MIX_Track *track) { } +// Liang-Barsky clip of a line to the overlay. False when none of it is on the overlay; otherwise +// the ends are moved onto it. +static bool _clipLine(int32_t *x1, int32_t *y1, int32_t *x2, int32_t *y2) { + const double dx = *x2 - *x1; + const double dy = *y2 - *y1; + double t0 = 0.0; + double t1 = 1.0; + double r = 0.0; + double p[4]; + double q[4]; + int32_t i = 0; + + // Each edge as the parametric distance to it: left, right, top, bottom. + p[0] = -dx; + q[0] = *x1; + p[1] = dx; + q[1] = (_global.overlay->w - 1) - *x1; + p[2] = -dy; + q[2] = *y1; + p[3] = dy; + q[3] = (_global.overlay->h - 1) - *y1; + for (i = 0; i < 4; i++) { + if (p[i] == 0.0) { + // Parallel to this edge: outside it means gone altogether. + if (q[i] < 0.0) { + return false; + } + continue; + } + r = q[i] / p[i]; + if (p[i] < 0.0) { + if (r > t1) { + return false; + } + t0 = SDL_max(t0, r); + } else { + if (r < t0) { + return false; + } + t1 = SDL_min(t1, r); + } + } + *x2 = (int32_t)lround(*x1 + t1 * dx); + *y2 = (int32_t)lround(*y1 + t1 * dy); + *x1 = (int32_t)lround(*x1 + t0 * dx); + *y1 = (int32_t)lround(*y1 + t0 * dy); + + return true; +} + + // Maps an SDL joystick instance ID to our controller slot, or -1. static int32_t _controllerSlot(SDL_JoystickID which) { int32_t x = 0; @@ -1671,16 +1737,31 @@ static void _deliverKey(bool down, int32_t keysym, int32_t scancode) { } -// Seeks the laserdisc, whichever kind it is. +// The disc's current frame, whichever kind it is: a frame file counts across all its segments. +// Only with a disc (videoHandle >= 0). +static int64_t _discGetFrame(void) { + if (_global.conf->isFrameFile) { + return frameFileGetFrame(_global.frameFileHandle, _global.videoHandle); + } + + return videoGetFrame(_global.videoHandle); +} + + +// Seeks the laserdisc, whichever kind it is, held within its ends (a frame file clamps inside its +// segments itself) rather than wrapping around. static void _discSeek(int64_t frame) { int64_t actualFrame = 0; + int64_t count = 0; if (_global.conf->isFrameFile) { frameFileSeek(_global.frameFileHandle, frame, &_global.videoHandle, &actualFrame); - } else { - if (_global.videoHandle >= 0) { - videoSeek(_global.videoHandle, frame); + } else if (_global.videoHandle >= 0) { + count = videoGetFrameCount(_global.videoHandle); + if (count > 0) { + frame = SDL_clamp(frame, 0, count - 1); } + videoSeek(_global.videoHandle, frame); } } @@ -1749,16 +1830,26 @@ static void _doLogos(void) { } -// Bresenham line into the overlay. The overlay must be locked by the caller. +// Bresenham line into the overlay, clipped to it first so a line from far off screen costs only +// its visible part. The overlay must be locked by the caller. static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t pixel) { - int32_t x = x1; - int32_t y = y1; - int32_t dx = abs(x2 - x1); - int32_t dy = abs(y2 - y1); - int32_t incX = (x2 >= x1) ? 1 : -1; - int32_t incY = (y2 >= y1) ? 1 : -1; + int32_t x = 0; + int32_t y = 0; + int32_t dx = 0; + int32_t dy = 0; + int32_t incX = 0; + int32_t incY = 0; int32_t balance = 0; + if (!_clipLine(&x1, &y1, &x2, &y2)) { + return; + } + x = x1; + y = y1; + dx = abs(x2 - x1); + dy = abs(y2 - y1); + incX = (x2 >= x1) ? 1 : -1; + incY = (y2 >= y1) ? 1 : -1; if (dx >= dy) { dy <<= 1; balance = dy - dx; @@ -1790,110 +1881,89 @@ static void _drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t p } -// Draws the PAUSED indicator, built from the console font on first use, centered on the target. // Draws the 2D emitters queued this frame on one layer: a textured, tinted, rotated quad per particle -// through SDL_RenderGeometry, in overlay pixels mapped onto the window target. +// through SDL_RenderGeometry, in overlay pixels mapped onto the window target. Every quad is built +// in one pass; with several frames the indices are bucketed by frame so each texture is one call. static void _drawParticles2D(ParticleLayerE layer, const SDL_FRect *target) { - int32_t emitters[64]; - int32_t n = particlesQueued2D(layer, emitters, 64); + int32_t emitters[PARTICLES_QUEUE_MAX]; + int32_t n = particlesQueued2D(layer, emitters, (int32_t)SDL_arraysize(emitters)); float scaleX = target->w / (float)_global.overlay->w; float scaleY = target->h / (float)_global.overlay->h; SDL_Vertex *vertices = NULL; + SDL_Vertex *v = NULL; int32_t *indices = NULL; + int32_t *starts = NULL; ParticleTexturesT *cache = NULL; + ParticleViewT *particle = NULL; EmitterViewT view; - ParticleViewT *particle; - SDL_FColor colour; - float c; - float s; - float half; - int32_t e; - int32_t i; - int32_t f; - int32_t frame; - int32_t run; + SDL_FColor color; + float c = 0.0f; + float s = 0.0f; + float half = 0.0f; + float dx = 0.0f; + float dy = 0.0f; + int32_t begin = 0; + int32_t count = 0; + int32_t e = 0; + int32_t i = 0; + int32_t f = 0; + int32_t frame = 0; for (e = 0; e < n; e++) { - if (!particlesViewEmitter(emitters[e], &view) || (view.count == 0)) { + if (!particlesViewEmitter(emitters[e], &view) || (view.count == 0) || (view.frameCount == 0)) { continue; } - // Textures for the emitter's frames, made or remade when the frames change. - HASH_FIND_INT(_global.particleTextures, &view.id, cache); - if ((cache != NULL) && (cache->version != view.textureVersion)) { - _particleTexturesFree(view.id); - cache = NULL; + cache = _particleTexturesGet(&view); + if (cache->blend != view.blend) { + for (f = 0; f < cache->count; f++) { + SDL_SetTextureBlendMode(cache->textures[f], (view.blend == PARTICLE_ADD) ? SDL_BLENDMODE_ADD : SDL_BLENDMODE_BLEND); + } + cache->blend = view.blend; } - if (cache == NULL) { - cache = SDL_calloc(1, sizeof(ParticleTexturesT)); - if (cache == NULL) { - utilDie("Out of memory for particle textures."); + _quadScratch(view.count, cache->count); + vertices = _global.quadVertices; + indices = _global.quadIndices; + starts = _global.frameStarts; + memset(starts, 0, (size_t)cache->count * sizeof(int32_t)); + for (i = 0; i < view.count; i++) { + particle = &view.particles[i]; + v = &vertices[i * QUAD_VERTICES]; + c = SDL_cosf(DEGREES_TO_RADIANS(particle->angle)); + s = SDL_sinf(DEGREES_TO_RADIANS(particle->angle)); + half = particle->size * 0.5f; + color.r = particle->colour[0]; + color.g = particle->colour[1]; + color.b = particle->colour[2]; + color.a = particle->colour[3]; + for (f = 0; f < QUAD_VERTICES; f++) { + dx = ((f == 1) || (f == 2)) ? half : -half; + dy = (f >= 2) ? half : -half; + v[f].position.x = target->x + (particle->position.x + dx * c - dy * s) * scaleX; + v[f].position.y = target->y + (particle->position.y + dx * s + dy * c) * scaleY; + v[f].color = color; + v[f].tex_coord.x = ((f == 1) || (f == 2)) ? 1.0f : 0.0f; + v[f].tex_coord.y = (f >= 2) ? 1.0f : 0.0f; } - cache->id = view.id; - cache->version = view.textureVersion; - cache->count = view.frameCount; - cache->textures = SDL_calloc((size_t)view.frameCount, sizeof(SDL_Texture *)); - if (cache->textures == NULL) { - utilDie("Out of memory for particle textures."); - } - for (f = 0; f < view.frameCount; f++) { - cache->textures[f] = SDL_CreateTextureFromSurface(_global.renderer, view.frames[f]); - if (cache->textures[f] == NULL) { - utilDie("%s", SDL_GetError()); - } - SDL_SetTextureScaleMode(cache->textures[f], SDL_SCALEMODE_LINEAR); - } - HASH_ADD_INT(_global.particleTextures, id, cache); + starts[SDL_clamp(particle->frame, 0, cache->count - 1)]++; } + // Each frame's quads start where the earlier frames' end; filling advances each start to its end. + begin = 0; for (f = 0; f < cache->count; f++) { - SDL_SetTextureBlendMode(cache->textures[f], (view.blend == PARTICLE_ADD) ? SDL_BLENDMODE_ADD : SDL_BLENDMODE_BLEND); + count = starts[f]; + starts[f] = begin; + begin += count; } - vertices = SDL_malloc((size_t)view.count * 4 * sizeof(SDL_Vertex)); - indices = SDL_malloc((size_t)view.count * 6 * sizeof(int32_t)); - if ((vertices == NULL) || (indices == NULL)) { - utilDie("Out of memory drawing particles."); + for (i = 0; i < view.count; i++) { + frame = SDL_clamp(view.particles[i].frame, 0, cache->count - 1); + _quadIndex(indices, starts[frame]++, i); } - // One geometry call per run of particles sharing a frame texture. - for (frame = 0; frame < cache->count; frame++) { - run = 0; - for (i = 0; i < view.count; i++) { - particle = &view.particles[i]; - if ((particle->frame != frame) && (cache->count > 1)) { - continue; - } - c = SDL_cosf(particle->angle * (SDL_PI_F / 180.0f)); - s = SDL_sinf(particle->angle * (SDL_PI_F / 180.0f)); - half = particle->size * 0.5f; - colour.r = particle->colour[0]; - colour.g = particle->colour[1]; - colour.b = particle->colour[2]; - colour.a = particle->colour[3]; - for (f = 0; f < 4; f++) { - float dx = ((f == 1) || (f == 2)) ? half : -half; - float dy = (f >= 2) ? half : -half; - - vertices[run * 4 + f].position.x = target->x + (particle->position.x + dx * c - dy * s) * scaleX; - vertices[run * 4 + f].position.y = target->y + (particle->position.y + dx * s + dy * c) * scaleY; - vertices[run * 4 + f].color = colour; - vertices[run * 4 + f].tex_coord.x = ((f == 1) || (f == 2)) ? 1.0f : 0.0f; - vertices[run * 4 + f].tex_coord.y = (f >= 2) ? 1.0f : 0.0f; - } - indices[run * 6 + 0] = run * 4 + 0; - indices[run * 6 + 1] = run * 4 + 1; - indices[run * 6 + 2] = run * 4 + 2; - indices[run * 6 + 3] = run * 4 + 0; - indices[run * 6 + 4] = run * 4 + 2; - indices[run * 6 + 5] = run * 4 + 3; - run++; - } - if (run > 0) { - SDL_RenderGeometry(_global.renderer, cache->textures[frame], vertices, run * 4, indices, run * 6); - } - if (cache->count == 1) { - break; + begin = 0; + for (f = 0; f < cache->count; f++) { + if (starts[f] > begin) { + SDL_RenderGeometry(_global.renderer, cache->textures[f], vertices, view.count * QUAD_VERTICES, indices + begin * QUAD_INDICES, (starts[f] - begin) * QUAD_INDICES); } + begin = starts[f]; } - SDL_free(vertices); - SDL_free(indices); _drawTrails2D(&view, cache, target, scaleX, scaleY); } } @@ -1902,70 +1972,73 @@ static void _drawParticles2D(ParticleLayerE layer, const SDL_FRect *target) { // The 2D emitter's trails: a ribbon of quads through each particle's recorded points, fading // toward the tail, textured by the middle column of the first frame so the disc's edge softens it. static void _drawTrails2D(const EmitterViewT *view, const ParticleTexturesT *cache, const SDL_FRect *target, float scaleX, float scaleY) { - SDL_Vertex *vertices; - int32_t *indices; - int32_t segments = 0; - int32_t i; - int32_t j; - float half = view->trailWidth * 0.5f; + SDL_Vertex *vertices = NULL; + SDL_Vertex *v = NULL; + int32_t *indices = NULL; + const float *points = NULL; + SDL_FColor color; + int32_t segments = 0; + int32_t count = 0; + int32_t i = 0; + int32_t j = 0; + int32_t k = 0; + float half = view->trailWidth * 0.5f; + float x0 = 0.0f; + float y0 = 0.0f; + float x1 = 0.0f; + float y1 = 0.0f; + float dx = 0.0f; + float dy = 0.0f; + float length = 0.0f; + float nx = 0.0f; + float ny = 0.0f; + float side = 0.0f; + bool head = false; if ((view->trailLength < 2) || (view->trailCounts == NULL) || (view->count == 0)) { return; } - vertices = SDL_malloc((size_t)view->count * (size_t)(view->trailLength - 1) * 4 * sizeof(SDL_Vertex)); - indices = SDL_malloc((size_t)view->count * (size_t)(view->trailLength - 1) * 6 * sizeof(int32_t)); - if ((vertices == NULL) || (indices == NULL)) { - utilDie("Out of memory drawing particle trails."); - } + _quadScratch(view->count * (view->trailLength - 1), 0); + vertices = _global.quadVertices; + indices = _global.quadIndices; for (i = 0; i < view->count; i++) { - const float *points = view->trailPoints + (size_t)i * (size_t)view->trailLength * 3; - int32_t count = view->trailCounts[i]; - SDL_FColor colour; - - colour.r = view->particles[i].colour[0]; - colour.g = view->particles[i].colour[1]; - colour.b = view->particles[i].colour[2]; + points = view->trailPoints + (size_t)i * (size_t)view->trailLength * TRAIL_POINT_FLOATS; + count = view->trailCounts[i]; + color.r = view->particles[i].colour[0]; + color.g = view->particles[i].colour[1]; + color.b = view->particles[i].colour[2]; for (j = 1; j < count; j++) { - float x0 = view->trailOffset.x + points[(j - 1) * 3]; - float y0 = view->trailOffset.y + points[(j - 1) * 3 + 1]; - float x1 = view->trailOffset.x + points[j * 3]; - float y1 = view->trailOffset.y + points[j * 3 + 1]; - float dx = x1 - x0; - float dy = y1 - y0; - float length = SDL_sqrtf(dx * dx + dy * dy); - float nx = (length > 0.0f) ? -dy / length * half : 0.0f; - float ny = (length > 0.0f) ? dx / length * half : 0.0f; - SDL_Vertex *v = &vertices[segments * 4]; - int32_t k; - - for (k = 0; k < 4; k++) { - bool head = (k == 1) || (k == 2); - float side = (k >= 2) ? 1.0f : -1.0f; - + x0 = view->trailOffset.x + points[(j - 1) * TRAIL_POINT_FLOATS]; + y0 = view->trailOffset.y + points[(j - 1) * TRAIL_POINT_FLOATS + 1]; + x1 = view->trailOffset.x + points[j * TRAIL_POINT_FLOATS]; + y1 = view->trailOffset.y + points[j * TRAIL_POINT_FLOATS + 1]; + dx = x1 - x0; + dy = y1 - y0; + length = SDL_sqrtf(dx * dx + dy * dy); + nx = (length > 0.0f) ? -dy / length * half : 0.0f; + ny = (length > 0.0f) ? dx / length * half : 0.0f; + v = &vertices[segments * QUAD_VERTICES]; + for (k = 0; k < QUAD_VERTICES; k++) { + head = (k == 1) || (k == 2); + side = (k >= 2) ? 1.0f : -1.0f; v[k].position.x = target->x + ((head ? x1 : x0) + nx * side) * scaleX; v[k].position.y = target->y + ((head ? y1 : y0) + ny * side) * scaleY; - v[k].color = colour; + v[k].color = color; v[k].color.a = view->particles[i].colour[3] * (float)(head ? j : j - 1) / (float)(count - 1); v[k].tex_coord.x = 0.5f; v[k].tex_coord.y = (side < 0.0f) ? 0.0f : 1.0f; } - indices[segments * 6 + 0] = segments * 4 + 0; - indices[segments * 6 + 1] = segments * 4 + 1; - indices[segments * 6 + 2] = segments * 4 + 2; - indices[segments * 6 + 3] = segments * 4 + 0; - indices[segments * 6 + 4] = segments * 4 + 2; - indices[segments * 6 + 5] = segments * 4 + 3; + _quadIndex(indices, segments, segments); segments++; } } if (segments > 0) { - SDL_RenderGeometry(_global.renderer, cache->textures[0], vertices, segments * 4, indices, segments * 6); + SDL_RenderGeometry(_global.renderer, cache->textures[0], vertices, segments * QUAD_VERTICES, indices, segments * QUAD_INDICES); } - SDL_free(vertices); - SDL_free(indices); } +// Draws the PAUSED indicator, built from the console font on first use, centered on the target. static void _drawPauseIndicator(const SDL_FRect *target) { SDL_Surface *text = NULL; SDL_Rect src; @@ -2119,8 +2192,8 @@ static void _heldListUpdate(HeldKeyT *list, int32_t *count, bool down, int32_t k static void _installFileHooks(lua_State *L) { static const struct { const char *name; - int32_t mode; // 0 read, 1 io.open (mode string decides), 2 write - } ioHooks[] = { { "input", 0 }, { "lines", 0 }, { "open", 1 }, { "output", 2 } }; + IoHookModeE mode; + } ioHooks[] = { { "input", IO_HOOK_READ }, { "lines", IO_HOOK_READ }, { "open", IO_HOOK_OPEN }, { "output", IO_HOOK_WRITE } }; size_t i = 0; lua_getglobal(L, "dofile"); @@ -2150,10 +2223,7 @@ static int32_t _lfsAttributes(lua_State *L) { bool directory = false; if (vfsIsFilesystem(name)) { - lua_pushvalue(L, lua_upvalueindex(1)); - lua_insert(L, 1); - lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); - return lua_gettop(L); + return _luaCallOriginal(L); } directory = vfsIsDirectory(name); if (!directory && !vfsStat(name, &size, &modified)) { @@ -2189,10 +2259,7 @@ static int32_t _lfsDir(lua_State *L) { int32_t i = 0; if (vfsIsFilesystem(name)) { - lua_pushvalue(L, lua_upvalueindex(1)); - lua_insert(L, 1); - lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); - return lua_gettop(L); + return _luaCallOriginal(L); } if (!vfsIsDirectory(name)) { return luaL_error(L, "cannot open %s: No such file or directory", name); @@ -2232,13 +2299,15 @@ static int32_t _lfsMkdir(lua_State *L) { bool ok = false; if (vfsIsFilesystem(name)) { - lua_pushvalue(L, lua_upvalueindex(1)); - lua_insert(L, 1); - lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); - return lua_gettop(L); + return _luaCallOriginal(L); } path = vfsFilePath(name, true); - ok = utilMkDirP(path, 0755); + if (path == NULL) { + lua_pushnil(L); + lua_pushfstring(L, "%s reaches outside the game", name); + return 2; + } + ok = utilMkDirP(path, DIRECTORY_MODE); free(path); if (!ok) { lua_pushnil(L); @@ -2259,13 +2328,15 @@ static int32_t _lfsRmdir(lua_State *L) { bool ok = false; if (vfsIsFilesystem(name)) { - lua_pushvalue(L, lua_upvalueindex(1)); - lua_insert(L, 1); - lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); - return lua_gettop(L); + return _luaCallOriginal(L); } path = vfsFilePath(name, true); - ok = (rmdir(path) == 0); + if (path == NULL) { + lua_pushnil(L); + lua_pushfstring(L, "%s reaches outside the game", name); + return 2; + } + ok = (rmdir(path) == 0); free(path); if (!ok) { lua_pushnil(L); @@ -2300,18 +2371,103 @@ static int32_t _loadAudioCalibration(void) { } +// The built-in controls.cfg, then every override in turn (the working directory, above and inside +// the data directory, beside the script), each place only once, in a throwaway Lua state that has +// the framework but not the API. Leaves the dead zone and the switch mappings behind. +static void _loadControlMappings(void) { + lua_State *L = NULL; + char *scriptDir = utilGetUpToLastPathComponent(_global.conf->scriptFile); + char *candidates[4]; + bool seen = false; + int32_t c = 0; + int32_t x = 0; + int32_t y = 0; + + _progTrace("Creating Lua context for Singe setup"); + L = luaL_newstate(); + _global.luaContext = L; + _startLuaContext(L); + // Load framework - NOTE! SINGE API NOT AVAILABLE AT THIS POINT! + // Any calls in the framework need to be wrapped with nil checks! + _progTrace("Loading Singe framework"); + if (luaL_loadbuffer(L, (const char *)Framework_singe, Framework_singe_len, "Framework.singe") || lua_pcall(L, 0, 0, 0)) { + utilDie("%s", lua_tostring(L, -1)); + } + _progTrace("Loading default control mappings"); + if (luaL_loadbuffer(L, (const char *)controls_cfg, controls_cfg_len, CONTROLS_FILE) || lua_pcall(L, 0, 0, 0)) { + utilDie("%s", lua_tostring(L, -1)); + } + candidates[0] = strdup(CONTROLS_FILE); + candidates[1] = utilCreateString("%s..%c%s", _global.conf->dataDir, utilGetPathSeparator(), CONTROLS_FILE); + candidates[2] = utilCreateString("%s%s", _global.conf->dataDir, CONTROLS_FILE); + candidates[3] = utilCreateString("%s%s", scriptDir, CONTROLS_FILE); + for (c = 0; c < (int32_t)SDL_arraysize(candidates); c++) { + // Without -d the data directory is the script's, so the same file would run twice. + seen = false; + for (x = 0; x < c; x++) { + if (strcmp(candidates[x], candidates[c]) == 0) { + seen = true; + } + } + if (!seen) { + _loadControlsFile(candidates[c]); + } + } + for (c = 0; c < (int32_t)SDL_arraysize(candidates); c++) { + free(candidates[c]); + } + free(scriptDir); + // Parse results + lua_getglobal(L, "DEAD_ZONE"); + if (lua_isnumber(L, -1)) { + _global.controllerDeadZone = (int32_t)lua_tonumber(L, -1); + } + lua_pop(L, 1); + _progTrace("Controller dead zone is %d", _global.controllerDeadZone); + for (x = 0; x < INPUT_COUNT; x++) { + // Each INPUT_* table holds { name = ..., value = ... } entries; collect the values. + lua_getglobal(L, _inputNames[x].configName); + if (!lua_istable(L, -1)) { + utilSay("Configuration option %s missing!", _inputNames[x].configName); + lua_pop(L, 1); + continue; + } + y = (int32_t)lua_rawlen(L, -1); + _global.controlMappings[x].input = (int32_t *)calloc((size_t)(y + 1), sizeof(int32_t)); + if (!_global.controlMappings[x].input) { + utilDie("Unable to allocate memory for control mappings."); + } + _global.controlMappings[x].inputCount = 0; + lua_pushnil(L); + while (lua_next(L, -2)) { + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "value"); + if (lua_isnumber(L, -1) && (_global.controlMappings[x].inputCount < y)) { + _global.controlMappings[x].input[_global.controlMappings[x].inputCount++] = (int32_t)lua_tonumber(L, -1); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + lua_close(L); + _global.luaContext = NULL; +} + + // Runs a controls.cfg if it exists. static void _loadControlsFile(const char *path) { if (vfsExists(path)) { _progTrace("Loading %s", path); - if (_luaLoadFile(_global.luaContext, path, NULL) || lua_pcall(_global.luaContext, 0, 0, 0)) { + if (_luaLoadFile(_global.luaContext, path, NULL, _global.conf->reload) || lua_pcall(_global.luaContext, 0, 0, 0)) { utilDie("%s", lua_tostring(_global.luaContext, -1)); } } } -static SDL_Surface *_loadEmbeddedPng(const unsigned char *data, unsigned int length) { +static SDL_Surface *_loadEmbeddedPng(const uint8_t *data, size_t length) { SDL_Surface *surface = IMG_LoadTyped_IO(SDL_IOFromConstMem(data, length), true, "PNG"); if (surface == NULL) { @@ -2324,7 +2480,7 @@ static SDL_Surface *_loadEmbeddedPng(const unsigned char *data, unsigned int len // Loads an embedded PNG as a texture. The surface stays alive so callers can read its size. -static SDL_Texture *_loadEmbeddedTexture(const unsigned char *data, unsigned int length, SDL_Surface **surface) { +static SDL_Texture *_loadEmbeddedTexture(const uint8_t *data, size_t length, SDL_Surface **surface) { SDL_Texture *texture = NULL; *surface = _loadEmbeddedPng(data, length); @@ -2337,6 +2493,16 @@ static SDL_Texture *_loadEmbeddedTexture(const unsigned char *data, unsigned int } +// Hands a hooked call to the original function kept as upvalue 1, returning all it returns. +static int32_t _luaCallOriginal(lua_State *L) { + lua_pushvalue(L, lua_upvalueindex(1)); + lua_insert(L, 1); + lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); + + return lua_gettop(L); +} + + // Reports a script level error with the calling Lua line and exits. static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) { va_list args; @@ -2352,19 +2518,15 @@ static void _luaDie(lua_State *L, const char *method, const char *fmt, ...) { } -// Formats "line:method: message" for tracing and errors. Caller frees. // dofile(name) through the vfs; without a name the original reads stdin. static int32_t _luaDofile(lua_State *L) { const char *name = luaL_optstring(L, 1, NULL); if (name == NULL) { - lua_pushvalue(L, lua_upvalueindex(1)); - lua_insert(L, 1); - lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); - return lua_gettop(L); + return _luaCallOriginal(L); } lua_settop(L, 1); - if (_luaLoadFile(L, name, NULL) != LUA_OK) { + if (_luaLoadFile(L, name, NULL, _global.conf->reload) != LUA_OK) { return lua_error(L); } lua_call(L, 0, LUA_MULTRET); @@ -2396,7 +2558,7 @@ static int32_t _luaFileSearcher(lua_State *L) { for (x = 0; patterns[x] != NULL; x++) { name = utilCreateString(patterns[x], prefixes[y], module); if (vfsExists(name)) { - if (_luaLoadFile(L, name, NULL) != LUA_OK) { + if (_luaLoadFile(L, name, NULL, _global.conf->reload) != LUA_OK) { lua_pushfstring(L, "error loading module '%s' from file '%s':\n\t%s", lua_tostring(L, 1), name, lua_tostring(L, -1)); free(name); free(module); @@ -2420,6 +2582,7 @@ static int32_t _luaFileSearcher(lua_State *L) { } +// Formats "line:method: message" for tracing and errors. Caller frees. static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_list args) { lua_Debug ar; int32_t line = 0; @@ -2443,30 +2606,34 @@ static char *_luaFormat(lua_State *L, const char *method, const char *fmt, va_li } -// Lua panic handler: something went wrong outside a protected call. // io.open and friends with a name resolved through the vfs. Upvalues: the original, and how the name is used. static int32_t _luaIoHook(lua_State *L) { - int32_t mode = (int32_t)lua_tointeger(L, lua_upvalueindex(2)); + IoHookModeE mode = (IoHookModeE)lua_tointeger(L, lua_upvalueindex(2)); const char *modeString = NULL; char *path = NULL; - bool writing = (mode == 2); - int32_t top = lua_gettop(L); + bool writing = (mode == IO_HOOK_WRITE); if (lua_type(L, 1) == LUA_TSTRING) { - if (mode == 1) { + if (mode == IO_HOOK_OPEN) { modeString = luaL_optstring(L, 2, "r"); writing = (strpbrk(modeString, "wa+") != NULL); } path = vfsFilePath(lua_tostring(L, 1), writing); + if (path == NULL) { + // A name inside a packed game that resolves nowhere: io.open reports it the Lua way, the others raise. + if (mode != IO_HOOK_OPEN) { + return luaL_error(L, "%s reaches outside the game", lua_tostring(L, 1)); + } + lua_pushnil(L); + lua_pushfstring(L, "%s reaches outside the game", lua_tostring(L, 1)); + return 2; + } lua_pushstring(L, path); lua_replace(L, 1); free(path); } - lua_pushvalue(L, lua_upvalueindex(1)); - lua_insert(L, 1); - lua_call(L, top, LUA_MULTRET); - return lua_gettop(L); + return _luaCallOriginal(L); } @@ -2476,12 +2643,9 @@ static int32_t _luaLoadfile(lua_State *L) { const char *mode = luaL_optstring(L, 2, NULL); if (name == NULL) { - lua_pushvalue(L, lua_upvalueindex(1)); - lua_insert(L, 1); - lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); - return lua_gettop(L); + return _luaCallOriginal(L); } - if (_luaLoadFile(L, name, mode) != LUA_OK) { + if (_luaLoadFile(L, name, mode, _global.conf->reload) != LUA_OK) { lua_pushnil(L); lua_insert(L, -2); return 2; @@ -2497,8 +2661,9 @@ static int32_t _luaLoadfile(lua_State *L) { } -// Loads a chunk from the vfs, leaving the function or an error message on the stack. Returns the Lua status. -static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode) { +// Loads a chunk from the vfs, leaving the function or an error message on the stack. Returns the +// Lua status. With watch, a loose file joins the list --reload checks. +static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode, bool watch) { char *data = NULL; char *chunkName = NULL; size_t bytes = 0; @@ -2513,13 +2678,14 @@ static int32_t _luaLoadFile(lua_State *L, const char *name, const char *mode) { status = luaL_loadbufferx(L, data, bytes, chunkName, mode); free(chunkName); free(data); - if ((status == LUA_OK) && _global.conf->reload) { + if ((status == LUA_OK) && watch) { _watchFile(name); } return status; } + // A fresh Lua state with the standard libraries, the vfs hooks, the constants and the whole API. static void _createScriptContext(void) { _progTrace("Creating Lua context for script"); @@ -2528,6 +2694,7 @@ static void _createScriptContext(void) { _registerApi(_global.luaContext); } + // require("lfs") with dir and attributes routed through the vfs, so a packed game can list itself. static int32_t _luaopenLfs(lua_State *L) { luaopen_lfs(L); @@ -2551,6 +2718,7 @@ static int32_t _luaopenLfs(lua_State *L) { } +// Lua panic handler: something went wrong outside a protected call. static int32_t _luaPanic(lua_State *L) { lua_Debug ar; int32_t level = 0; @@ -2626,6 +2794,36 @@ static int32_t _luaTraceback(lua_State *L) { } +// materialSetXxxMap(material[, image[, strength]]): a loaded sprite's surface or a KTX2 file as one +// of the material's maps; nil (or nothing) clears it. Strength is only for the normal and +// occlusion maps. +static int32_t _materialSetMap(lua_State *L, const char *method, MaterialMapE map, bool hasStrength) { + int32_t material = 0; + SDL_Surface *surface = NULL; + Ktx2ImageT ktx2; + bool ok = false; + float strength = 1.0f; + + _argCheck(L, method, 1, hasStrength ? 3 : 2); + material = _argMaterial(L, method, 1); + if (hasStrength && (lua_gettop(L) >= 3)) { + strength = (float)_argNumber(L, method, 3); + } + if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2) && _argMapImage(L, method, 2, &surface, &ktx2)) { + ok = materialSetMap(material, map, &ktx2, strength); + ktx2Free(&ktx2); + if (!ok) { + _luaDie(L, method, "Unable to upload the texture."); + } + return 0; + } + if (!materialSetMapSurface(material, map, surface, strength)) { + _luaDie(L, method, "%s", SDL_GetError()); + } + return 0; +} + + // Converts the script visible 0..AUDIO_MAX_VOLUME scale to the mixer's scale. static float _mixerGain(int32_t effectsVolume) { return (float)effectsVolume / (float)AUDIO_MAX_VOLUME; @@ -2644,7 +2842,6 @@ static int32_t _mouseCode(int32_t device, int32_t button) { } - // Replaces the overlay surface and texture (and the scene's targets) at a new size; its contents are lost. static void _overlayResize(int32_t width, int32_t height) { SDL_DestroySurface(_global.overlay); @@ -2665,6 +2862,7 @@ static void _overlayResize(int32_t width, int32_t height) { _overlayTouched(); } + // Every overlay drawing call ends here so the texture is only re-uploaded when needed. static void _overlayTouched(void) { _global.overlayDirty = true; @@ -2694,7 +2892,7 @@ static void _pauseAllVideos(bool pause) { // Routes a key, button, or axis direction code: engine switches first, then the script. static void _processKey(bool down, int32_t keysym, int32_t scancode) { InputE engine = INPUT_COUNT; - bool keyboard = (scancode < CODE_GAMEPAD_BASE); // Real scancodes never reach the gamepad range + bool keyboard = (scancode < CODE_GAMEPAD_BASE); // Every defined scancode is below the gamepad range // Physical state is tracked even while the game is frozen. if (down) { @@ -2702,7 +2900,7 @@ static void _processKey(bool down, int32_t keysym, int32_t scancode) { } else { _global.keyboardLastUp = scancode; } - if ((scancode >= 0) && (scancode < SDL_SCANCODE_COUNT)) { + if (keyboard && (scancode >= 0) && (scancode < SDL_SCANCODE_COUNT)) { _global.keyboardState[scancode] = down; } _heldListUpdate(_global.physicalKeys, &_global.physicalKeyCount, down, keysym, scancode); @@ -2771,30 +2969,119 @@ static int32_t _pushVec3(lua_State *L, Vec3T v) { } -// Hands the step's contacts and trigger overlaps to the script: onCollision(nodeA, nodeB, x, y, z, -// speed) and onTrigger(trigger, other, entered), when the script defines them. +// Writes the two triangles of quad number quad (corners quad * 4 onwards) at index slot. +static void _quadIndex(int32_t *indices, int32_t slot, int32_t quad) { + int32_t *out = indices + slot * QUAD_INDICES; + int32_t first = quad * QUAD_VERTICES; + + out[0] = first + 0; + out[1] = first + 1; + out[2] = first + 2; + out[3] = first + 0; + out[4] = first + 2; + out[5] = first + 3; +} + + +// Vertex and index room for this many textured quads, and start slots for this many frames, +// kept between frames and grown only when a bigger emitter comes along. +static void _quadScratch(int32_t quads, int32_t frames) { + if (quads > _global.quadCapacity) { + _global.quadVertices = SDL_realloc(_global.quadVertices, (size_t)quads * QUAD_VERTICES * sizeof(SDL_Vertex)); + _global.quadIndices = SDL_realloc(_global.quadIndices, (size_t)quads * QUAD_INDICES * sizeof(int32_t)); + if ((_global.quadVertices == NULL) || (_global.quadIndices == NULL)) { + utilDie("Out of memory drawing particles."); + } + _global.quadCapacity = quads; + } + if (frames > _global.frameStartCapacity) { + _global.frameStarts = SDL_realloc(_global.frameStarts, (size_t)frames * sizeof(int32_t)); + if (_global.frameStarts == NULL) { + utilDie("Out of memory drawing particles."); + } + _global.frameStartCapacity = frames; + } +} + + // Frees the renderer textures kept for an emitter's 2D drawing. +static void _particleTexturesDestroy(ParticleTexturesT *cache) { + int32_t f = 0; + + HASH_DEL(_global.particleTextures, cache); + for (f = 0; f < cache->count; f++) { + SDL_DestroyTexture(cache->textures[f]); + } + SDL_free(cache->textures); + SDL_free(cache); +} + + static void _particleTexturesFree(int32_t emitter) { ParticleTexturesT *cache = NULL; - int32_t f; HASH_FIND_INT(_global.particleTextures, &emitter, cache); if (cache != NULL) { - HASH_DEL(_global.particleTextures, cache); - for (f = 0; f < cache->count; f++) { - SDL_DestroyTexture(cache->textures[f]); - } - SDL_free(cache->textures); - SDL_free(cache); + _particleTexturesDestroy(cache); } } +// Every emitter's textures; the emitters themselves go with particlesQuit. +static void _particleTexturesFreeAll(void) { + ParticleTexturesT *cache = NULL; + ParticleTexturesT *temp = NULL; + + HASH_ITER(hh, _global.particleTextures, cache, temp) { + _particleTexturesDestroy(cache); + } +} + + +// The textures for an emitter's frames, made on first use and remade when the frames change. +static ParticleTexturesT *_particleTexturesGet(const EmitterViewT *view) { + ParticleTexturesT *cache = NULL; + int32_t f = 0; + + HASH_FIND_INT(_global.particleTextures, &view->id, cache); + if ((cache != NULL) && (cache->version != view->textureVersion)) { + _particleTexturesDestroy(cache); + cache = NULL; + } + if (cache != NULL) { + return cache; + } + cache = SDL_calloc(1, sizeof(ParticleTexturesT)); + if (cache == NULL) { + utilDie("Out of memory for particle textures."); + } + cache->id = view->id; + cache->version = view->textureVersion; + cache->count = view->frameCount; + cache->blend = view->blend; + cache->textures = SDL_calloc((size_t)view->frameCount, sizeof(SDL_Texture *)); + if (cache->textures == NULL) { + utilDie("Out of memory for particle textures."); + } + for (f = 0; f < view->frameCount; f++) { + cache->textures[f] = SDL_CreateTextureFromSurface(_global.renderer, view->frames[f]); + if (cache->textures[f] == NULL) { + utilDie("%s", SDL_GetError()); + } + SDL_SetTextureScaleMode(cache->textures[f], SDL_SCALEMODE_LINEAR); + SDL_SetTextureBlendMode(cache->textures[f], (view->blend == PARTICLE_ADD) ? SDL_BLENDMODE_ADD : SDL_BLENDMODE_BLEND); + } + HASH_ADD_INT(_global.particleTextures, id, cache); + + return cache; +} + + // onNavArrived(agent) for every agent that reached its target this frame. static void _navCallbacks(void) { - int32_t agents[64]; - int32_t count = navPollArrived(agents, 64); - int32_t x; + int32_t agents[NAV_ARRIVAL_QUEUE]; + int32_t count = navPollArrived(agents, (int32_t)SDL_arraysize(agents)); + int32_t x = 0; for (x = 0; x < count; x++) { _callLua("onNavArrived", "i", agents[x]); @@ -2802,25 +3089,28 @@ static void _navCallbacks(void) { } +// Hands the step's contacts and trigger overlaps to the script: onCollision(nodeA, nodeB, x, y, z, +// speed) and onTrigger(trigger, other, entered), when the script defines them. Physics keeps what +// one batch cannot hold, so the queue is drained until empty. static void _physicsCallbacks(void) { - PhysicsEventT events[64]; - int32_t count; - int32_t x; + PhysicsEventT events[PHYSICS_MAX_EVENTS]; + PhysicsEventT *event = NULL; + int32_t count = 0; + int32_t x = 0; - count = physicsGetEvents(events, (int32_t)SDL_arraysize(events)); - for (x = 0; x < count; x++) { - PhysicsEventT *event = &events[x]; - - if (event->type == PHYSICS_EVENT_COLLISION) { - _callLua("onCollision", "iidddd", event->nodeA, event->nodeB, (double)event->point.x, (double)event->point.y, (double)event->point.z, (double)event->speed); - } else { - _callLua("onTrigger", "iib", event->nodeA, event->nodeB, (event->type == PHYSICS_EVENT_ENTER) ? 1 : 0); + while ((count = physicsGetEvents(events, (int32_t)SDL_arraysize(events))) > 0) { + for (x = 0; x < count; x++) { + event = &events[x]; + if (event->type == PHYSICS_EVENT_COLLISION) { + _callLua("onCollision", "iidddd", event->nodeA, event->nodeB, (double)event->point.x, (double)event->point.y, (double)event->point.z, (double)event->speed); + } else { + _callLua("onTrigger", "iib", event->nodeA, event->nodeB, (event->type == PHYSICS_EVENT_ENTER) ? 1 : 0); + } } } } - // Every Singe call a script may make. Comments give the version each call appeared in. static void _registerApi(lua_State *L) { lua_register(L, "animationGetTime", apiAnimationGetTime); // 3.00 @@ -2849,13 +3139,12 @@ static void _registerApi(lua_State *L) { lua_register(L, "bodySetMass", apiBodySetMass); // 3.00 lua_register(L, "bodySetTrigger", apiBodySetTrigger); // 3.00 lua_register(L, "bodySetVelocity", apiBodySetVelocity); // 3.00 - lua_register(L, "colorBackground", apiColorBackground); // 1.xx - lua_register(L, "colorForeground", apiColorForeground); // 1.xx - lua_register(L, "bodySetWater", apiBodySetWater); // 3.00 lua_register(L, "cameraSet", apiCameraSet); // 3.00 lua_register(L, "cameraSetOrthographic", apiCameraSetOrthographic); // 3.00 lua_register(L, "cameraSetPerspective", apiCameraSetPerspective); // 3.00 + lua_register(L, "colorBackground", apiColorBackground); // 1.xx + lua_register(L, "colorForeground", apiColorForeground); // 1.xx lua_register(L, "controllerGetAxis", apiControllerGetAxis); // 2.00 lua_register(L, "controllerGetButton", apiControllerGetButton); // 2.10 @@ -3209,55 +3498,36 @@ static void _registerApi(lua_State *L) { // back to its default size), the engine (window, GPU device, disc, controllers) stays, and the // script runs afresh. An error in it is traced and the game sits empty until the next reload. static void _reloadScript(void) { - int32_t x; - _progTrace("Reloading %s", _global.conf->scriptFile); _global.reloadRequested = false; MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0); - for (x = 0; x < EFFECT_TRACKS; x++) { - _effectReset(x); - } - _global.soundQueueCount = 0; lua_close(_global.luaContext); _unloadScriptResources(); - modelQuit(); - navQuit(); - particlesQuit(); - physicsQuit(); - sceneQuit(); - sceneInit(_global.device, _global.renderer); - physicsInit(); - particlesInit(); - navInit(); + _subsystemsQuit(); + _subsystemsInit(); _overlayResize((int32_t)(_global.canvasWidth * OVERLAY_SCALE_DEFAULT), (int32_t)(_global.canvasHeight * OVERLAY_SCALE_DEFAULT)); - _global.colorForeground.r = SDL_ALPHA_OPAQUE; - _global.colorForeground.g = SDL_ALPHA_OPAQUE; - _global.colorForeground.b = SDL_ALPHA_OPAQUE; - _global.colorForeground.a = SDL_ALPHA_OPAQUE; - memset(&_global.colorBackground, 0, sizeof(_global.colorBackground)); - _global.fontQuality = FONT_QUALITY_SOLID; - _global.keyboardMode = KEYBOARD_NORMAL; - _global.listenerNode = LISTENER_CAMERA; - _global.pauseEnabled = true; - if (_global.pauseState) { - _setPause(false, false); - } - for (x = 0; x < _global.watchedCount; x++) { - free(_global.watched[x].name); - } - free(_global.watched); - _global.watched = NULL; - _global.watchedCount = 0; + _resetScriptState(); _createScriptContext(); + _runScript(false); + _global.refreshDisplay = true; +} + + +// Loads and runs the game script under the traceback handler. A failure is fatal when the game +// starts; on a reload it is reported and the game sits empty until the next one. +static void _runScript(bool fatal) { _progTrace("Running %s", _global.conf->scriptFile); lua_pushcfunction(_global.luaContext, _luaTraceback); - if (_luaLoadFile(_global.luaContext, _global.conf->scriptFile, NULL) || lua_pcall(_global.luaContext, 0, 0, -2)) { + if (_luaLoadFile(_global.luaContext, _global.conf->scriptFile, NULL, _global.conf->reload) || lua_pcall(_global.luaContext, 0, 0, -2)) { + if (fatal) { + utilDie("Error running script: %s", lua_tostring(_global.luaContext, -1)); + } utilSay("Error running script: %s", lua_tostring(_global.luaContext, -1)); } lua_settop(_global.luaContext, 0); - _global.refreshDisplay = true; } + // A player's current frame for the 3D scene's video materials: the disc's texture is already // updated for this frame; a loaded video is advanced here (drawing it on the overlay too is harmless). static SDL_Texture *_sceneVideoSource(int32_t player) { @@ -3276,6 +3546,25 @@ static SDL_Texture *_sceneVideoSource(int32_t player) { } +// scriptExecute and scriptPush: the games.dat style table at argument 1 as a config, with the data +// directory the launched game will write to. Caller destroys it. +static ConfigT *_scriptConfFromTable(lua_State *L, const char *method) { + ConfigT *conf = NULL; + + _argCheck(L, method, 1, 1); + if (!lua_istable(L, 1)) { + _luaDie(L, method, "Argument 1 must be a table."); + } + conf = _buildConfFromTable(L, _global.conf); + conf->dataDir = resolveDataDir(conf); + if (conf->dataDir == NULL) { + _luaDie(L, method, "Unable to create the data directory for %s.", conf->scriptFile); + } + + return conf; +} + + // Constants every script (and controls.cfg) can rely on. These are the single source of truth. static void _pushConstants(lua_State *L) { int32_t x = 0; @@ -3422,9 +3711,9 @@ static void _pushConstants(lua_State *L) { lua_pushinteger(L, SOFT_ROPE); lua_setglobal(L, "SOFT_ROPE"); - lua_pushinteger(L, -1); + lua_pushinteger(L, SOUND_CHANNEL_NONE); lua_setglobal(L, "SOUND_ERROR_INVALID"); - lua_pushinteger(L, -1); + lua_pushinteger(L, SOUND_CHANNEL_NONE); lua_setglobal(L, "SOUND_REMOVE_HANDLE"); // Input code layout so Framework.singe can build the GAMEPAD_N and MOUSE_N tables. @@ -3463,39 +3752,34 @@ static void _pushConstants(lua_State *L) { } -// Writes one overlay pixel. The overlay is always 32 bit and must be locked by the caller. +// Writes one overlay pixel. The overlay is always BGRA32, one uint32_t per pixel (_overlayResize +// makes it so), and must be locked by the caller. static void _putPixel(int32_t x, int32_t y, uint32_t pixel) { SDL_Surface *surface = _global.overlay; - uint8_t *p = NULL; + uint32_t *row = NULL; if ((x < 0) || (x >= surface->w) || (y < 0) || (y >= surface->h)) { return; } - p = (uint8_t *)surface->pixels + y * surface->pitch + x * SDL_BYTESPERPIXEL(surface->format); - memcpy(p, &pixel, sizeof(pixel)); + row = (uint32_t *)((uint8_t *)surface->pixels + (size_t)y * (size_t)surface->pitch); + row[x] = pixel; } // colorXxx(r, g, b[, a]) with components clamped to 0..255. static void _readColor(lua_State *L, const char *method, SDL_Color *color, uint8_t defaultAlpha) { - int32_t n = lua_gettop(L); - int32_t value[4] = { 0, 0, 0, defaultAlpha }; - int32_t x = 0; + int32_t n = lua_gettop(L); + uint8_t value[COLOR_COMPONENTS] = { 0, 0, 0, defaultAlpha }; + int32_t x = 0; - _argCheck(L, method, 3, 4); + _argCheck(L, method, 3, COLOR_COMPONENTS); for (x = 0; x < n; x++) { - value[x] = _argInteger(L, method, x + 1); - if (value[x] < 0) { - value[x] = 0; - } - if (value[x] > SDL_ALPHA_OPAQUE) { - value[x] = SDL_ALPHA_OPAQUE; - } + value[x] = _argColorByte(L, method, x + 1); } - color->r = (uint8_t)value[0]; - color->g = (uint8_t)value[1]; - color->b = (uint8_t)value[2]; - color->a = (uint8_t)value[3]; + color->r = value[0]; + color->g = value[1]; + color->b = value[2]; + color->a = value[3]; _luaTrace(L, method, "%d %d %d %d", color->r, color->g, color->b, color->a); } @@ -3509,6 +3793,49 @@ static void _releaseAxis(int32_t axisIndex) { } +// Everything a script sets about itself, back to what a script may expect at its start: colours, +// font quality, keyboard mode, listener, pause, the effect channels and their completion queue, +// what the previous script believed was held, and --reload's file list. Keys physically down now +// were held over from before this script, so they are ignored until released. Nothing here calls +// into Lua, so it is safe between one state closing and the next opening. +static void _resetScriptState(void) { + int32_t finished[SOUND_QUEUE_SIZE]; + int32_t x = 0; + + for (x = 0; x < EFFECT_TRACKS; x++) { + _effectReset(x); + } + _soundQueueDrain(finished); + _global.colorForeground.r = SDL_ALPHA_OPAQUE; + _global.colorForeground.g = SDL_ALPHA_OPAQUE; + _global.colorForeground.b = SDL_ALPHA_OPAQUE; + _global.colorForeground.a = SDL_ALPHA_OPAQUE; + memset(&_global.colorBackground, 0, sizeof(_global.colorBackground)); + _global.fontQuality = FONT_QUALITY_SOLID; + _global.keyboardMode = KEYBOARD_NORMAL; + _global.listenerNode = LISTENER_CAMERA; + _global.pauseEnabled = true; + _global.frozen = false; + if (_global.pauseState) { + _global.pauseState = false; + _updatePauseState(); + } + memset(_global.switchHeld, 0, sizeof(_global.switchHeld)); + memset(_global.axisCode, 0, sizeof(_global.axisCode)); + _global.heldKeyCount = 0; + _global.physicalKeyCount = 0; + _global.keyboardLastDown = SDL_SCANCODE_UNKNOWN; + _global.keyboardLastUp = SDL_SCANCODE_UNKNOWN; + _suppressHeldInput(); + for (x = 0; x < _global.watchedCount; x++) { + free(_global.watched[x].name); + } + free(_global.watched); + _global.watched = NULL; + _global.watchedCount = 0; +} + + // Renders text with the current font, quality, and colors. static SDL_Surface *_renderText(lua_State *L, const char *method, const char *message) { SDL_Surface *surface = NULL; @@ -3557,7 +3884,7 @@ static void _saveAudioCalibration(int32_t milliseconds) { // Applies the command line audio track to a freshly loaded video, when it has one. static void _selectDefaultAudioTrack(int32_t handle) { - if (_global.conf->audioOutputTrack < videoGetAudioTracks(handle)) { + if ((_global.conf->audioOutputTrack >= 0) && (_global.conf->audioOutputTrack < videoGetAudioTracks(handle))) { videoSetAudioTrack(handle, _global.conf->audioOutputTrack); } } @@ -3595,6 +3922,20 @@ static void _soundDestroy(SoundT *sound) { } +// Takes the channels the mixer thread reported finished since the last call, under its lock. +static int32_t _soundQueueDrain(int32_t *finished) { + int32_t count = 0; + + videoLockAudio(); + count = _global.soundQueueCount; + memcpy(finished, _global.soundQueue, sizeof(int32_t) * (size_t)count); + _global.soundQueueCount = 0; + videoUnlockAudio(); + + return count; +} + + static void _spriteDestroy(SpriteT *sprite) { HASH_DEL(_global.spriteList, sprite); _spriteFreeSurface(sprite); @@ -3687,7 +4028,7 @@ static void _startLuaContext(lua_State *L) { lua_getfield(L, -1, "searchers"); length = lua_rawlen(L, -1); for (i = length + 2; i > 2; i--) { - lua_rawgeti(L, -2, (lua_Integer)(i - 2)); + lua_rawgeti(L, -1, (lua_Integer)(i - 2)); lua_rawseti(L, -2, (lua_Integer)i); } lua_pushcfunction(L, _luaSearcher); @@ -3715,6 +4056,25 @@ static void _stopControllers(void) { } +// The 3D side a script builds on: scene, physics, particles and navigation, in dependency order. +static void _subsystemsInit(void) { + sceneInit(_global.device, _global.renderer); + physicsInit(); + particlesInit(); + navInit(); +} + + +static void _subsystemsQuit(void) { + _particleTexturesFreeAll(); + modelQuit(); + navQuit(); + particlesQuit(); + physicsQuit(); + sceneQuit(); +} + + // Keys and buttons that are already down when a script starts, or when the window gains focus, are // not presses meant for this script: SDL reports them as fresh key downs, and the button that // confirmed "exit" in a game would otherwise relaunch it from the menu. They stay ignored until released. @@ -3770,18 +4130,6 @@ static void _surfaceUnpack(SDL_Surface **surface) { } -static SDL_Surface *_surfaceCopy(SDL_Surface *source) { - SDL_Surface *destination = SDL_CreateSurface(source->w, source->h, source->format); - - if (destination == NULL) { - utilDie("%s", SDL_GetError()); - } - SDL_BlitSurface(source, NULL, destination, NULL); - - return destination; -} - - // Saves the current frame buffer to the next free singeNNN.png in the data directory. // Must be called before SDL_RenderPresent for the frame being captured. static void _takeScreenshot(void) { @@ -3829,7 +4177,6 @@ static void _takeScreenshot(void) { } - // Everything a script loads: fonts, sounds, sprites and videos, whatever it forgot to unload. static void _unloadScriptResources(void) { FontT *font; @@ -3857,9 +4204,9 @@ static void _unloadScriptResources(void) { _progTrace("Unloading video handle %d", video->id); _videoDestroy(video); } - _global.fontCurrent = NULL; } + static void _updatePauseState(void) { if (_global.pauseState) { // Pause laserdisc @@ -3886,12 +4233,19 @@ static void _updatePauseState(void) { // own distance curve is flat inside one unit, and its gain is the range curve here. A channel // following a node that has gone falls back to plain playback. static void _updateSounds(void) { - Mat4T view = sceneGetView(); - Vec3T listener = vec3(0.0f, 0.0f, 0.0f); - QuatT inverse = quatIdentity(); - Vec3T scale; - bool useNode = false; - int32_t x; + Mat4T view = sceneGetView(); + Vec3T listener = vec3(0.0f, 0.0f, 0.0f); + QuatT inverse = quatIdentity(); + Vec3T scale; + Vec3T world; + Vec3T relative; + MIX_Point3D point; + EffectT *effect = NULL; + float baseGain = _mixerGain(_global.effectsVolume); + float distance = 0.0f; + float gain = 0.0f; + bool useNode = false; + int32_t x = 0; if ((_global.listenerNode != LISTENER_CAMERA) && nodeValid(_global.listenerNode) && nodeGetWorldTransform(_global.listenerNode, &listener, &inverse, &scale)) { inverse.x = -inverse.x; @@ -3900,12 +4254,7 @@ static void _updateSounds(void) { useNode = true; } for (x = 0; x < EFFECT_TRACKS; x++) { - EffectT *effect = &_effects[x]; - Vec3T world; - Vec3T relative; - float distance; - MIX_Point3D point; - + effect = &_effects[x]; if (!effect->positioned) { continue; } @@ -3913,7 +4262,7 @@ static void _updateSounds(void) { if (!nodeValid(effect->node)) { effect->positioned = false; MIX_SetTrack3DPosition(_effectTracks[x], NULL); - MIX_SetTrackGain(_effectTracks[x], _mixerGain(_global.effectsVolume)); + MIX_SetTrackGain(_effectTracks[x], baseGain); continue; } world = nodeGetWorldPosition(effect->node); @@ -3922,18 +4271,24 @@ static void _updateSounds(void) { } relative = useNode ? quatRotate(inverse, vec3Subtract(world, listener)) : mat4TransformPoint(view, world); distance = vec3Length(relative); - if (distance > 0.0001f) { + if (distance > SOUND_DIRECTION_EPSILON) { relative = vec3Scale(relative, 1.0f / distance); } - effect->gain = _effectGain(effect, distance); + gain = _effectGain(effect, distance); + // The mixer only hears about changes. + if ((gain == effect->gain) && (baseGain == effect->baseGain) && (relative.x == effect->relative[0]) && (relative.y == effect->relative[1]) && (relative.z == effect->relative[2])) { + continue; + } + effect->gain = gain; + effect->baseGain = baseGain; effect->relative[0] = relative.x; effect->relative[1] = relative.y; effect->relative[2] = relative.z; - point.x = relative.x; - point.y = relative.y; - point.z = relative.z; + point.x = relative.x; + point.y = relative.y; + point.z = relative.z; MIX_SetTrack3DPosition(_effectTracks[x], &point); - MIX_SetTrackGain(_effectTracks[x], _mixerGain(_global.effectsVolume) * effect->gain); + MIX_SetTrackGain(_effectTracks[x], baseGain * gain); } } @@ -3946,10 +4301,6 @@ static void _videoDestroy(VideoT *video) { } -// ===== Lua API ===== - - - // --reload keeps the modification time of every loose script file the game loads. static void _watchFile(const char *name) { int64_t size = 0; @@ -3998,6 +4349,10 @@ static bool _watchedChanged(void) { return false; } + +// ===== Lua API ===== + + // seconds = animationGetTime(node) static int32_t apiAnimationGetTime(lua_State *L) { int32_t root; @@ -4247,11 +4602,11 @@ static int32_t apiBodyIsResting(lua_State *L) { // bodyNew(node, type, shape [, a [, b [, c]]]): BODY_* and SHAPE_*; a, b, c size the primitive shapes static int32_t apiBodyNew(lua_State *L) { - int32_t node; - int32_t type; - int32_t shape; - float dims[3] = { 0.0f, 0.0f, 0.0f }; - int32_t x; + int32_t node = 0; + int32_t type = 0; + int32_t shape = 0; + float dims[SHAPE_SIZES] = { 0.0f, 0.0f, 0.0f }; + int32_t x = 0; _argCheck(L, "bodyNew", 3, 6); node = _argNode(L, "bodyNew", 1); @@ -4263,7 +4618,7 @@ static int32_t apiBodyNew(lua_State *L) { if ((shape < SHAPE_BOX) || (shape > SHAPE_MESH)) { _luaDie(L, "bodyNew", "Unknown shape %d.", shape); } - for (x = 0; x < 3; x++) { + for (x = 0; x < SHAPE_SIZES; x++) { if (lua_gettop(L) >= 4 + x) { dims[x] = (float)_argNumber(L, "bodyNew", 4 + x); } @@ -4295,7 +4650,6 @@ static int32_t apiBodySetBounce(lua_State *L) { } -// bodySetEnabled(node, bool): out of the world and back // bodySetBuoyancy(node, factor): 1 floats neutrally in water, more floats, less sinks static int32_t apiBodySetBuoyancy(lua_State *L) { _argCheck(L, "bodySetBuoyancy", 2, 2); @@ -4312,6 +4666,7 @@ static int32_t apiBodySetCurrent(lua_State *L) { } +// bodySetEnabled(node, bool): out of the world and back static int32_t apiBodySetEnabled(lua_State *L) { _argCheck(L, "bodySetEnabled", 2, 2); bodySetEnabled(_argBody(L, "bodySetEnabled", 1), _argBoolean(L, "bodySetEnabled", 2)); @@ -4356,17 +4711,17 @@ static int32_t apiBodySetVelocity(lua_State *L) { } -// Any node can be the camera (it looks down its own -Z); -1 restores the default view. // bodySetWater(node, density, linearDrag, angularDrag): a static body becomes a water volume static int32_t apiBodySetWater(lua_State *L) { _argCheck(L, "bodySetWater", 2, 4); - if (!bodySetWater(_argBody(L, "bodySetWater", 1), (float)_argNumber(L, "bodySetWater", 2), (lua_gettop(L) >= 3) ? (float)_argNumber(L, "bodySetWater", 3) : 0.5f, (lua_gettop(L) >= 4) ? (float)_argNumber(L, "bodySetWater", 4) : 0.1f)) { + if (!bodySetWater(_argBody(L, "bodySetWater", 1), (float)_argNumber(L, "bodySetWater", 2), (lua_gettop(L) >= 3) ? (float)_argNumber(L, "bodySetWater", 3) : WATER_DEFAULT_LINEAR_DRAG, (lua_gettop(L) >= 4) ? (float)_argNumber(L, "bodySetWater", 4) : WATER_DEFAULT_ANGULAR_DRAG)) { _luaDie(L, "bodySetWater", "Water needs a static body."); } return 0; } +// cameraSet(node): any node can be the camera (it looks down its own -Z); -1 restores the default view. static int32_t apiCameraSet(lua_State *L) { int32_t node; @@ -4542,11 +4897,7 @@ static int32_t apiDiscGetFrame(lua_State *L) { int64_t frame = 0; if (!_global.discStopped && (_global.videoHandle >= 0)) { - if (_global.conf->isFrameFile) { - frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle); - } else { - frame = videoGetFrame(_global.videoHandle); - } + frame = _discGetFrame(); } _luaTrace(L, "discGetFrame", "%" PRId64, frame); lua_pushinteger(L, frame); @@ -4557,9 +4908,8 @@ static int32_t apiDiscGetFrame(lua_State *L) { // height = discGetHeight() Also registered as vldpGetHeight. static int32_t apiDiscGetHeight(lua_State *L) { - int32_t height = 0; + int32_t height = _global.canvasHeight; - height = _global.canvasHeight; _luaTrace(L, "discGetHeight", "%d", height); lua_pushinteger(L, height); @@ -4595,10 +4945,8 @@ static int32_t apiDiscGetState(lua_State *L) { state = DISC_EJECTED; } else if (_global.discStopped) { state = DISC_STOPPED; - } else { - if ((_global.videoHandle >= 0) && videoIsPlaying(_global.videoHandle)) { - state = DISC_PLAYING; - } + } else if (videoIsPlaying(_global.videoHandle)) { + state = DISC_PLAYING; } _luaTrace(L, "discGetState", "%d", state); lua_pushinteger(L, state); @@ -4609,9 +4957,8 @@ static int32_t apiDiscGetState(lua_State *L) { // width = discGetWidth() Also registered as vldpGetWidth. static int32_t apiDiscGetWidth(lua_State *L) { - int32_t width = 0; + int32_t width = _global.canvasWidth; - width = _global.canvasWidth; _luaTrace(L, "discGetWidth", "%d", width); lua_pushinteger(L, width); @@ -4700,7 +5047,7 @@ static int32_t apiDiscSkipBackward(lua_State *L) { _luaTrace(L, "discSkipBackward", "Ignored. Disc is stopped."); return 0; } - frame = videoGetFrame(_global.videoHandle) - _argInteger64(L, "discSkipBackward", 1); + frame = _discGetFrame() - _argInteger64(L, "discSkipBackward", 1); _discSeek(frame); _luaTrace(L, "discSkipBackward", "%" PRId64, frame); @@ -4722,7 +5069,7 @@ static int32_t apiDiscSkipForward(lua_State *L) { _luaTrace(L, "discSkipForward", "Ignored. Disc is stopped."); return 0; } - frame = videoGetFrame(_global.videoHandle) + _argInteger64(L, "discSkipForward", 1); + frame = _discGetFrame() + _argInteger64(L, "discSkipForward", 1); _discSeek(frame); _luaTrace(L, "discSkipForward", "%" PRId64, frame); @@ -4747,41 +5094,34 @@ static int32_t apiDiscSkipToFrame(lua_State *L) { } -// discStepBackward() Go back a frame and pause. +// discStepBackward() Go back a frame and pause. Ignored while stopped, like the skips. static int32_t apiDiscStepBackward(lua_State *L) { int64_t frame = 0; - if (_global.videoHandle >= 0) { - if (_global.conf->isFrameFile) { - frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle) - 1; - } else { - frame = videoGetFrame(_global.videoHandle) - 1; - } - if (frame < 0) { - frame = 0; - } - _discSeek(frame); - videoPause(_global.videoHandle); + if (_global.discStopped || (_global.videoHandle < 0)) { + _luaTrace(L, "discStepBackward", "Ignored. Disc is stopped."); + return 0; } + frame = _discGetFrame() - 1; + _discSeek(frame); + videoPause(_global.videoHandle); _luaTrace(L, "discStepBackward", "%" PRId64, frame); return 0; } -// discStepForward() Go forward a frame and pause. +// discStepForward() Go forward a frame and pause. Ignored while stopped, like the skips. static int32_t apiDiscStepForward(lua_State *L) { int64_t frame = 0; - if (_global.videoHandle >= 0) { - if (_global.conf->isFrameFile) { - frame = frameFileGetFrame(_global.frameFileHandle, _global.videoHandle) + 1; - } else { - frame = videoGetFrame(_global.videoHandle) + 1; - } - _discSeek(frame); - videoPause(_global.videoHandle); + if (_global.discStopped || (_global.videoHandle < 0)) { + _luaTrace(L, "discStepForward", "Ignored. Disc is stopped."); + return 0; } + frame = _discGetFrame() + 1; + _discSeek(frame); + videoPause(_global.videoHandle); _luaTrace(L, "discStepForward", "%" PRId64, frame); return 0; @@ -4805,7 +5145,6 @@ static int32_t apiDiscStop(lua_State *L) { } -// id = fontLoad(filename, points) The new font becomes current. static int32_t apiEmitterBurst(lua_State *L) { _argCheck(L, "emitterBurst", 2, 2); emitterBurst(_argEmitter(L, "emitterBurst", 1), _argInteger(L, "emitterBurst", 2)); @@ -4874,27 +5213,31 @@ static int32_t apiEmitterNew(lua_State *L) { static int32_t apiEmitterSetBlend(lua_State *L) { - int32_t blend = 0; + int32_t emitter = 0; + int32_t blend = 0; _argCheck(L, "emitterSetBlend", 2, 2); - blend = _argInteger(L, "emitterSetBlend", 2); + emitter = _argEmitter(L, "emitterSetBlend", 1); + blend = _argInteger(L, "emitterSetBlend", 2); if ((blend != PARTICLE_ALPHA) && (blend != PARTICLE_ADD)) { _luaDie(L, "emitterSetBlend", "Blend must be PARTICLE_ALPHA or PARTICLE_ADD."); } - emitterSetBlend(_argEmitter(L, "emitterSetBlend", 1), (ParticleBlendE)blend); + emitterSetBlend(emitter, (ParticleBlendE)blend); return 0; } // emitterSetCollide(emitter, COLLIDE_* [, bounce [, friction [, floor]]]) static int32_t apiEmitterSetCollide(lua_State *L) { - int32_t mode; - float bounce = 0.5f; - float friction = 0.2f; - float floor = 0.0f; + int32_t emitter = 0; + int32_t mode = 0; + float bounce = EMITTER_DEFAULT_BOUNCE; + float friction = EMITTER_DEFAULT_FRICTION; + float floor = EMITTER_DEFAULT_FLOOR; _argCheck(L, "emitterSetCollide", 2, 5); - mode = _argInteger(L, "emitterSetCollide", 2); + emitter = _argEmitter(L, "emitterSetCollide", 1); + mode = _argInteger(L, "emitterSetCollide", 2); if ((mode != COLLIDE_NONE) && (mode != COLLIDE_FLOOR) && (mode != COLLIDE_SCENE)) { _luaDie(L, "emitterSetCollide", "Mode must be COLLIDE_NONE, COLLIDE_FLOOR or COLLIDE_SCENE."); } @@ -4907,20 +5250,20 @@ static int32_t apiEmitterSetCollide(lua_State *L) { if (lua_gettop(L) >= 5) { floor = (float)_argNumber(L, "emitterSetCollide", 5); } - emitterSetCollide(_argEmitter(L, "emitterSetCollide", 1), (ParticleCollideE)mode, bounce, friction, floor); + emitterSetCollide(emitter, (ParticleCollideE)mode, bounce, friction, floor); return 0; } static int32_t apiEmitterSetColor(lua_State *L) { - float start[4]; - float finish[4]; - int32_t c; + float start[COLOR_COMPONENTS]; + float finish[COLOR_COMPONENTS]; + int32_t c = 0; _argCheck(L, "emitterSetColor", 9, 9); - for (c = 0; c < 4; c++) { - start[c] = SDL_clamp((float)_argNumber(L, "emitterSetColor", 2 + c) / 255.0f, 0.0f, 1.0f); - finish[c] = SDL_clamp((float)_argNumber(L, "emitterSetColor", 6 + c) / 255.0f, 0.0f, 1.0f); + for (c = 0; c < COLOR_COMPONENTS; c++) { + start[c] = SDL_clamp((float)_argNumber(L, "emitterSetColor", 2 + c) / (float)COLOR_BYTE_MAX, 0.0f, 1.0f); + finish[c] = SDL_clamp((float)_argNumber(L, "emitterSetColor", 6 + c) / (float)COLOR_BYTE_MAX, 0.0f, 1.0f); } emitterSetColor(_argEmitter(L, "emitterSetColor", 1), start, finish); return 0; @@ -4966,14 +5309,16 @@ static int32_t apiEmitterSetGravity(lua_State *L) { static int32_t apiEmitterSetLayer(lua_State *L) { - int32_t layer = 0; + int32_t emitter = 0; + int32_t layer = 0; _argCheck(L, "emitterSetLayer", 2, 2); - layer = _argInteger(L, "emitterSetLayer", 2); + emitter = _argEmitter(L, "emitterSetLayer", 1); + layer = _argInteger(L, "emitterSetLayer", 2); if ((layer != PARTICLE_OVER) && (layer != PARTICLE_UNDER)) { _luaDie(L, "emitterSetLayer", "Layer must be PARTICLE_OVER or PARTICLE_UNDER."); } - emitterSetLayer(_argEmitter(L, "emitterSetLayer", 1), (ParticleLayerE)layer); + emitterSetLayer(emitter, (ParticleLayerE)layer); return 0; } @@ -5001,14 +5346,16 @@ static int32_t apiEmitterSetLocal(lua_State *L) { static int32_t apiEmitterSetMax(lua_State *L) { - int32_t count = 0; + int32_t emitter = 0; + int32_t count = 0; _argCheck(L, "emitterSetMax", 2, 2); - count = _argInteger(L, "emitterSetMax", 2); + emitter = _argEmitter(L, "emitterSetMax", 1); + count = _argInteger(L, "emitterSetMax", 2); if (count < 1) { _luaDie(L, "emitterSetMax", "An emitter needs room for at least one particle."); } - emitterSetMax(_argEmitter(L, "emitterSetMax", 1), count); + emitterSetMax(emitter, count); return 0; } @@ -5118,19 +5465,25 @@ static int32_t apiEmitterStop(lua_State *L) { } +// id = fontLoad(filename, points) The new font becomes current. static int32_t apiFontLoad(lua_State *L) { - const char *name = NULL; - int32_t points = 0; - FontT *font = NULL; + const char *name = NULL; + int32_t points = 0; + FontT *font = NULL; + SDL_IOStream *io = NULL; _argCheck(L, "fontLoad", 2, 2); name = _argString(L, "fontLoad", 1); points = _argInteger(L, "fontLoad", 2); - font = (FontT *)calloc(1, sizeof(FontT)); + io = vfsOpenIO(name); + if (io == NULL) { + _luaDie(L, "fontLoad", "Unable to open %s", name); + } + font = (FontT *)calloc(1, sizeof(FontT)); if (!font) { _luaDie(L, "fontLoad", "Unable to allocate new font."); } - font->font = TTF_OpenFontIO(vfsOpenIO(name), true, (float)points); + font->font = TTF_OpenFontIO(io, true, (float)points); if (!font->font) { _luaDie(L, "fontLoad", "%s", SDL_GetError()); } @@ -5420,7 +5773,7 @@ static int32_t apiLightSetColor(lua_State *L) { _argCheck(L, "lightSetColor", 4, 4); node = _argNode(L, "lightSetColor", 1); - if (!lightSetColor(node, (uint8_t)_argInteger(L, "lightSetColor", 2), (uint8_t)_argInteger(L, "lightSetColor", 3), (uint8_t)_argInteger(L, "lightSetColor", 4))) { + if (!lightSetColor(node, _argColorByte(L, "lightSetColor", 2), _argColorByte(L, "lightSetColor", 3), _argColorByte(L, "lightSetColor", 4))) { _luaDie(L, "lightSetColor", "Node %d is not a light.", node); } return 0; @@ -5481,21 +5834,26 @@ static int32_t apiLightSetShadow(lua_State *L) { // lineDraw(x0, y0, z0, x1, y1, z1 [, r, g, b]): a world-space line over the scene for this frame, white by default static int32_t apiLineDraw(lua_State *L) { - uint8_t r = COLOUR_BYTE_MAX; - uint8_t g = COLOUR_BYTE_MAX; - uint8_t b = COLOUR_BYTE_MAX; + uint8_t r = COLOR_BYTE_MAX; + uint8_t g = COLOR_BYTE_MAX; + uint8_t b = COLOR_BYTE_MAX; _argCheck(L, "lineDraw", 6, 9); - _argOptionalColour(L, "lineDraw", 7, &r, &g, &b); + _argOptionalColor(L, "lineDraw", 7, &r, &g, &b); sceneDrawLine(_argVec3(L, "lineDraw", 1), _argVec3(L, "lineDraw", 4), r, g, b); return 0; } -// materialDelete(material) +// materialDelete(material): a sprite node's own material (from nodeGetMaterial) stays with its node static int32_t apiMaterialDelete(lua_State *L) { + int32_t material = 0; + _argCheck(L, "materialDelete", 1, 1); - materialDelete(_argMaterial(L, "materialDelete", 1)); + material = _argMaterial(L, "materialDelete", 1); + if (!materialDelete(material)) { + _luaDie(L, "materialDelete", "Material %d belongs to a sprite node; clear the sprite instead.", material); + } return 0; } @@ -5504,7 +5862,6 @@ static int32_t apiMaterialDelete(lua_State *L) { static int32_t apiMaterialNew(lua_State *L) { int32_t material; - _argCheck(L, "materialNew", 0, 0); material = materialNew(); if (material < 0) { _luaDie(L, "materialNew", "3D is not available on this machine."); @@ -5525,15 +5882,15 @@ static int32_t apiMaterialSetBlend(lua_State *L) { // materialSetColor(material, r, g, b [, a]) static int32_t apiMaterialSetColor(lua_State *L) { - int32_t material; - int32_t a = SDL_ALPHA_OPAQUE; + int32_t material = 0; + uint8_t a = SDL_ALPHA_OPAQUE; _argCheck(L, "materialSetColor", 4, 5); material = _argMaterial(L, "materialSetColor", 1); if (lua_gettop(L) >= 5) { - a = _argInteger(L, "materialSetColor", 5); + a = _argColorByte(L, "materialSetColor", 5); } - materialSetColor(material, (uint8_t)_argInteger(L, "materialSetColor", 2), (uint8_t)_argInteger(L, "materialSetColor", 3), (uint8_t)_argInteger(L, "materialSetColor", 4), (uint8_t)a); + materialSetColor(material, _argColorByte(L, "materialSetColor", 2), _argColorByte(L, "materialSetColor", 3), _argColorByte(L, "materialSetColor", 4), a); return 0; } @@ -5549,51 +5906,33 @@ static int32_t apiMaterialSetDoubleSided(lua_State *L) { // materialSetEmissive(material, r, g, b) static int32_t apiMaterialSetEmissive(lua_State *L) { _argCheck(L, "materialSetEmissive", 4, 4); - materialSetEmissive(_argMaterial(L, "materialSetEmissive", 1), (uint8_t)_argInteger(L, "materialSetEmissive", 2), (uint8_t)_argInteger(L, "materialSetEmissive", 3), (uint8_t)_argInteger(L, "materialSetEmissive", 4)); + materialSetEmissive(_argMaterial(L, "materialSetEmissive", 1), _argColorByte(L, "materialSetEmissive", 2), _argColorByte(L, "materialSetEmissive", 3), _argColorByte(L, "materialSetEmissive", 4)); + return 0; +} + + +// materialSetEmissiveMap(material[, image]) nil clears. +static int32_t apiMaterialSetEmissiveMap(lua_State *L) { + return _materialSetMap(L, "materialSetEmissiveMap", MAP_EMISSIVE, false); +} + + +static int32_t apiMaterialSetFilter(lua_State *L) { + int32_t material = 0; + int32_t filter = 0; + + _argCheck(L, "materialSetFilter", 2, 2); + material = _argMaterial(L, "materialSetFilter", 1); + filter = _argInteger(L, "materialSetFilter", 2); + if ((filter != FILTER_LINEAR) && (filter != FILTER_NEAREST)) { + _luaDie(L, "materialSetFilter", "Filter must be FILTER_LINEAR or FILTER_NEAREST."); + } + materialSetFilter(material, (MaterialFilterE)filter); return 0; } // materialSetMetallic(material, 0..1) -// materialSetEmissiveMap(material[, sprite]) nil clears. -static int32_t apiMaterialSetEmissiveMap(lua_State *L) { - int32_t material; - SDL_Surface *surface = NULL; - Ktx2ImageT ktx2; - bool ok; - - _argCheck(L, "materialSetEmissiveMap", 1, 2); - material = _argMaterial(L, "materialSetEmissiveMap", 1); - if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) { - if (_argMapImage(L, "materialSetEmissiveMap", 2, &surface, &ktx2)) { - ok = materialSetMap(material, MAP_EMISSIVE, &ktx2, 1.0f); - ktx2Free(&ktx2); - if (!ok) { - _luaDie(L, "materialSetEmissiveMap", "Unable to upload the texture."); - } - return 0; - } - } - if (!materialSetEmissiveMap(material, surface)) { - _luaDie(L, "materialSetEmissiveMap", "%s", SDL_GetError()); - } - return 0; -} - - -static int32_t apiMaterialSetFilter(lua_State *L) { - int32_t filter = 0; - - _argCheck(L, "materialSetFilter", 2, 2); - filter = _argInteger(L, "materialSetFilter", 2); - if ((filter != FILTER_LINEAR) && (filter != FILTER_NEAREST)) { - _luaDie(L, "materialSetFilter", "Filter must be FILTER_LINEAR or FILTER_NEAREST."); - } - materialSetFilter(_argMaterial(L, "materialSetFilter", 1), (MaterialFilterE)filter); - return 0; -} - - static int32_t apiMaterialSetMetallic(lua_State *L) { _argCheck(L, "materialSetMetallic", 2, 2); materialSetMetallic(_argMaterial(L, "materialSetMetallic", 1), (float)_argNumber(L, "materialSetMetallic", 2)); @@ -5601,93 +5940,25 @@ static int32_t apiMaterialSetMetallic(lua_State *L) { } -// materialSetRoughness(material, 0..1) -// materialSetMetallicRoughnessMap(material[, sprite]) roughness in G, metallic in B; nil clears. +// materialSetMetallicRoughnessMap(material[, image]) roughness in G, metallic in B; nil clears. static int32_t apiMaterialSetMetallicRoughnessMap(lua_State *L) { - int32_t material; - SDL_Surface *surface = NULL; - Ktx2ImageT ktx2; - bool ok; - - _argCheck(L, "materialSetMetallicRoughnessMap", 1, 2); - material = _argMaterial(L, "materialSetMetallicRoughnessMap", 1); - if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) { - if (_argMapImage(L, "materialSetMetallicRoughnessMap", 2, &surface, &ktx2)) { - ok = materialSetMap(material, MAP_METALLIC_ROUGHNESS, &ktx2, 1.0f); - ktx2Free(&ktx2); - if (!ok) { - _luaDie(L, "materialSetMetallicRoughnessMap", "Unable to upload the texture."); - } - return 0; - } - } - if (!materialSetMetallicRoughnessMap(material, surface)) { - _luaDie(L, "materialSetMetallicRoughnessMap", "%s", SDL_GetError()); - } - return 0; + return _materialSetMap(L, "materialSetMetallicRoughnessMap", MAP_METALLIC_ROUGHNESS, false); } -// materialSetNormalMap(material[, sprite[, strength]]) nil clears; strength defaults to 1. +// materialSetNormalMap(material[, image[, strength]]) nil clears; strength defaults to 1. static int32_t apiMaterialSetNormalMap(lua_State *L) { - int32_t material; - SDL_Surface *surface = NULL; - Ktx2ImageT ktx2; - bool ok; - float strength = 1.0f; - - _argCheck(L, "materialSetNormalMap", 1, 3); - material = _argMaterial(L, "materialSetNormalMap", 1); - if (lua_gettop(L) >= 3) { - strength = (float)_argNumber(L, "materialSetNormalMap", 3); - } - if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) { - if (_argMapImage(L, "materialSetNormalMap", 2, &surface, &ktx2)) { - ok = materialSetMap(material, MAP_NORMAL, &ktx2, strength); - ktx2Free(&ktx2); - if (!ok) { - _luaDie(L, "materialSetNormalMap", "Unable to upload the texture."); - } - return 0; - } - } - if (!materialSetNormalMap(material, surface, strength)) { - _luaDie(L, "materialSetNormalMap", "%s", SDL_GetError()); - } - return 0; + return _materialSetMap(L, "materialSetNormalMap", MAP_NORMAL, true); } -// materialSetOcclusionMap(material[, sprite[, strength]]) occlusion in R; nil clears; strength defaults to 1. +// materialSetOcclusionMap(material[, image[, strength]]) occlusion in R; nil clears; strength defaults to 1. static int32_t apiMaterialSetOcclusionMap(lua_State *L) { - int32_t material; - SDL_Surface *surface = NULL; - Ktx2ImageT ktx2; - bool ok; - float strength = 1.0f; - - _argCheck(L, "materialSetOcclusionMap", 1, 3); - material = _argMaterial(L, "materialSetOcclusionMap", 1); - if (lua_gettop(L) >= 3) { - strength = (float)_argNumber(L, "materialSetOcclusionMap", 3); - } - if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) { - if (_argMapImage(L, "materialSetOcclusionMap", 2, &surface, &ktx2)) { - ok = materialSetMap(material, MAP_OCCLUSION, &ktx2, strength); - ktx2Free(&ktx2); - if (!ok) { - _luaDie(L, "materialSetOcclusionMap", "Unable to upload the texture."); - } - return 0; - } - } - if (!materialSetOcclusionMap(material, surface, strength)) { - _luaDie(L, "materialSetOcclusionMap", "%s", SDL_GetError()); - } - return 0; + return _materialSetMap(L, "materialSetOcclusionMap", MAP_OCCLUSION, true); } +// materialSetRoughness(material, 0..1) static int32_t apiMaterialSetRoughness(lua_State *L) { _argCheck(L, "materialSetRoughness", 2, 2); materialSetRoughness(_argMaterial(L, "materialSetRoughness", 1), (float)_argNumber(L, "materialSetRoughness", 2)); @@ -5695,33 +5966,12 @@ static int32_t apiMaterialSetRoughness(lua_State *L) { } -// materialSetTexture(material [, sprite]): a sprite's image as the base colour; none clears it +// materialSetTexture(material [, image]): a sprite's image (or a KTX2 file) as the base colour; none clears it static int32_t apiMaterialSetTexture(lua_State *L) { - int32_t material; - SDL_Surface *surface = NULL; - Ktx2ImageT ktx2; - bool ok; - - _argCheck(L, "materialSetTexture", 1, 2); - material = _argMaterial(L, "materialSetTexture", 1); - if ((lua_gettop(L) >= 2) && !lua_isnil(L, 2)) { - if (_argMapImage(L, "materialSetTexture", 2, &surface, &ktx2)) { - ok = materialSetMap(material, MAP_BASE, &ktx2, 1.0f); - ktx2Free(&ktx2); - if (!ok) { - _luaDie(L, "materialSetTexture", "Unable to upload the texture."); - } - return 0; - } - } - if (!materialSetTexture(material, surface)) { - _luaDie(L, "materialSetTexture", "%s", SDL_GetError()); - } - return 0; + return _materialSetMap(L, "materialSetTexture", MAP_BASE, false); } -// materialSetUnlit(material, bool): shows the base colour as is // materialSetTiling(material, u, v): how many times its textures repeat across a surface static int32_t apiMaterialSetTiling(lua_State *L) { _argCheck(L, "materialSetTiling", 3, 3); @@ -5730,6 +5980,7 @@ static int32_t apiMaterialSetTiling(lua_State *L) { } +// materialSetUnlit(material, bool): shows the base colour as is static int32_t apiMaterialSetUnlit(lua_State *L) { _argCheck(L, "materialSetUnlit", 2, 2); materialSetUnlit(_argMaterial(L, "materialSetUnlit", 1), _argBoolean(L, "materialSetUnlit", 2)); @@ -5791,8 +6042,8 @@ static int32_t apiMeshBox(lua_State *L) { // mesh = meshCone(radius, height [, segments]) static int32_t apiMeshCone(lua_State *L) { - int32_t mesh; - int32_t segments = 24; + int32_t mesh = 0; + int32_t segments = MESH_SEGMENTS_DEFAULT; _argCheck(L, "meshCone", 2, 3); if (lua_gettop(L) >= 3) { @@ -5810,8 +6061,8 @@ static int32_t apiMeshCone(lua_State *L) { // mesh = meshCylinder(radius, height [, segments]) static int32_t apiMeshCylinder(lua_State *L) { - int32_t mesh; - int32_t segments = 24; + int32_t mesh = 0; + int32_t segments = MESH_SEGMENTS_DEFAULT; _argCheck(L, "meshCylinder", 2, 3); if (lua_gettop(L) >= 3) { @@ -5835,20 +6086,21 @@ static int32_t apiMeshDelete(lua_State *L) { } - // mesh = meshHeightmap(image, sizeX, sizeY, sizeZ): a terrain from a greyscale image (its red channel, // black low and white sizeY high), sizeX by sizeZ across, one vertex per pixel static int32_t apiMeshHeightmap(lua_State *L) { - const char *name; - SDL_IOStream *io; - SDL_Surface *image; - SDL_Surface *rgba; - float *heights; - int32_t columns; - int32_t rows; - int32_t x; - int32_t y; - int32_t mesh; + const char *name = NULL; + SDL_IOStream *io = NULL; + SDL_Surface *image = NULL; + SDL_Surface *rgba = NULL; + const uint8_t *row = NULL; + float *heights = NULL; + int32_t bytesPerPixel = 0; + int32_t columns = 0; + int32_t rows = 0; + int32_t x = 0; + int32_t y = 0; + int32_t mesh = 0; _argCheck(L, "meshHeightmap", 4, 4); name = _argString(L, "meshHeightmap", 1); @@ -5875,11 +6127,11 @@ static int32_t apiMeshHeightmap(lua_State *L) { if (heights == NULL) { utilDie("Out of memory reading a heightmap."); } + bytesPerPixel = (int32_t)SDL_BYTESPERPIXEL(rgba->format); for (y = 0; y < rgba->h; y++) { - const uint8_t *row = (const uint8_t *)rgba->pixels + (size_t)y * (size_t)rgba->pitch; - + row = (const uint8_t *)rgba->pixels + (size_t)y * (size_t)rgba->pitch; for (x = 0; x < rgba->w; x++) { - heights[y * rgba->w + x] = row[x * 4] / 255.0f; + heights[y * rgba->w + x] = row[x * bytesPerPixel] / (float)COLOR_BYTE_MAX; } } SDL_DestroySurface(rgba); @@ -5892,6 +6144,7 @@ static int32_t apiMeshHeightmap(lua_State *L) { return 1; } + // mesh = meshNew(positions, normals, uvs, indices): tables of numbers; normals and uvs may be nil static int32_t apiMeshNew(lua_State *L) { float *positions; @@ -5976,8 +6229,8 @@ static int32_t apiMeshPlane(lua_State *L) { // mesh = meshSphere(radius [, segments]) static int32_t apiMeshSphere(lua_State *L) { - int32_t mesh; - int32_t segments = 32; + int32_t mesh = 0; + int32_t segments = MESH_SPHERE_SEGMENTS_DEFAULT; _argCheck(L, "meshSphere", 1, 2); if (lua_gettop(L) >= 2) { @@ -5995,8 +6248,8 @@ static int32_t apiMeshSphere(lua_State *L) { // mesh = meshTorus(radius, tubeRadius [, segments]) static int32_t apiMeshTorus(lua_State *L) { - int32_t mesh; - int32_t segments = 32; + int32_t mesh = 0; + int32_t segments = MESH_SPHERE_SEGMENTS_DEFAULT; _argCheck(L, "meshTorus", 2, 3); if (lua_gettop(L) >= 3) { @@ -6153,7 +6406,6 @@ static int32_t apiMouseSetMode(lua_State *L) { } - // navAddNode(nav, node): the node's mesh and its children's become walkable geometry for navBuild static int32_t apiNavAddNode(lua_State *L) { _argCheck(L, "navAddNode", 2, 2); @@ -6210,7 +6462,7 @@ static int32_t apiNavAgentNew(lua_State *L) { node = _argNode(L, "navAgentNew", 2); agent = navAgentNew(nav, node, (float)_argNumber(L, "navAgentNew", 3), (float)_argNumber(L, "navAgentNew", 4), (float)_argNumber(L, "navAgentNew", 5)); if (agent < 0) { - _luaDie(L, "navAgentNew", "No agent available (is the mesh built, and are fewer than 128 agents on it?)."); + _luaDie(L, "navAgentNew", "No agent available (is the mesh built, and are fewer than %d agents on it?).", NAV_MAX_CROWD_AGENTS); } lua_pushinteger(L, agent); return 1; @@ -6254,63 +6506,68 @@ static int32_t apiNavDelete(lua_State *L) { // navDraw(nav [, r, g, b]): the baked mesh's triangles as lines over the scene for this frame, cyan by default static int32_t apiNavDraw(lua_State *L) { - int32_t nav = _argNav(L, "navDraw", 1); - int32_t capacity = NAV_DRAW_VERTICES; - Vec3T *vertices = NULL; - int32_t count; - int32_t x; - uint8_t r = 0; - uint8_t g = COLOUR_BYTE_MAX; - uint8_t b = COLOUR_BYTE_MAX; + int32_t nav = 0; + int32_t capacity = 0; + int32_t count = 0; + int32_t x = 0; + Vec3T *grown = NULL; + Vec3T *vertices = NULL; + uint8_t r = 0; + uint8_t g = COLOR_BYTE_MAX; + uint8_t b = COLOR_BYTE_MAX; _argCheck(L, "navDraw", 1, 4); - _argOptionalColour(L, "navDraw", 2, &r, &g, &b); - do { - Vec3T *grown; - - capacity *= 2; - grown = SDL_realloc(vertices, (size_t)capacity * sizeof(Vec3T)); - if (grown == NULL) { - SDL_free(vertices); - return 0; + nav = _argNav(L, "navDraw", 1); + _argOptionalColor(L, "navDraw", 2, &r, &g, &b); + // The triangle list is kept between frames (this is drawn every frame) and doubled until the mesh fits. + capacity = SDL_max(_global.navDrawCapacity, NAV_DRAW_VERTICES); + for (;;) { + if (capacity > _global.navDrawCapacity) { + grown = SDL_realloc(_global.navDrawVertices, (size_t)capacity * sizeof(Vec3T)); + if (grown == NULL) { + _luaDie(L, "navDraw", "Out of memory."); + } + _global.navDrawVertices = grown; + _global.navDrawCapacity = capacity; } - vertices = grown; + vertices = _global.navDrawVertices; count = navGetPolygons(nav, vertices, capacity); - } while (count + 3 > capacity); + if (count + 3 <= capacity) { + break; + } + capacity *= 2; + } for (x = 0; x + 2 < count; x += 3) { sceneDrawLine(vertices[x], vertices[x + 1], r, g, b); sceneDrawLine(vertices[x + 1], vertices[x + 2], r, g, b); sceneDrawLine(vertices[x + 2], vertices[x], r, g, b); } - SDL_free(vertices); return 0; } // nav = navLoad(name, agentRadius, agentHeight): a mesh navSave wrote, from the game or its data folder static int32_t apiNavLoad(lua_State *L) { - const char *name; - char *data; + const char *name = NULL; + char *data = NULL; + char *path = NULL; size_t size = 0; - int32_t nav; + int32_t nav = 0; _argCheck(L, "navLoad", 3, 3); name = _argString(L, "navLoad", 1); data = vfsRead(name, &size); if (data == NULL) { - char *path = utilCreateString("%s%s", _global.conf->dataDir, name); - - data = SDL_LoadFile(path, &size); + // Not in the game: maybe in its data folder, where navSave writes. + path = utilCreateString("%s%s", _global.conf->dataDir, name); + data = utilReadFile(path, &size); free(path); - if (data == NULL) { - _luaDie(L, "navLoad", "Unable to read %s", name); - } - nav = navLoad(data, size, (float)_argNumber(L, "navLoad", 2), (float)_argNumber(L, "navLoad", 3)); - SDL_free(data); - } else { - nav = navLoad(data, size, (float)_argNumber(L, "navLoad", 2), (float)_argNumber(L, "navLoad", 3)); - free(data); } + if (data == NULL) { + _luaDie(L, "navLoad", "Unable to read %s", name); + } + nav = navLoad(data, size, (float)_argNumber(L, "navLoad", 2), (float)_argNumber(L, "navLoad", 3)); + free(data); if (nav < 0) { _luaDie(L, "navLoad", "%s is not a navigation mesh, or every mesh slot is in use.", name); } @@ -6348,12 +6605,12 @@ static int32_t apiNavNew(lua_State *L) { // points = navPath(nav, x0, y0, z0, x1, y1, z1): a table of {x, y, z} corners, or nil for no path static int32_t apiNavPath(lua_State *L) { - Vec3T points[256]; - int32_t count; - int32_t x; + Vec3T points[NAV_MAX_PATH]; + int32_t count = 0; + int32_t x = 0; _argCheck(L, "navPath", 7, 7); - count = navPath(_argNav(L, "navPath", 1), vec3((float)_argNumber(L, "navPath", 2), (float)_argNumber(L, "navPath", 3), (float)_argNumber(L, "navPath", 4)), vec3((float)_argNumber(L, "navPath", 5), (float)_argNumber(L, "navPath", 6), (float)_argNumber(L, "navPath", 7)), points, 256); + count = navPath(_argNav(L, "navPath", 1), vec3((float)_argNumber(L, "navPath", 2), (float)_argNumber(L, "navPath", 3), (float)_argNumber(L, "navPath", 4)), vec3((float)_argNumber(L, "navPath", 5), (float)_argNumber(L, "navPath", 6), (float)_argNumber(L, "navPath", 7)), points, (int32_t)SDL_arraysize(points)); if (count < 0) { lua_pushnil(L); return 1; @@ -6418,6 +6675,7 @@ static int32_t apiNavSave(lua_State *L) { return 0; } + // nodeDelete(node): the node and everything under it static int32_t apiNodeDelete(lua_State *L) { int32_t node; @@ -6434,14 +6692,16 @@ static int32_t apiNodeDelete(lua_State *L) { // node = nodeFind(name [, root]): by name, below root; nil when absent static int32_t apiNodeFind(lua_State *L) { - int32_t root = SCENE_ROOT_NODE; - int32_t found; + const char *name = NULL; + int32_t root = SCENE_ROOT_NODE; + int32_t found = 0; _argCheck(L, "nodeFind", 1, 2); + name = _argString(L, "nodeFind", 1); if (lua_gettop(L) >= 2) { root = _argNode(L, "nodeFind", 2); } - found = nodeFind(root, _argString(L, "nodeFind", 1)); + found = nodeFind(root, name); if (found < 0) { lua_pushnil(L); } else { @@ -6616,22 +6876,23 @@ static int32_t apiNodeRotate(lua_State *L) { } -// nodeSetMesh(node, mesh [, material]) -// nodeSetMaterial(node, material): a new material on whatever mesh the node has (nil for none) // nodeSetBillboard(node, BILLBOARD_*): the node turns to face the camera static int32_t apiNodeSetBillboard(lua_State *L) { - int32_t mode; + int32_t node = 0; + int32_t mode = 0; _argCheck(L, "nodeSetBillboard", 2, 2); + node = _argNode(L, "nodeSetBillboard", 1); mode = _argInteger(L, "nodeSetBillboard", 2); if ((mode != BILLBOARD_NONE) && (mode != BILLBOARD_ALL) && (mode != BILLBOARD_Y)) { _luaDie(L, "nodeSetBillboard", "Mode must be BILLBOARD_NONE, BILLBOARD_ALL or BILLBOARD_Y."); } - nodeSetBillboard(_argNode(L, "nodeSetBillboard", 1), (BillboardE)mode); + nodeSetBillboard(node, (BillboardE)mode); return 0; } +// nodeSetMaterial(node, material): a new material on whatever mesh the node has (nil for none) static int32_t apiNodeSetMaterial(lua_State *L) { int32_t material = -1; @@ -6644,6 +6905,7 @@ static int32_t apiNodeSetMaterial(lua_State *L) { } +// nodeSetMesh(node, mesh [, material]) static int32_t apiNodeSetMesh(lua_State *L) { int32_t node; int32_t mesh; @@ -6753,7 +7015,6 @@ static int32_t apiNodeSetShadow(lua_State *L) { } - // nodeSetSprite(node, sprite [, height [, lit]]): the sprite's picture (every frame of an animated one) // on a quad height world units tall, wide by its aspect; nil clears it static int32_t apiNodeSetSprite(lua_State *L) { @@ -6836,6 +7097,7 @@ static int32_t apiNodeSetText(lua_State *L) { return 0; } + // nodeSetVisible(node, bool): hides the node and its children static int32_t apiNodeSetVisible(lua_State *L) { _argCheck(L, "nodeSetVisible", 2, 2); @@ -6993,7 +7255,7 @@ static int32_t apiOverlayEllipse(lua_State *L) { dy += a; err += dy; } - if (e2 >= dx || 2 * err > dy) { // x step + if ((e2 >= dx) || (2 * err > dy)) { // x step x0++; x1--; dx += b1; @@ -7194,8 +7456,6 @@ static int32_t apiPhysicsSetGravity(lua_State *L) { } -// scriptExecute(config) Runs another script after this one ends. -// Turns the 3D layer on or off. static int32_t apiPlayerDelete(lua_State *L) { _argCheck(L, "playerDelete", 1, 1); playerDelete(_argPlayer(L, "playerDelete", 1)); @@ -7269,13 +7529,14 @@ static int32_t apiPlayerMove(lua_State *L) { } -// playerNew(node, radius, height) for a capsule, or playerNew(node, SHAPE_*, sizes...) +// playerNew(node, radius, height) for a capsule, or playerNew(node, SHAPE_*, a, b, c). Three arguments +// always mean a capsule, so a sphere or cylinder spells out every size (SHAPE_HULL needs none). static int32_t apiPlayerNew(lua_State *L) { - int32_t node; - int32_t shape = SHAPE_CAPSULE; - float dims[3] = { 0.0f, 0.0f, 0.0f }; - int32_t first = 2; - int32_t x; + int32_t node = 0; + int32_t shape = SHAPE_CAPSULE; + float dims[SHAPE_SIZES] = { 0.0f, 0.0f, 0.0f }; + int32_t first = 2; + int32_t x = 0; _argCheck(L, "playerNew", 2, 5); node = _argNode(L, "playerNew", 1); @@ -7283,7 +7544,7 @@ static int32_t apiPlayerNew(lua_State *L) { shape = (int32_t)lua_tointeger(L, 2); first = 3; } - for (x = 0; x < 3; x++) { + for (x = 0; x < SHAPE_SIZES; x++) { if (lua_gettop(L) >= first + x) { dims[x] = (float)_argNumber(L, "playerNew", first + x); } @@ -7444,6 +7705,7 @@ static int32_t apiRagdollSetStrength(lua_State *L) { } +// sceneEnable(bool): turns the 3D layer on or off static int32_t apiSceneEnable(lua_State *L) { bool enabled; @@ -7463,7 +7725,6 @@ static int32_t apiSceneGetSize(lua_State *L) { int32_t width; int32_t height; - _argCheck(L, "sceneGetSize", 0, 0); sceneGetSize(&width, &height); lua_pushinteger(L, width); lua_pushinteger(L, height); @@ -7471,7 +7732,6 @@ static int32_t apiSceneGetSize(lua_State *L) { } -// x, y, depth, inFront = sceneProject(wx, wy, wz): a world point in overlay coordinates // sceneGetStats(): last frame's draws collected, inside the view, and draw calls made static int32_t apiSceneGetStats(lua_State *L) { int32_t total = 0; @@ -7479,16 +7739,16 @@ static int32_t apiSceneGetStats(lua_State *L) { int32_t batches = 0; int64_t textureBytes = 0; - _argCheck(L, "sceneGetStats", 0, 0); sceneGetStats(&total, &drawn, &batches, &textureBytes); lua_pushinteger(L, total); lua_pushinteger(L, drawn); lua_pushinteger(L, batches); - lua_pushinteger(L, textureBytes / 1024); + lua_pushinteger(L, textureBytes / BYTES_PER_KIB); return 4; } +// x, y, depth, inFront = sceneProject(wx, wy, wz): a world point in overlay coordinates static int32_t apiSceneProject(lua_State *L) { float x; float y; @@ -7539,8 +7799,6 @@ static int32_t apiSceneSetBackground(lua_State *L) { } -// sceneSetShadowSize(size): shadow map texels per side, 256 to 4096 (default 1024) -// sceneSetEnvironment(lit): whether the sky lights the scene // sceneSetBloom(threshold, strength): the glow of everything brighter than the threshold; strength 0 for none static int32_t apiSceneSetBloom(lua_State *L) { _argCheck(L, "sceneSetBloom", 2, 2); @@ -7549,6 +7807,7 @@ static int32_t apiSceneSetBloom(lua_State *L) { } +// sceneSetEnvironment(lit): whether the sky lights the scene static int32_t apiSceneSetEnvironment(lua_State *L) { _argCheck(L, "sceneSetEnvironment", 1, 1); sceneSetEnvironment(_argBoolean(L, "sceneSetEnvironment", 1)); @@ -7594,6 +7853,7 @@ static int32_t apiSceneSetShadowDistance(lua_State *L) { } +// sceneSetShadowSize(size): shadow map texels per side, 256 to 4096 (default 1024) static int32_t apiSceneSetShadowSize(lua_State *L) { _argCheck(L, "sceneSetShadowSize", 1, 1); sceneSetShadowSize(_argInteger(L, "sceneSetShadowSize", 1)); @@ -7601,7 +7861,6 @@ static int32_t apiSceneSetShadowSize(lua_State *L) { } -// x, y, z = sceneUnproject(sx, sy, distance): the world point that far along the ray through an overlay point // sceneSetSky(file) from an equirectangular image (Radiance .hdr keeps its range; PNG or JPEG is // decoded from sRGB), or sceneSetSky() / sceneSetSky(nil) for none static int32_t apiSceneSetSky(lua_State *L) { @@ -7649,20 +7908,17 @@ static int32_t apiSceneSetTonemap(lua_State *L) { } +// x, y, z = sceneUnproject(sx, sy, distance): the world point that far along the ray through an overlay point static int32_t apiSceneUnproject(lua_State *L) { _argCheck(L, "sceneUnproject", 3, 3); return _pushVec3(L, sceneUnproject((float)_argNumber(L, "sceneUnproject", 1), (float)_argNumber(L, "sceneUnproject", 2), (float)_argNumber(L, "sceneUnproject", 3))); } +// scriptExecute(config) Runs another script after this one ends. static int32_t apiScriptExecute(lua_State *L) { - ConfigT *conf = NULL; + ConfigT *conf = _scriptConfFromTable(L, "scriptExecute"); - _argCheck(L, "scriptExecute", 1, 1); - if (!lua_istable(L, 1)) { - _luaDie(L, "scriptExecute", "Argument 1 must be a table."); - } - conf = _buildConfFromTable(L, _global.conf); queueScript(conf); destroyConf(&conf); _global.running = false; @@ -7674,13 +7930,8 @@ static int32_t apiScriptExecute(lua_State *L) { // scriptPush(config) Runs another script, then returns to this one. static int32_t apiScriptPush(lua_State *L) { - ConfigT *conf = NULL; + ConfigT *conf = _scriptConfFromTable(L, "scriptPush"); - _argCheck(L, "scriptPush", 1, 1); - if (!lua_istable(L, 1)) { - _luaDie(L, "scriptPush", "Argument 1 must be a table."); - } - conf = _buildConfFromTable(L, _global.conf); queueScript(conf); destroyConf(&conf); queueScript(_global.conf); @@ -7795,7 +8046,6 @@ static int32_t apiSingeQuit(lua_State *L) { } -// singeScreenshot() Saved after the next frame is drawn. // singeReload(): runs the game again from its script at the end of this frame static int32_t apiSingeReload(lua_State *L) { _luaTrace(L, "singeReload", "Reload requested."); @@ -7804,6 +8054,7 @@ static int32_t apiSingeReload(lua_State *L) { } +// singeScreenshot() Saved after the next frame is drawn. static int32_t apiSingeScreenshot(lua_State *L) { _luaTrace(L, "singeScreenshot", "Screenshot requested."); _global.requestScreenShot = true; @@ -7899,7 +8150,6 @@ static int32_t apiSingeWantsCrosshairs(lua_State *L) { } -// soundFullStop() Halts every sound effect channel. static int32_t apiSoftDelete(lua_State *L) { _argCheck(L, "softDelete", 1, 1); softDelete(_argSoft(L, "softDelete", 1)); @@ -7984,6 +8234,7 @@ static int32_t apiSoftUnpin(lua_State *L) { } +// soundFullStop() Halts every sound effect channel. static int32_t apiSoundFullStop(lua_State *L) { _luaTrace(L, "soundFullStop", "Halting all channels."); MIX_StopTag(videoGetMixer(), EFFECT_TAG, 0); @@ -7992,7 +8243,6 @@ static int32_t apiSoundFullStop(lua_State *L) { } -// volume = soundGetVolume() 0 to AUDIO_MAX_VOLUME. // soundGetPosition(channel): where a positioned channel sits relative to the listener (x right, y up, // z back, unit length) and its distance gain; a channel with no position returns 0, 0, 0, 1. static int32_t apiSoundGetPosition(lua_State *L) { @@ -8010,6 +8260,7 @@ static int32_t apiSoundGetPosition(lua_State *L) { } +// volume = soundGetVolume() 0 to AUDIO_MAX_VOLUME. static int32_t apiSoundGetVolume(lua_State *L) { _luaTrace(L, "soundGetVolume", "%d", _global.effectsVolume); lua_pushinteger(L, _global.effectsVolume); @@ -8035,16 +8286,21 @@ static int32_t apiSoundIsPlaying(lua_State *L) { // id = soundLoad(filename) static int32_t apiSoundLoad(lua_State *L) { - const char *name = NULL; - SoundT *sound = NULL; + const char *name = NULL; + SoundT *sound = NULL; + SDL_IOStream *io = NULL; _argCheck(L, "soundLoad", 1, 1); - name = _argString(L, "soundLoad", 1); + name = _argString(L, "soundLoad", 1); + io = vfsOpenIO(name); + if (io == NULL) { + _luaDie(L, "soundLoad", "Unable to open %s", name); + } sound = (SoundT *)calloc(1, sizeof(SoundT)); if (!sound) { _luaDie(L, "soundLoad", "Unable to allocate new sound."); } - sound->audio = MIX_LoadAudio_IO(videoGetMixer(), vfsOpenIO(name), true, true); + sound->audio = MIX_LoadAudio_IO(videoGetMixer(), io, true, true); if (!sound->audio) { _luaDie(L, "soundLoad", "%s", SDL_GetError()); } @@ -8077,7 +8333,7 @@ static int32_t apiSoundPause(lua_State *L) { // soundPlay(id [, loops]): loops 0 (the default) plays once, N repeats N more times, -1 forever static int32_t apiSoundPlay(lua_State *L) { SoundT *sound = NULL; - int32_t channel = -1; + int32_t channel = SOUND_CHANNEL_NONE; int32_t loops = 0; SDL_PropertiesID options; @@ -8125,7 +8381,6 @@ static int32_t apiSoundResume(lua_State *L) { } -// soundSetVolume(volume) 0 to AUDIO_MAX_VOLUME, applied to every effect channel. // soundSetListener([node]): positioned sounds are heard from this node; none means the scene camera static int32_t apiSoundSetListener(lua_State *L) { _argCheck(L, "soundSetListener", 0, 1); @@ -8211,6 +8466,7 @@ static int32_t apiSoundSetRange(lua_State *L) { } +// soundSetVolume(volume) 0 to AUDIO_MAX_VOLUME, applied to every effect channel. static int32_t apiSoundSetVolume(lua_State *L) { int32_t volume = 0; @@ -8288,6 +8544,10 @@ static int32_t apiSpriteDraw(lua_State *L) { now = SDL_GetTicks(); sprite->ticks += now - sprite->lastTick; sprite->lastTick = now; + // Whole loops (after a long pause, say) land on the same frame, so they need not be stepped. + if (sprite->loop && (sprite->ticks >= sprite->loopMs)) { + sprite->ticks %= sprite->loopMs; + } while (sprite->animating) { delay = sprite->animation->delays[sprite->currentFrame]; if (delay < ANIMATION_MIN_DELAY_MS) { @@ -8407,8 +8667,11 @@ static int32_t apiSpriteLoad(lua_State *L) { } sprite->animation = IMG_LoadAnimation_IO(io, false); if ((sprite->animation != NULL) && (sprite->animation->count < 2)) { - // Only one frame - keep it as a still image. - sprite->originalSurface = _surfaceCopy(sprite->animation->frames[0]); + // Only one frame - keep it as a still image (a pixel copy; a blit would premultiply the alpha). + sprite->originalSurface = SDL_DuplicateSurface(sprite->animation->frames[0]); + if (sprite->originalSurface == NULL) { + _luaDie(L, "spriteLoad", "%s", SDL_GetError()); + } _surfaceUnpack(&sprite->originalSurface); IMG_FreeAnimation(sprite->animation); sprite->animation = NULL; @@ -8417,6 +8680,7 @@ static int32_t apiSpriteLoad(lua_State *L) { for (x = 0; x < sprite->animation->count; x++) { _surfaceUnpack(&sprite->animation->frames[x]); SDL_SetSurfaceColorKey(sprite->animation->frames[x], true, COLOR_KEY_VALUE); + sprite->loopMs += (uint64_t)SDL_max(sprite->animation->delays[x], ANIMATION_MIN_DELAY_MS); } sprite->originalSurface = sprite->animation->frames[0]; } else { @@ -8429,7 +8693,10 @@ static int32_t apiSpriteLoad(lua_State *L) { if (!sprite->originalSurface) { _luaDie(L, "spriteLoad", "%s", SDL_GetError()); } - SDL_SetSurfaceColorKey(sprite->originalSurface, true, COLOR_KEY_VALUE); + if (sprite->animation == NULL) { + // An animation's frames were keyed above, this one included. + SDL_SetSurfaceColorKey(sprite->originalSurface, true, COLOR_KEY_VALUE); + } sprite->surface = sprite->originalSurface; sprite->scaleX = 1.0; sprite->scaleY = 1.0; @@ -8600,9 +8867,6 @@ static int32_t apiSpriteUnload(lua_State *L) { } -// videoDraw(id, x, y, x2, y2) - Stretch the frame into the rectangle -// videoDraw(id, x, y, centered) - Draw with the video's rotation and scale - // height = terrainGetHeight(node, x, z): the terrain's height at a world x, z, or nil off it static int32_t apiTerrainGetHeight(lua_State *L) { float height = 0.0f; @@ -8616,6 +8880,7 @@ static int32_t apiTerrainGetHeight(lua_State *L) { return 1; } + // index = vehicleAddWheel(vehicle, wheelNode, radius, width, suspensionLength) static int32_t apiVehicleAddWheel(lua_State *L) { int32_t index = 0; @@ -8639,13 +8904,13 @@ static int32_t apiVehicleDelete(lua_State *L) { // vehicleDrive(vehicle, forward, right, brake, handBrake) static int32_t apiVehicleDrive(lua_State *L) { - int32_t node = 0; - float in[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; - int32_t x; + int32_t node = 0; + float in[VEHICLE_DRIVE_INPUTS] = { 0.0f, 0.0f, 0.0f, 0.0f }; + int32_t x = 0; _argCheck(L, "vehicleDrive", 3, 5); node = _argVehicle(L, "vehicleDrive", 1); - for (x = 0; x < 4; x++) { + for (x = 0; x < VEHICLE_DRIVE_INPUTS; x++) { if (lua_gettop(L) >= 2 + x) { in[x] = (float)_argNumber(L, "vehicleDrive", 2 + x); } @@ -8726,17 +8991,17 @@ static int32_t apiVehicleSetBrakes(lua_State *L) { // vehicleSetEngine(vehicle, maxTorque, maxRpm[, minRpm]) static int32_t apiVehicleSetEngine(lua_State *L) { _argCheck(L, "vehicleSetEngine", 3, 4); - vehicleSetEngine(_argVehicle(L, "vehicleSetEngine", 1), (float)_argNumber(L, "vehicleSetEngine", 2), (float)_argNumber(L, "vehicleSetEngine", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "vehicleSetEngine", 4) : 1000.0f); + vehicleSetEngine(_argVehicle(L, "vehicleSetEngine", 1), (float)_argNumber(L, "vehicleSetEngine", 2), (float)_argNumber(L, "vehicleSetEngine", 3), (lua_gettop(L) == 4) ? (float)_argNumber(L, "vehicleSetEngine", 4) : VEHICLE_DEFAULT_MIN_RPM); return 0; } // vehicleSetGears(vehicle, {ratio, ...}[, reverseRatio][, automatic]) static int32_t apiVehicleSetGears(lua_State *L) { - int32_t node = 0; - int32_t count = 0; - float *ratios; - float reverse = 2.9f; + int32_t node = 0; + int32_t count = 0; + float *ratios = NULL; + float reverse = VEHICLE_DEFAULT_REVERSE_GEAR; bool automatic = true; _argCheck(L, "vehicleSetGears", 2, 4); @@ -8748,9 +9013,9 @@ static int32_t apiVehicleSetGears(lua_State *L) { if (lua_gettop(L) == 4) { automatic = _argBoolean(L, "vehicleSetGears", 4); } - if ((count < 1) || (count > 8) || !vehicleSetGears(node, ratios, count, reverse, automatic)) { + if ((count < 1) || (count > VEHICLE_MAX_GEARS) || !vehicleSetGears(node, ratios, count, reverse, automatic)) { SDL_free(ratios); - _luaDie(L, "vehicleSetGears", "Give one to eight forward gear ratios."); + _luaDie(L, "vehicleSetGears", "Give one to %d forward gear ratios.", VEHICLE_MAX_GEARS); } SDL_free(ratios); return 0; @@ -8797,6 +9062,8 @@ static int32_t apiVehicleSetWheel(lua_State *L) { } +// videoDraw(id, x, y, x2, y2) - Stretch the frame into the rectangle +// videoDraw(id, x, y, centered) - Draw with the video's rotation and scale static int32_t apiVideoDraw(lua_State *L) { int32_t n = lua_gettop(L); VideoT *video = NULL; @@ -9221,20 +9488,8 @@ static int32_t apiVideoSetVolume(lua_State *L) { _argCheck(L, "videoSetVolume", 3, 3); video = _argVideo(L, "videoSetVolume", 1); - left = _argInteger(L, "videoSetVolume", 2); - right = _argInteger(L, "videoSetVolume", 3); - if (left < 0) { - left = 0; - } - if (left > VIDEO_VOLUME_MAX) { - left = VIDEO_VOLUME_MAX; - } - if (right < 0) { - right = 0; - } - if (right > VIDEO_VOLUME_MAX) { - right = VIDEO_VOLUME_MAX; - } + left = SDL_clamp(_argInteger(L, "videoSetVolume", 2), 0, VIDEO_VOLUME_MAX); + right = SDL_clamp(_argInteger(L, "videoSetVolume", 3), 0, VIDEO_VOLUME_MAX); videoSetVolume(video->handle, left, right); _luaTrace(L, "videoSetVolume", "%d %d %d", video->id, left, right); @@ -9270,7 +9525,7 @@ static int32_t apiViewNew(lua_State *L) { _argCheck(L, "viewNew", 2, 2); view = viewNew(_argInteger(L, "viewNew", 1), _argInteger(L, "viewNew", 2)); if (view < 0) { - _luaDie(L, "viewNew", "No view available (is 3D available, and are fewer than four in use?)."); + _luaDie(L, "viewNew", "No view available (is 3D available, and are fewer than %d in use?).", MAX_VIEWS); } lua_pushinteger(L, view); return 1; @@ -9334,7 +9589,7 @@ ConfigT *confFromDatabase(const ConfigT *conf) { base->scriptFile = NULL; vfsInit(base->container, base->dataDirBase, NULL); luaL_openlibs(L); - if ((_luaLoadFile(L, "games.dat", NULL) != LUA_OK) || (lua_pcall(L, 0, 0, 0) != LUA_OK)) { + if ((_luaLoadFile(L, VFS_GAMES_DAT, NULL, false) != LUA_OK) || (lua_pcall(L, 0, 0, 0) != LUA_OK)) { utilDie("%s: %s", base->container, lua_tostring(L, -1)); } lua_getglobal(L, "GAMES"); @@ -9370,13 +9625,12 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co int64_t thisFrame = -1; int64_t lastFrame = -1; uint64_t frameClock = 0; - char *temp = NULL; - char *temp2 = NULL; SDL_FRect windowTarget; + SDL_FRect scaledTarget; SDL_FRect sindenWhite; SDL_FRect sindenBlack; SDL_Texture *sceneTexture = NULL; - SDL_Color sindenWhiteColor = { 255, 255, 255, SDL_ALPHA_OPAQUE }; + SDL_Color sindenWhiteColor = { COLOR_BYTE_MAX, COLOR_BYTE_MAX, COLOR_BYTE_MAX, SDL_ALPHA_OPAQUE }; SDL_Color sindenBlackColor = { 0, 0, 0, SDL_ALPHA_OPAQUE }; SDL_Event event; ManyMouseEvent mouseEvent; @@ -9386,31 +9640,17 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co // Set up globals memset(&_global, 0, sizeof(GlobalT)); - _global.colorForeground.r = SDL_ALPHA_OPAQUE; - _global.colorForeground.g = SDL_ALPHA_OPAQUE; - _global.colorForeground.b = SDL_ALPHA_OPAQUE; - _global.colorForeground.a = SDL_ALPHA_OPAQUE; - _global.effectsVolume = AUDIO_MAX_VOLUME; - _global.listenerNode = LISTENER_CAMERA; - _global.keyboardMode = KEYBOARD_NORMAL; _global.frameFileHandle = -1; _global.videoHandle = -1; - _global.fontQuality = FONT_QUALITY_SOLID; _global.mouseMode = MOUSE_SINGLE; - _global.overlayScaleX = OVERLAY_SCALE_DEFAULT; - _global.overlayScaleY = OVERLAY_SCALE_DEFAULT; _global.controllerDeadZone = CONTROLLER_DEAD_ZONE_DEFAULT; - _global.pauseEnabled = true; _global.running = true; _global.discStopped = true; _global.mouseEnabled = true; _global.window = window; _global.renderer = renderer; _global.device = device; - sceneInit(device, renderer); - physicsInit(); - particlesInit(); - navInit(); + _subsystemsInit(); // Local copy of config _global.conf = cloneConf(conf); @@ -9419,68 +9659,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co videoSetAudioCalibration(_loadAudioCalibration()); utilTrace("Audio delay: device queue %d ms, calibration %d ms, game %d ms", videoGetAudioLatency(), videoGetAudioCalibration(), videoGetAudioDelay()); - // Load controller mappings in a throwaway Lua context. - _progTrace("Creating Lua context for Singe setup"); - _global.luaContext = luaL_newstate(); - _startLuaContext(_global.luaContext); - // Load framework - NOTE! SINGE API NOT AVAILABLE AT THIS POINT! - // Any calls in the framework need to be wrapped with nil checks! - _progTrace("Loading Singe framework"); - if (luaL_loadbuffer(_global.luaContext, (const char *)Framework_singe, Framework_singe_len, "Framework.singe") || lua_pcall(_global.luaContext, 0, 0, 0)) { - utilDie("%s", lua_tostring(_global.luaContext, -1)); - } - // Load default mappings, then each override in turn. - _progTrace("Loading default control mappings"); - if (luaL_loadbuffer(_global.luaContext, (const char *)controls_cfg, controls_cfg_len, "controls.cfg") || lua_pcall(_global.luaContext, 0, 0, 0)) { - utilDie("%s", lua_tostring(_global.luaContext, -1)); - } - _loadControlsFile("controls.cfg"); - temp = utilCreateString("%s..%ccontrols.cfg", _global.conf->dataDir, utilGetPathSeparator()); - _loadControlsFile(temp); - free(temp); - temp = utilCreateString("%scontrols.cfg", _global.conf->dataDir); - _loadControlsFile(temp); - free(temp); - temp2 = utilGetUpToLastPathComponent(_global.conf->scriptFile); - temp = utilCreateString("%scontrols.cfg", temp2); - _loadControlsFile(temp); - free(temp); - free(temp2); - // Parse results - lua_getglobal(_global.luaContext, "DEAD_ZONE"); - if (lua_isnumber(_global.luaContext, -1)) { - _global.controllerDeadZone = (int32_t)lua_tonumber(_global.luaContext, -1); - } - lua_pop(_global.luaContext, 1); - _progTrace("Controller dead zone is %d", _global.controllerDeadZone); - for (x = 0; x < INPUT_COUNT; x++) { - // Each INPUT_* table holds { name = ..., value = ... } entries; collect the values. - lua_getglobal(_global.luaContext, _inputNames[x].configName); - if (!lua_istable(_global.luaContext, -1)) { - utilSay("Configuration option %s missing!", _inputNames[x].configName); - lua_pop(_global.luaContext, 1); - continue; - } - y = (int32_t)lua_rawlen(_global.luaContext, -1); - _global.controlMappings[x].input = (int32_t *)calloc((size_t)(y + 1), sizeof(int32_t)); - if (!_global.controlMappings[x].input) { - utilDie("Unable to allocate memory for control mappings."); - } - _global.controlMappings[x].inputCount = 0; - lua_pushnil(_global.luaContext); - while (lua_next(_global.luaContext, -2)) { - if (lua_istable(_global.luaContext, -1)) { - lua_getfield(_global.luaContext, -1, "value"); - if (lua_isnumber(_global.luaContext, -1) && (_global.controlMappings[x].inputCount < y)) { - _global.controlMappings[x].input[_global.controlMappings[x].inputCount++] = (int32_t)lua_tonumber(_global.luaContext, -1); - } - lua_pop(_global.luaContext, 1); - } - lua_pop(_global.luaContext, 1); - } - lua_pop(_global.luaContext, 1); - } - lua_close(_global.luaContext); + _loadControlMappings(); // Show splash screens if (!_global.conf->noLogos) { @@ -9513,27 +9692,24 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co videoHeight = _global.canvasHeight; // Should we resize the window to the video's shape? - if (conf->resolutionWasCalculated && !conf->fullScreen && !conf->fullScreenWindow) { - if (videoWidth * conf->yResolution > videoHeight * conf->xResolution) { + if (_global.conf->resolutionWasCalculated && !_global.conf->fullScreen && !_global.conf->fullScreenWindow) { + if (videoWidth * _global.conf->yResolution > videoHeight * _global.conf->xResolution) { // Video is wider than the window: keep the width, shrink the height. - conf->yResolution = conf->xResolution * videoHeight / videoWidth; + _global.conf->yResolution = _global.conf->xResolution * videoHeight / videoWidth; } else { // Video is taller: keep the height, shrink the width. - conf->xResolution = conf->yResolution * videoWidth / videoHeight; + _global.conf->xResolution = _global.conf->yResolution * videoWidth / videoHeight; } - _global.conf->xResolution = conf->xResolution; - _global.conf->yResolution = conf->yResolution; - _progTrace("Resizing window to %dx%d based on main video file", conf->xResolution, conf->yResolution); - SDL_SetWindowSize(_global.window, conf->xResolution, conf->yResolution); + _progTrace("Resizing window to %dx%d based on main video file", _global.conf->xResolution, _global.conf->yResolution); + SDL_SetWindowSize(_global.window, _global.conf->xResolution, _global.conf->yResolution); SDL_SyncWindow(_global.window); SDL_SetRenderDrawColor(_global.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); SDL_RenderClear(_global.renderer); } - // Everything renders in video coordinates unless the user wants it stretched. - if (!_global.conf->stretchVideo) { - SDL_SetRenderLogicalPresentation(_global.renderer, videoWidth, videoHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX); - } + // Everything renders in video coordinates, letterboxed unless the user wants it stretched, and + // either way mouse positions convert back into those coordinates. + SDL_SetRenderLogicalPresentation(_global.renderer, videoWidth, videoHeight, _global.conf->stretchVideo ? SDL_LOGICAL_PRESENTATION_STRETCH : SDL_LOGICAL_PRESENTATION_LETTERBOX); // Default render location is the entire window windowTarget.x = 0; @@ -9543,14 +9719,6 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co sindenWhite.x = -1; sindenBlack.x = -1; - // Overscan compensation - if (_global.conf->scaleFactor < SCALE_FACTOR_MAX) { - windowTarget.w = videoWidth * _global.conf->scaleFactor / SCALE_FACTOR_MAX; - windowTarget.h = videoHeight * _global.conf->scaleFactor / SCALE_FACTOR_MAX; - windowTarget.x = (videoWidth - windowTarget.w) / 2; - windowTarget.y = (videoHeight - windowTarget.h) / 2; - } - // Sinden Light Gun Border Setup if (_global.conf->sindenArgc > 0) { //***TODO*** ADD MOUSE SCALING TO COMPENSATE FOR BORDER @@ -9614,22 +9782,21 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co windowTarget = sindenWhite; } + // Overscan compensation shrinks whatever the game is drawn into (the whole window, or the inside + // of the Sinden border) about its centre. + if (_global.conf->scaleFactor < SCALE_FACTOR_MAX) { + scaledTarget.w = windowTarget.w * (float)_global.conf->scaleFactor / (float)SCALE_FACTOR_MAX; + scaledTarget.h = windowTarget.h * (float)_global.conf->scaleFactor / (float)SCALE_FACTOR_MAX; + scaledTarget.x = windowTarget.x + (windowTarget.w - scaledTarget.w) / 2.0f; + scaledTarget.y = windowTarget.y + (windowTarget.h - scaledTarget.h) / 2.0f; + windowTarget = scaledTarget; + } + // Create overlay surface and its texture - x = (int32_t)(videoWidth * _global.overlayScaleX); - y = (int32_t)(videoHeight * _global.overlayScaleY); + x = (int32_t)(videoWidth * OVERLAY_SCALE_DEFAULT); + y = (int32_t)(videoHeight * OVERLAY_SCALE_DEFAULT); _progTrace("Creating overlay of %dx%d", x, y); - _global.overlay = SDL_CreateSurface(x, y, SDL_PIXELFORMAT_BGRA32); - if (_global.overlay == NULL) { - utilDie("%s", SDL_GetError()); - } - SDL_SetSurfaceBlendMode(_global.overlay, SDL_BLENDMODE_BLEND); - _global.overlayTexture = SDL_CreateTexture(_global.renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, x, y); - if (_global.overlayTexture == NULL) { - utilDie("%s", SDL_GetError()); - } - sceneResize(x, y); - SDL_SetTextureBlendMode(_global.overlayTexture, SDL_BLENDMODE_BLEND); - _global.overlayDirty = true; + _overlayResize(x, y); // Mouse setup _global.mouseEnabled = !_global.conf->noMouse; @@ -9661,7 +9828,6 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co // Controllers are started by the event loop only for the first script in // the queue - so kick 'em here to be sure they're going. _startControllers(); - _suppressHeldInput(); // Sound effect tracks: a fixed pool so scripts keep getting small channel numbers. _global.effectsVolume = AUDIO_MAX_VOLUME * _global.conf->volumeNonVldp / VOLUME_MAX; @@ -9677,6 +9843,9 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co MIX_SetTrackStoppedCallback(_effectTracks[x], _effectStopped, (void *)(intptr_t)x); } + // The script's own defaults, now that everything they touch exists. + _resetScriptState(); + // Load overlay font _progTrace("Loading console font"); _global.consoleFontSurface = _loadEmbeddedPng(font_png, font_png_len); @@ -9696,12 +9865,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co } // Start script - _progTrace("Running %s", _global.conf->scriptFile); - lua_pushcfunction(_global.luaContext, _luaTraceback); - if (_luaLoadFile(_global.luaContext, _global.conf->scriptFile, NULL) || lua_pcall(_global.luaContext, 0, 0, -2)) { - utilDie("Error running script: %s", lua_tostring(_global.luaContext, -1)); - } - lua_settop(_global.luaContext, 0); + _runScript(true); // Game Loop _progTrace("Script is running"); @@ -9716,8 +9880,6 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co // SDL Event Loop while (SDL_PollEvent(&event)) { - // Mouse positions arrive in window pixels; the game works in the video's coordinates. - SDL_ConvertEventToRenderCoordinates(_global.renderer, &event); switch (event.type) { case SDL_EVENT_GAMEPAD_AXIS_MOTION: slot = _controllerSlot(event.gaxis.which); @@ -9798,6 +9960,8 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co case SDL_EVENT_MOUSE_MOTION: if (_global.mouseEnabled && (_global.mouseMode == MOUSE_SINGLE)) { + // Positions arrive in window pixels; the game works in the video's coordinates. + SDL_ConvertEventToRenderCoordinates(_global.renderer, &event); x = (int32_t)(event.motion.x * _global.overlayScaleX); y = (int32_t)(event.motion.y * _global.overlayScaleY); xr = (int32_t)(event.motion.xrel * _global.overlayScaleX); @@ -9906,11 +10070,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co // Deliver sound completions on this thread. They wait out an engine pause. if (!_global.frozen) { - videoLockAudio(); - finishedCount = _global.soundQueueCount; - memcpy(finished, _global.soundQueue, sizeof(int32_t) * (size_t)finishedCount); - _global.soundQueueCount = 0; - videoUnlockAudio(); + finishedCount = _soundQueueDrain(finished); for (x = 0; x < finishedCount; x++) { _callLua("onSoundCompleted", "i", finished[x]); } @@ -10025,11 +10185,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co // Free overlay & overlay font _progTrace("Destroying overlay"); SDL_DestroyTexture(_global.pauseTexture); - modelQuit(); - navQuit(); - particlesQuit(); - physicsQuit(); - sceneQuit(); + _subsystemsQuit(); SDL_DestroyTexture(_global.overlayTexture); SDL_DestroySurface(_global.overlay); _progTrace("Destroying console font"); @@ -10070,4 +10226,8 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co free(_global.watched[x].name); } free(_global.watched); + SDL_free(_global.navDrawVertices); + SDL_free(_global.quadVertices); + SDL_free(_global.quadIndices); + SDL_free(_global.frameStarts); } diff --git a/src/singe.h b/src/singe.h index 3af5fbae9..fa3ac7601 100644 --- a/src/singe.h +++ b/src/singe.h @@ -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, diff --git a/src/util.c b/src/util.c index a69c8379c..b7cb2e069 100644 --- a/src/util.c +++ b/src/util.c @@ -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); } diff --git a/src/vfs.c b/src/vfs.c index c891bc011..4c12d9dad 100644 --- a/src/vfs.c +++ b/src/vfs.c @@ -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 #include @@ -54,12 +55,10 @@ #define fileTell ftello #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 CACHE_DIRECTORY "cache" +#define OVERLAY_DIRECTORY "files" +#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]); @@ -192,72 +258,81 @@ static DatabaseT *_databaseOpen(const char *path) { uri = utilCreateString("file:%s?immutable=1", encoded); free(encoded); - db = (DatabaseT *)calloc(1, sizeof(DatabaseT)); + 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()); - db->next = _databases; + stem = vfsDatabaseStem(path); + db->loose = utilCreateString("%s%c", stem, utilGetPathSeparator()); + free(stem); + db->next = _databases; _databases = db; return db; } -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,42 +574,49 @@ 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++) { - if ((norm[i] == '/') && (strncasecmp(norm + i - extLen, VFS_DATABASE_EXTENSION, extLen) == 0)) { - prefix = utilStrndup(norm, i); - if (utilFileExists(prefix)) { - target->db = _databaseOpen(prefix); - if (target->db != NULL) { - inner = norm + i + 1; - if (target->db->overlay == NULL) { - target->db->overlay = _overlayFor(_dataDirBase, _dataDir, prefix, false, OVERLAY_DIRECTORY); - target->db->cache = _overlayFor(_dataDirBase, _dataDir, prefix, false, CACHE_DIRECTORY); - } - free(prefix); - break; + 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)) { + target->db = _databaseOpen(prefix); + if (target->db != NULL) { + inner = norm + i + 1; + if (target->db->overlay == NULL) { + target->db->overlay = _overlayFor(_dataDirBase, _dataDir, prefix, false, OVERLAY_DIRECTORY); + target->db->cache = _overlayFor(_dataDirBase, _dataDir, prefix, false, CACHE_DIRECTORY); } + free(prefix); + break; } - free(prefix); - prefix = NULL; } + free(prefix); + 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)) { @@ -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; - uint8_t *data = NULL; - size_t bytes = 0; + 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)) { - path = 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; + 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) { @@ -598,10 +753,11 @@ char *vfsFilePath(const char *name, bool forWriting) { utilDie("Unable to copy %s to %s.", name, target.cache); } free(data); - path = target.cache; + 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; @@ -853,7 +1021,10 @@ VfsStreamT *vfsStreamOpen(const char *name) { if (!_resolve(name, &target)) { return NULL; } - stream = (VfsStreamT *)calloc(1, sizeof(VfsStreamT)); + 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; diff --git a/src/vfs.h b/src/vfs.h index 5aee084f7..be477d272 100644 --- a/src/vfs.h +++ b/src/vfs.h @@ -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 +// 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); diff --git a/src/videoPlayer.c b/src/videoPlayer.c index 188c64d98..b02ebacbf 100644 --- a/src/videoPlayer.c +++ b/src/videoPlayer.c @@ -32,8 +32,7 @@ // demuxed and decoded separately on the main thread and fed to an SDL3_mixer track. #include -#include -#include +#include // unlink #ifdef __linux__ #include #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,12 +370,8 @@ 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); - } + avcodec_free_context(&v->audioCodec); + swr_free(&v->swr); } @@ -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); - } + 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; + VideoPlayerT *v = (VideoPlayerT *)data; + int64_t want = -1; + 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); - io = avio_alloc_context(buffer, AVIO_BUFFER_BYTES, 0, stream, _avioRead, NULL, _avioSeek); + 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; + AVDictionaryEntry *tag = NULL; + int32_t x = 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,17 +1201,25 @@ 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; - int32_t x = 0; + VideoPlayerT *v = (VideoPlayerT *)codec->opaque; + const AVPixFmtDescriptor *desc = NULL; + int32_t x = 0; for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) { if (formats[x] == v->hwPixelFormat) { 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. - _loadAudio(v, audioFilename ? audioFilename : videoFilename); + // 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; diff --git a/src/videoPlayer.h b/src/videoPlayer.h index 3d90571a6..eca71c9e7 100644 --- a/src/videoPlayer.h +++ b/src/videoPlayer.h @@ -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);