Compare commits
40 Commits
coverage-t
...
93e8e4afa4
| Author | SHA1 | Date | |
|---|---|---|---|
|
93e8e4afa4
|
|||
|
5dfa49482c
|
|||
|
76c6240280
|
|||
|
fb58bb01b0
|
|||
|
80fdf2e098
|
|||
|
af304dc2f9
|
|||
|
8a920860c5
|
|||
|
bcd49fc5b1
|
|||
|
c6227545a6
|
|||
|
f35443e84d
|
|||
|
57bf1c7649
|
|||
|
a3eada1b3f
|
|||
|
34a076b851
|
|||
|
42b53dcb20
|
|||
|
ede3452c49
|
|||
|
18399f2726
|
|||
|
54df954ed6
|
|||
|
55675cc9de
|
|||
|
f0858b0d38
|
|||
|
582008a411
|
|||
|
1066ac716e
|
|||
|
996cacb10c
|
|||
|
42b60f725d
|
|||
|
4208d9d471
|
|||
|
f56f88710f
|
|||
|
2a3ca48d8f
|
|||
|
dba0f8db89
|
|||
|
1ddc64010a
|
|||
|
17e6e04c79
|
|||
|
5f03475e0f
|
|||
|
6dfe7487ae
|
|||
|
c2b16d3c18
|
|||
|
2be9831c0c
|
|||
|
c5a7b6053d
|
|||
|
9b124f2e27
|
|||
|
b014eb2360
|
|||
|
e423f9594e
|
|||
|
772f960865
|
|||
|
28bde4176d
|
|||
|
8d21d5c7dd
|
16
.dir-locals.el
Normal file
16
.dir-locals.el
Normal file
@@ -0,0 +1,16 @@
|
||||
;;; Directory Local Variables -*- no-byte-compile: t -*-
|
||||
;;; See AGENTS.md -> "Coding Style" for the rationale.
|
||||
;;;
|
||||
;;; The canonical style is cc-mode "stroustrup" with tabs enabled: 4 columns per
|
||||
;;; level, tabs 8 columns wide, so depth 1 is four spaces, depth 2 is one tab,
|
||||
;;; depth 3 is a tab plus four spaces. A correctly formatted file is a fixed
|
||||
;;; point of `indent-region' under these settings.
|
||||
;;;
|
||||
;;; Reindent a whole file with C-x h C-M-\, or the tree with
|
||||
;;; `scripts/reindent.sh'.
|
||||
|
||||
((c-mode . ((c-file-style . "stroustrup")
|
||||
(indent-tabs-mode . t)
|
||||
(tab-width . 8)
|
||||
(fill-column . 100)
|
||||
(require-final-newline . t))))
|
||||
@@ -28,7 +28,16 @@ jobs:
|
||||
cmake --build build --parallel
|
||||
- name: Build API documentation
|
||||
run: doxygen Doxyfile
|
||||
# The perf suites run here too -- they are ordinary CTest tests -- but this
|
||||
# is a Debug build with coverage instrumentation, where a timing is a
|
||||
# measurement of gcov. AKGL_BENCH_SCALE cuts their iteration counts so
|
||||
# they contribute path coverage without spending ten minutes proving
|
||||
# nothing; benchutil.h already refuses to enforce budgets in an
|
||||
# unoptimized build. The performance job below is where the timings are
|
||||
# taken and the budgets are checked.
|
||||
- name: Test (JUnit)
|
||||
env:
|
||||
AKGL_BENCH_SCALE: '0.02'
|
||||
run: |
|
||||
export LD_LIBRARY_PATH="$PWD/build:$PWD/build/deps/SDL:$PWD/build/deps/SDL_image:$PWD/build/deps/SDL_mixer:$PWD/build/deps/SDL_ttf"
|
||||
ctest \
|
||||
@@ -53,6 +62,118 @@ jobs:
|
||||
path: build/coverage/
|
||||
if-no-files-found: warn
|
||||
|
||||
# The benchmarks, in the only build where their numbers mean anything.
|
||||
# tests/benchutil.h enforces a budget per measurement only when it is compiled
|
||||
# optimized and running at full scale, which is exactly this job and no other:
|
||||
# the coverage job above is Debug, and the memory job below is under valgrind.
|
||||
# A budget is roughly ten times the baseline recorded in PERFORMANCE.md, which
|
||||
# is what makes a shared, slower runner viable -- it catches an algorithmic
|
||||
# regression and ignores a busy machine. If a budget still proves flaky here,
|
||||
# raise that one budget with a measurement behind it and re-record the
|
||||
# baseline; do not drop the gate.
|
||||
performance:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
cmake gcc pkg-config \
|
||||
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
||||
libpng-dev libtiff-dev libwebp-dev \
|
||||
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
||||
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
|
||||
libxss-dev libxtst-dev
|
||||
- name: Configure and build
|
||||
run: |
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build build --parallel
|
||||
# --verbose rather than --output-on-failure: a passing benchmark's output
|
||||
# is the entire point of running it, and --output-on-failure prints
|
||||
# nothing at all for a green run. pipefail because the step ends in a
|
||||
# pipe, whose status would otherwise be tee's -- always 0 -- and a blown
|
||||
# budget would be reported as a passing step.
|
||||
- name: Benchmarks (JUnit)
|
||||
run: |
|
||||
set -o pipefail
|
||||
export LD_LIBRARY_PATH="$PWD/build:$PWD/build/deps/SDL:$PWD/build/deps/SDL_image:$PWD/build/deps/SDL_mixer:$PWD/build/deps/SDL_ttf"
|
||||
ctest \
|
||||
--test-dir build \
|
||||
-L perf \
|
||||
--verbose \
|
||||
--output-junit "$(pwd)/perf-junit.xml" \
|
||||
2>&1 | tee "$(pwd)/perf-baseline.txt"
|
||||
- name: Publish benchmark results
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
with:
|
||||
report_paths: 'perf-junit.xml'
|
||||
annotate_only: true
|
||||
detailed_summary: true
|
||||
include_passed: true
|
||||
fail_on_failure: 'true'
|
||||
# Kept whether the job passed or failed: the tables are the point. A run
|
||||
# that went red says which measurement moved, and a run that went green is
|
||||
# the next baseline if somebody re-records PERFORMANCE.md.
|
||||
- name: Upload benchmark tables
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: performance-baseline
|
||||
path: perf-baseline.txt
|
||||
if-no-files-found: warn
|
||||
|
||||
# Every suite under valgrind. No memory-check programs of their own: the perf
|
||||
# suites scale themselves down when they detect valgrind, which turns them
|
||||
# into the broadest path coverage in the tree at a cost this job can afford.
|
||||
#
|
||||
# This job gates. A definite leak, a read past the end of an allocation, or a
|
||||
# branch on uninitialised memory fails the build on the push that introduced
|
||||
# it -- not on the day somebody gets around to caring. The six findings this
|
||||
# job had on its first run were fixed to make that possible rather than
|
||||
# excused into a warning.
|
||||
memory_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install memory-check dependencies
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
cmake gcc pkg-config valgrind \
|
||||
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
||||
libpng-dev libtiff-dev libwebp-dev \
|
||||
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
||||
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
|
||||
libxss-dev libxtst-dev
|
||||
# RelWithDebInfo rather than Debug: valgrind needs the symbols, and -O0
|
||||
# would leave every inlined frame in the stacks it prints.
|
||||
- name: Configure and build
|
||||
run: |
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build build --parallel
|
||||
# The character suite is excluded here for the same reason as in the
|
||||
# coverage job: it fails deliberately, and a failing suite would be
|
||||
# reported as "valgrind found nothing but ctest exited non-zero".
|
||||
- name: Memory check
|
||||
run: scripts/memcheck.sh -E '^character$'
|
||||
- name: Upload valgrind logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: valgrind-logs
|
||||
path: build/Testing/Temporary/MemoryChecker.*.log
|
||||
if-no-files-found: warn
|
||||
|
||||
mutation_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,3 +1,11 @@
|
||||
./build/*
|
||||
# A leading ./ is not a valid gitignore pattern, so "./build/*" matched nothing
|
||||
# and every out-of-tree build tree showed up as untracked. This also covers the
|
||||
# instrumented trees scripts/coverage.py and the AKGL_COVERAGE option create.
|
||||
build*/
|
||||
.aider*
|
||||
*~
|
||||
|
||||
# Generated by configure_file into the build tree. include/ precedes the build
|
||||
# tree on the include path, so a stray copy here would silently shadow the real
|
||||
# one and pin every consumer to whatever version it was generated at.
|
||||
include/akgl/version.h
|
||||
|
||||
329
AGENTS.md
329
AGENTS.md
@@ -2,7 +2,43 @@
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
This is a C library intended to support the development of video games. Public C headers live in `include/akgl/`; keep declarations there aligned with their implementations in `src/`. Tests are standalone C programs under `tests/`, with JSON, image, and map fixtures in `tests/assets/`. The `util/` directory contains the `charviewer` utility and its sample assets. Third-party and companion libraries are vendored in `deps/`. Treat `build/`, generated `akgl.pc` files, and `include/akgl/SDL_GameControllerDB.h` as build outputs rather than hand-maintained source.
|
||||
This is a C library intended to support the development of video games. Public C headers live in `include/akgl/`; keep declarations there aligned with their implementations in `src/`. Tests are standalone C programs under `tests/`, with JSON, image, and map fixtures in `tests/assets/`. The `util/` directory contains the `charviewer` utility and its sample assets. Third-party and companion libraries are vendored in `deps/`. Treat `build/` and generated `akgl.pc` files as build outputs rather than hand-maintained source; `include/akgl/SDL_GameControllerDB.h` is generated too, but is tracked on purpose — see below.
|
||||
|
||||
## Generated and Vendored Sources
|
||||
|
||||
### `include/akgl/SDL_GameControllerDB.h` is generated, and is tracked deliberately
|
||||
|
||||
`mkcontrollermappings.sh` regenerates this header by fetching the community
|
||||
controller database from `raw.githubusercontent.com`. **It is committed to the
|
||||
repository on purpose**: it is the offline fallback that keeps the library
|
||||
buildable if upstream is renamed, rate-limited, taken down, or simply
|
||||
unreachable from the build machine. Do not delete it, do not add it to
|
||||
`.gitignore`, and do not "clean up" the fact that a generated file is tracked.
|
||||
|
||||
Working rules:
|
||||
|
||||
- **Never hand-edit it.** It is machine-written; changes belong in
|
||||
`mkcontrollermappings.sh`.
|
||||
- **Do not commit incidental regeneration churn.** The build regenerates the
|
||||
header on every run, and the script stamps `$(date)` into a comment, so an
|
||||
ordinary build leaves the file modified with nothing of substance changed.
|
||||
Revert that before committing: `git checkout -- include/akgl/SDL_GameControllerDB.h`.
|
||||
- **Update it as a deliberate, standalone commit** when you actually want newer
|
||||
mappings, so the diff is reviewable and bisectable.
|
||||
- **Never commit a copy with `AKGL_SDL_GAMECONTROLLER_DB_LEN 0`.** That is the
|
||||
signature of a failed fetch, not an empty upstream — see the defect note
|
||||
below. Check the constant before staging this file.
|
||||
- Formatting tooling ignores it: `scripts/reindent.sh` and the pre-commit hook
|
||||
both skip it.
|
||||
|
||||
> **Known defect (tracked in `TODO.md`).** The safety net is currently
|
||||
> self-defeating. `mkcontrollermappings.sh` has no `set -e` and does not check
|
||||
> `curl`'s exit status, so when the fetch fails it writes a header with
|
||||
> `AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty array **over the good tracked
|
||||
> copy, and exits 0**. Because CMake re-runs the generator on every build, an
|
||||
> offline build silently destroys the very fallback the file exists to provide.
|
||||
> Until that is fixed, treat a dirty `SDL_GameControllerDB.h` after a build as
|
||||
> suspect and check the length constant before committing it.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
@@ -26,13 +62,266 @@ ctest --test-dir build --output-on-failure
|
||||
|
||||
Run one test while iterating, for example `ctest --test-dir build -R sprite --output-on-failure`. The `rebuild.sh` script also installs into a developer-specific `/home/andrew/local` prefix and removes existing outputs; prefer the portable commands above unless that exact workflow is intended.
|
||||
|
||||
Run only the performance suites with `ctest --test-dir build -L perf --output-on-failure`, or leave them out of an ordinary run with `-LE perf`. They take about 30 seconds together, print a table of nanoseconds per operation, and fail only when a measurement exceeds a budget set at roughly ten times the recorded baseline. `AKGL_BENCH_SCALE` scales every iteration count — `AKGL_BENCH_SCALE=0.1 ctest --test-dir build -L perf` for a quick look — and below 1.0 the budgets are reported but not enforced. The recorded baseline and what it means are in `PERFORMANCE.md`.
|
||||
|
||||
Check for memory defects with `cmake --build build --target memcheck`, or
|
||||
`scripts/memcheck.sh` directly — it takes ctest's selection flags, so
|
||||
`scripts/memcheck.sh -R tilemap` and `scripts/memcheck.sh -LE perf` both work.
|
||||
It runs the suites that already exist under valgrind (`ctest -T memcheck`) with
|
||||
the headless drivers forced, and exits non-zero when valgrind finds a definite
|
||||
leak, an invalid access, or a read of uninitialised memory — which `ctest -T
|
||||
memcheck` on its own will not do. The whole run takes about thirty seconds
|
||||
because the perf suites scale themselves down under valgrind. Suppressions for
|
||||
third-party findings live in `scripts/valgrind.supp`; add one only when the
|
||||
finding genuinely cannot be fixed from this repository.
|
||||
|
||||
Run mutation testing with `cmake --build build --target mutation`. For a quick smoke run, use `scripts/mutation_test.py --target src/tilemap.c --max-mutants 10`; the harness mutates only a scratch copy and excludes the intentionally failing character test.
|
||||
|
||||
Generate HTML and Cobertura coverage reports with `cmake -S . -B build-coverage -DAKGL_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug`, then build and run CTest. Reports are written to `build-coverage/coverage/`; this mode requires `gcovr` and GCC or Clang.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
## Continuous integration
|
||||
|
||||
Follow the surrounding C style: braces on the next line for function bodies, spaces inside control-flow parentheses, and short, focused functions. Preserve the indentation of the file being edited; older files contain both spaces and tabs. Public symbols use the `akgl_` prefix, public types use `akgl_TypeName`, and constants/macros use `AKGL_UPPER_SNAKE_CASE`. Name matching header/source pairs by feature, such as `include/akgl/sprite.h` and `src/sprite.c`. No repository-wide formatter or linter is configured, so avoid unrelated formatting churn.
|
||||
`.gitea/workflows/ci.yaml` runs four jobs on every push, and each one exists
|
||||
because the others cannot do its work:
|
||||
|
||||
| Job | Build | What it is for |
|
||||
|---|---|---|
|
||||
| `cmake_build` | Debug + `AKGL_COVERAGE=ON` | The unit suites and the coverage report. The perf suites run here too, at `AKGL_BENCH_SCALE=0.02`, for path coverage only — a timing taken under gcov is a measurement of gcov. |
|
||||
| `performance` | RelWithDebInfo | `ctest -L perf`. The only job where budgets are enforced, because `tests/benchutil.h` enforces them only when compiled optimized at full scale. Keeps the tables as an artifact. |
|
||||
| `memory_check` | RelWithDebInfo | `scripts/memcheck.sh`, every suite under valgrind. Gates: a definite leak, an invalid access or a branch on uninitialised memory fails the build. Keeps the valgrind logs as an artifact. |
|
||||
| `mutation_test` | Debug | One focused source file, so CI stays bounded. |
|
||||
|
||||
The `character` suite is excluded wherever a whole run is selected: it fails
|
||||
deliberately, and pinning current-but-wrong behavior is not what it is for.
|
||||
|
||||
## Coding Style
|
||||
|
||||
The canonical style is Emacs `cc-mode` **`stroustrup`**, with tabs enabled. This
|
||||
is not advisory — new and edited code must match what `cc-mode` produces, so that
|
||||
reindenting a region never shows up as a diff.
|
||||
|
||||
### Emacs setup
|
||||
|
||||
`.dir-locals.el` in the repository root already applies the style to every
|
||||
`c-mode` buffer, so nothing needs configuring by hand:
|
||||
|
||||
```elisp
|
||||
((c-mode . ((c-file-style . "stroustrup")
|
||||
(indent-tabs-mode . t)
|
||||
(tab-width . 8)
|
||||
(fill-column . 100)
|
||||
(require-final-newline . t))))
|
||||
```
|
||||
|
||||
Interactively: `C-x h C-M-\` (`mark-whole-buffer` + `indent-region`) reindents
|
||||
the buffer. A correctly formatted file is a fixed point of that operation —
|
||||
reindenting must produce no change.
|
||||
|
||||
### Reindenting from the command line
|
||||
|
||||
```sh
|
||||
scripts/reindent.sh # reindent every tracked C source in place
|
||||
scripts/reindent.sh src/tilemap.c # reindent only the named files
|
||||
scripts/reindent.sh --check # list non-conforming files, change nothing
|
||||
```
|
||||
|
||||
`--check` exits 1 when something needs reindenting and 2 if Emacs is missing, so
|
||||
it is usable from CI. The script drives Emacs in batch mode through
|
||||
`scripts/reindent.el`, which reindents, normalises leading whitespace to the
|
||||
canonical tab/space mix, strips trailing whitespace, and ensures a final
|
||||
newline. `include/akgl/SDL_GameControllerDB.h` is generated and always skipped.
|
||||
|
||||
Note that `reindent.el` deliberately does **not** use Emacs' `tabify`: that
|
||||
function's `tabify-regexp` is `" [ \t]+"`, which is not anchored to the start of
|
||||
a line, so it rewrites runs of spaces anywhere and destroys the hand-aligned
|
||||
value columns in the bit-flag tables in `actor.h` and `iterator.h`. Only leading
|
||||
whitespace is ever rewritten.
|
||||
|
||||
### The pre-commit hook
|
||||
|
||||
`scripts/hooks/pre-commit` reindents staged C sources before the commit is
|
||||
written. Enable it once per clone:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath scripts/hooks
|
||||
```
|
||||
|
||||
It inspects the *staged* content rather than the working tree, so what lands in
|
||||
the commit is what was checked. When a file needs reindenting it is fixed in the
|
||||
working tree and re-staged — but only if the index and working tree agree for
|
||||
that file. If they differ, re-staging would sweep unstaged work into the commit,
|
||||
so the hook stops and tells you to reindent and stage it yourself. It steps
|
||||
aside during a merge, and warns rather than blocking if Emacs is unavailable.
|
||||
`git commit --no-verify` bypasses it.
|
||||
|
||||
For reference, `cc-mode` defines the style as:
|
||||
|
||||
```elisp
|
||||
("stroustrup"
|
||||
(c-basic-offset . 4)
|
||||
(c-comment-only-line-offset . 0)
|
||||
(c-offsets-alist . ((statement-block-intro . +)
|
||||
(substatement-open . 0)
|
||||
(substatement-label . 0)
|
||||
(label . 0)
|
||||
(statement-cont . +))))
|
||||
```
|
||||
|
||||
### Indentation and whitespace
|
||||
|
||||
- **4 columns per level** (`c-basic-offset` 4).
|
||||
- **Tabs are 8 columns wide and are used for indentation** (`indent-tabs-mode`
|
||||
`t`, `tab-width` 8). Emacs emits the largest possible run of tabs and pads the
|
||||
remainder with spaces, which produces this ladder. Editors that expand tabs, or
|
||||
that assume a 4-column tab, will silently corrupt it:
|
||||
|
||||
| Depth | Columns | Bytes |
|
||||
|---|---|---|
|
||||
| 1 | 4 | 4 spaces |
|
||||
| 2 | 8 | `TAB` |
|
||||
| 3 | 12 | `TAB` + 4 spaces |
|
||||
| 4 | 16 | `TAB` `TAB` |
|
||||
| 5 | 20 | `TAB` `TAB` + 4 spaces |
|
||||
|
||||
- No trailing whitespace. Files end with a single newline.
|
||||
- `case` labels sit at the same column as their `switch` (`label . 0`).
|
||||
- Continuation lines of a wrapped expression indent one level (`statement-cont . +`).
|
||||
|
||||
Most of `src/` already conforms. The known non-conforming files are
|
||||
`src/json_helpers.c`, `src/util.c` (from `akgl_rectangle_points` onward),
|
||||
`src/assets.c`, `src/staticstring.c`, parts of `src/actor.c`, and the headers
|
||||
`include/akgl/util.h` and `include/akgl/staticstring.h` — all of which use a
|
||||
2-column offset. Convert a file to the canonical style in its own commit, not
|
||||
mixed into a behavioral change.
|
||||
|
||||
### Braces
|
||||
|
||||
- **Function bodies open on their own line, in column 0.**
|
||||
- **Control statements keep the brace on the same line**, one space before it.
|
||||
- **`else`, `else if`, and `while` of a `do`/`while` stay on the closing-brace
|
||||
line**: `} else {`, not a bare `else` on the next line. Note that this is a
|
||||
house convention rather than something `cc-mode` enforces — the `stroustrup`
|
||||
style governs indentation only and will not move a brace or an `else` for you.
|
||||
- **Always brace, even single-statement bodies.**
|
||||
|
||||
```c
|
||||
akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
|
||||
{
|
||||
if ( obj->curSpriteFrameId == 0 ) {
|
||||
obj->curSpriteReversing = false;
|
||||
} else if ( obj->parent != NULL ) {
|
||||
obj->curSpriteFrameId -= 1;
|
||||
} else {
|
||||
obj->curSpriteFrameId += 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Spacing
|
||||
|
||||
- **Spaces inside control-flow parentheses**: `if ( x == y ) {`, `while ( done == false ) {`,
|
||||
`for ( i = 0; i < len; i++ ) {`. This is the dominant existing convention
|
||||
(140 sites to 7) and `cc-mode` will not add or remove it.
|
||||
- No space between a function name and its argument list, at both call and
|
||||
definition sites, and no padding inside call parentheses: `SDL_Log("x %d", n)`.
|
||||
- Binary operators and assignment are surrounded by single spaces; unary
|
||||
operators are not separated from their operand.
|
||||
- **The pointer `*` binds to the identifier**, not the type: `char *name`,
|
||||
`akgl_Actor **dest`. Never `char* name`.
|
||||
- When a call is too long for one line, put each argument on its own line
|
||||
indented one level past the callee, with the closing `)` on its own line. This
|
||||
is the established shape for the `FAIL_*` and `CATCH` macros:
|
||||
|
||||
```c
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
|
||||
AKERR_KEY,
|
||||
"Unable to add actor to registry"
|
||||
);
|
||||
```
|
||||
|
||||
### No repository-wide formatter
|
||||
|
||||
There is no `clang-format` or linter wired into the build, and `cc-mode`'s
|
||||
indentation engine is not exactly reproducible by `clang-format`. Do not
|
||||
introduce one without discussion, and do not reformat code you are not otherwise
|
||||
changing — unrelated whitespace churn makes review harder and is the reason the
|
||||
style drifted in the first place.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- **Public functions**: `akgl_<subsystem>_<verb>`, all lower snake_case —
|
||||
`akgl_actor_set_character`, `akgl_heap_next_string`. No camelCase, and never
|
||||
embed a type name (`akgl_Actor_cmhf_left_on` is wrong; it should be
|
||||
`akgl_actor_cmhf_left_on`).
|
||||
- **Public types**: `akgl_TypeName` — `akgl_Actor`, `akgl_SpriteSheet`,
|
||||
`akgl_PhysicsBackend`. Every type exported from a header takes the prefix;
|
||||
bare names like `point` and `RectanglePoints` are defects, not precedent.
|
||||
- **Constants and macros**: `AKGL_UPPER_SNAKE_CASE`. A constant belongs to the
|
||||
subsystem it describes: a character limit is `AKGL_CHARACTER_MAX_*`, not
|
||||
`AKGL_SPRITE_MAX_CHARACTER_*`. Name a constant for what its value *is* — a
|
||||
nanoseconds-per-millisecond scale factor is `AKGL_TIME_ONEMS_NS`.
|
||||
- **Exported globals** take the `akgl_` prefix like any other public symbol. Do
|
||||
not add bare names (`renderer`, `camera`, `window`), leading-underscore names
|
||||
(reserved at file scope), or SCREAMING_SNAKE names for mutable objects —
|
||||
`AKGL_UPPER_SNAKE_CASE` is for constants.
|
||||
- **`static` helpers drop the `akgl_` prefix**, which exists only to avoid
|
||||
external collisions.
|
||||
- **Include guards**: `_AKGL_<FILE>_H_`, matching the file name. Guard names
|
||||
without the project prefix risk colliding with system headers.
|
||||
- **Parameter names must match between the declaration and the definition** —
|
||||
Doxygen publishes the header spelling. Conventional names: `dest` for an
|
||||
output parameter (not `dst`), `self` for a backend receiver, `obj` for the
|
||||
instance being initialized or inspected, `e` for an incoming error context to
|
||||
be inspected.
|
||||
- **The local error context is named `errctx`** (92 sites to 45). New code uses
|
||||
`errctx`; convert `e` when you touch a function for other reasons.
|
||||
- **Header/source pairs are named by feature**: `include/akgl/sprite.h` and
|
||||
`src/sprite.c`.
|
||||
|
||||
## Error-Handling Protocol
|
||||
|
||||
The `akerror` control-flow macros have a shape that must be followed exactly,
|
||||
because the failure mode is silent.
|
||||
|
||||
- Inside an `ATTEMPT` block use the **`_BREAK`** variants (`FAIL_ZERO_BREAK`,
|
||||
`FAIL_NONZERO_BREAK`, `FAIL_BREAK`) and `CATCH`. Outside it use the
|
||||
**`_RETURN`** variants.
|
||||
- **Never use a `*_RETURN` macro inside an `ATTEMPT` block.** It returns past the
|
||||
`CLEANUP` block, so every release, `fclose`, and free in `CLEANUP` is skipped.
|
||||
This has already caused a heap-string leak on the success path of
|
||||
`akgl_get_json_tilemap_property`.
|
||||
- `CLEANUP` must precede `PROCESS`. Transposing them moves the cleanup body into
|
||||
the `PROCESS` switch, where it runs only when an error context exists.
|
||||
- `CATCH` reports failure by `break`ing, which binds to the innermost enclosing
|
||||
loop or `switch`. A `CATCH` written directly inside a `while` exits the loop
|
||||
rather than the function — put the `ATTEMPT` block inside the loop.
|
||||
- **Never `return` from inside a `HANDLE` block either.** `FINISH` ends with
|
||||
`RELEASE_ERROR`, so leaving before it means the handled context is never given
|
||||
back to `AKERR_ARRAY_ERROR` — one leaked slot per call, and the 129th call
|
||||
aborts the process with "Unable to pull an error context from the array!".
|
||||
`SUCCEED_RETURN` is safe because it releases first; a bare `return`, or a
|
||||
`return f(...)` that tail-calls the fallback, is not. Set a flag in the
|
||||
`HANDLE` block and act on it after `FINISH`. This has already cost a
|
||||
process-killing leak in `akgl_path_relative`; see TODO.md, "Performance".
|
||||
- Validate every pointer parameter before dereferencing it, including the ones a
|
||||
sibling function happens not to check.
|
||||
|
||||
## API Surface
|
||||
|
||||
Every function with external linkage must be declared in a header, and every
|
||||
declaration must have a definition. A non-`static` function that appears in no
|
||||
header is still in the ABI but is unreachable by callers; a declaration with no
|
||||
definition is a link error for anyone compiling against the header alone. If a
|
||||
function is exposed only so tests can reach it, declare it under the existing
|
||||
"part of the internal API" comment block in the relevant header.
|
||||
|
||||
Headers must be self-contained: include what you use, so that a translation unit
|
||||
including a single `akgl` header compiles without a particular include order.
|
||||
Within the project use the angled form, `#include <akgl/sibling.h>`.
|
||||
|
||||
Declare no-argument functions as `(void)`, not `()`.
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -43,6 +332,40 @@ Follow the surrounding C style: braces on the next line for function bodies, spa
|
||||
|
||||
Tests use simple executable return codes and are registered through CTest in `CMakeLists.txt`; there is no declared coverage threshold. Add focused tests as `tests/<feature>.c`, create a matching `test_<feature>` target, and register it with `add_test`. Put reusable fixtures in `tests/assets/` and keep paths compatible with tests launched from the build tree. Coverage mode wraps the suite in a CTest fixture so counters are reset before tests and reports are generated afterward.
|
||||
|
||||
### Performance suites
|
||||
|
||||
`tests/perf.c` (nothing that draws) and `tests/perf_render.c` (everything that
|
||||
does) are benchmarks, built and registered like any other suite but listed in
|
||||
`AKGL_PERF_SUITES` so they carry the `perf` label and a longer timeout. The
|
||||
harness is `tests/benchutil.h`. Three rules keep the numbers honest, and all
|
||||
three were learned by getting them wrong first:
|
||||
|
||||
- **Nothing that checks an error goes inside the clock.** `PASS` and `CATCH`
|
||||
call `akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` — more work
|
||||
than several of the calls being measured. Use `BENCH_LOOP`, which stashes the
|
||||
context and stops at the first failure, and hand it to `PASS` afterwards.
|
||||
- **Flush the renderer before stopping the clock.** SDL batches: a `draw_*` call
|
||||
queues a command and returns. `BENCH_FLUSH_STOP` in `tests/perf_render.c` is
|
||||
there because the first version of that suite measured queueing, reported a
|
||||
tilemap frame 250 times faster than it is, and paid the real cost at teardown
|
||||
inside `SDL_DestroyTexture`.
|
||||
- **A drawing benchmark needs a raw-SDL control that does the same pixel work.**
|
||||
Without one there is no way to separate what libakgl costs from what the
|
||||
rasterizer costs, and the answer is not the one you would guess — see
|
||||
`PERFORMANCE.md`.
|
||||
|
||||
Budgets are per-operation ceilings at roughly ten times the recorded baseline;
|
||||
they are enforced only in an optimized build at full scale. When a change moves
|
||||
a number for a good reason, re-record the baseline in `PERFORMANCE.md` in the
|
||||
same commit and say why it moved.
|
||||
|
||||
These two suites are also the memory-check vehicle. `tests/benchutil.h` detects
|
||||
valgrind from `LD_PRELOAD` and drops the scale to
|
||||
`AKGL_BENCH_VALGRIND_SCALE`, so the same binaries walk every path once instead
|
||||
of a hundred thousand times, and the timings a checked run prints are labelled
|
||||
as meaningless. **Do not add memory-check suites**: a new path worth checking
|
||||
belongs in a benchmark, where it gets both.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Recent commits use concise, imperative summaries such as `Fix a scale bug...` and often explain related changes in one sentence. Keep each commit scoped and describe user-visible behavior. Pull requests should summarize the change, identify affected modules, list the CMake/CTest commands run, and link relevant issues. Include screenshots only for rendering, map, or `charviewer` changes.
|
||||
|
||||
303
CMakeLists.txt
303
CMakeLists.txt
@@ -1,5 +1,39 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(akgl LANGUAGES C)
|
||||
# The single source of truth for the library version. It drives the generated
|
||||
# include/akgl/version.h, the shared library's VERSION and SOVERSION, and the
|
||||
# Version field in akgl.pc. Bump it here and nowhere else.
|
||||
project(akgl VERSION 0.4.0 LANGUAGES C)
|
||||
|
||||
# Memory checking reuses the suites that already exist -- `ctest -T memcheck`
|
||||
# runs every registered test under valgrind -- rather than adding programs of its
|
||||
# own. The perf suites carry most of the weight there: they are the only things
|
||||
# in the tree that load assets, draw a scene, and run a frame loop in one
|
||||
# process, and tests/benchutil.h drops their iteration counts by three orders of
|
||||
# magnitude when it finds itself under valgrind, which turns a benchmark into
|
||||
# exactly the broad, once-through path coverage a leak check wants.
|
||||
#
|
||||
# These have to be set before include(CTest): that is what writes them into
|
||||
# DartConfiguration.tcl, and a memcheck run reads them from there.
|
||||
find_program(MEMORYCHECK_COMMAND valgrind)
|
||||
# Set with FORCE, but only when empty. include(CTest) declares both of these as
|
||||
# empty cache entries, so a build tree configured before this block existed has
|
||||
# them already and a plain set(... CACHE ...) would be ignored -- the values
|
||||
# would silently never reach DartConfiguration.tcl. Guarding on emptiness still
|
||||
# leaves a deliberate -DMEMORYCHECK_SUPPRESSIONS_FILE=... alone.
|
||||
if(NOT MEMORYCHECK_SUPPRESSIONS_FILE)
|
||||
set(MEMORYCHECK_SUPPRESSIONS_FILE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/scripts/valgrind.supp"
|
||||
CACHE FILEPATH "Suppressions for third-party findings the memcheck run cannot fix" FORCE)
|
||||
endif()
|
||||
# Definite losses only. "Still reachable" is every global SDL and FreeType keeps
|
||||
# for the process lifetime and says nothing about libakgl; "possibly lost" is
|
||||
# dominated by interior pointers into pools and thread stacks. Neither is worth
|
||||
# the false positives.
|
||||
if(NOT MEMORYCHECK_COMMAND_OPTIONS)
|
||||
set(MEMORYCHECK_COMMAND_OPTIONS
|
||||
"--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite --track-origins=yes --num-callers=25"
|
||||
CACHE STRING "Options passed to the memory checker" FORCE)
|
||||
endif()
|
||||
|
||||
include(CTest)
|
||||
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
|
||||
@@ -11,67 +45,113 @@ if(AKGL_COVERAGE)
|
||||
find_program(GCOVR_EXECUTABLE gcovr REQUIRED)
|
||||
endif()
|
||||
|
||||
# Vendored projects own their test suites. Suppress their CTest registration so
|
||||
# the suite that runs contains only the targets built here. The override is
|
||||
# lifted again below, before this project registers its own tests.
|
||||
#
|
||||
# **Only when this project is top-level, and that guard is load-bearing.** CMake
|
||||
# exposes an overridden command as `_name` and chains exactly one level deep: a
|
||||
# second override rebinds `_add_test` to the *first* override and the builtin
|
||||
# becomes unreachable to everybody, forever. So two projects in one tree cannot
|
||||
# both shadow add_test() -- whoever goes second breaks, and an embedding
|
||||
# consumer that shadows it first sees its own registrations recurse until CMake
|
||||
# stops at depth 1000.
|
||||
#
|
||||
# An embedded libakgl therefore leaves the override alone and lets its consumer
|
||||
# suppress what it does not want; the consumer has to have that machinery
|
||||
# anyway, for libakerror and libakstdlib. libakstdlib guards its own shadow the
|
||||
# same way and for the same reason.
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
|
||||
# Vendored projects own their test suites. Suppress their CTest registration
|
||||
# while embedded so the top-level suite contains only targets built here.
|
||||
set(AKGL_SUPPRESS_DEPENDENCY_TESTS TRUE)
|
||||
function(add_test)
|
||||
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
||||
_add_test(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
function(set_tests_properties)
|
||||
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
||||
_set_tests_properties(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
set(AKGL_SUPPRESS_DEPENDENCY_TESTS TRUE)
|
||||
function(add_test)
|
||||
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
||||
_add_test(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
function(set_tests_properties)
|
||||
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
||||
_set_tests_properties(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
endif()
|
||||
|
||||
set(JANSSON_WITHOUT_TESTS ON CACHE BOOL "Do not build vendored Jansson tests" FORCE)
|
||||
set(JANSSON_EXAMPLES OFF CACHE BOOL "Do not build vendored Jansson examples" FORCE)
|
||||
set(JANSSON_BUILD_DOCS OFF CACHE BOOL "Do not build vendored Jansson docs" FORCE)
|
||||
|
||||
add_subdirectory(deps/jansson EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/SDL EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL)
|
||||
# Add one vendored dependency, if nobody has already declared it and the
|
||||
# submodule is actually checked out.
|
||||
#
|
||||
# A macro rather than a function on purpose: add_subdirectory() inside a
|
||||
# function runs with that function's variable scope, so every cache-ish variable
|
||||
# these projects set for their own subdirectories would be discarded on return.
|
||||
macro(akgl_add_vendored_dependency target dir)
|
||||
if(NOT TARGET ${target})
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/CMakeLists.txt")
|
||||
add_subdirectory(${dir} EXCLUDE_FROM_ALL)
|
||||
set(AKGL_VENDORED_DEPENDENCIES TRUE)
|
||||
endif()
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Reserve error-name table entries for libakgl-specific status codes.
|
||||
target_compile_definitions(akerror PUBLIC
|
||||
AKERR_MAX_ERR_VALUE=256
|
||||
)
|
||||
# Added on both paths, not just when this is the top-level project. Embedded
|
||||
# with add_subdirectory(), the submodules under deps/ are sitting right there --
|
||||
# the recursive clone the consumer just did put them there -- and refusing to
|
||||
# configure until SDL3 is installed system-wide is a failure with its own answer
|
||||
# three directories away. if(NOT TARGET ...) means a consumer that has already
|
||||
# declared one of these wins; the EXISTS check means a checkout without
|
||||
# submodules falls through to find_package below.
|
||||
akgl_add_vendored_dependency(jansson::jansson deps/jansson)
|
||||
akgl_add_vendored_dependency(akerror::akerror deps/libakerror)
|
||||
akgl_add_vendored_dependency(akstdlib::akstdlib deps/libakstdlib)
|
||||
akgl_add_vendored_dependency(SDL3::SDL3 deps/SDL)
|
||||
akgl_add_vendored_dependency(SDL3_image::SDL3_image deps/SDL_image)
|
||||
akgl_add_vendored_dependency(SDL3_mixer::SDL3_mixer deps/SDL_mixer)
|
||||
akgl_add_vendored_dependency(SDL3_ttf::SDL3_ttf deps/SDL_ttf)
|
||||
|
||||
# libakerror 1.0.0 sizes its own status-name registry; consumers no longer do.
|
||||
# libakgl claims its status codes at runtime in akgl_heap_init() instead.
|
||||
|
||||
set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE)
|
||||
|
||||
else()
|
||||
|
||||
# Anything the vendored block did not supply has to come from the system.
|
||||
if(NOT (TARGET SDL3::SDL3 AND TARGET SDL3_image::SDL3_image AND
|
||||
TARGET SDL3_mixer::SDL3_mixer AND TARGET SDL3_ttf::SDL3_ttf AND
|
||||
TARGET akerror::akerror AND TARGET akstdlib::akstdlib AND
|
||||
TARGET jansson::jansson))
|
||||
# Only needed to locate installed copies; a fully vendored build does not
|
||||
# require pkg-config to be present at all.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
endif()
|
||||
|
||||
if(NOT TARGET SDL3::SDL3)
|
||||
find_package(SDL3 REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET SDL3_image::SDL3_image)
|
||||
find_package(SDL3_image REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET SDL3_mixer::SDL3_mixer)
|
||||
find_package(SDL3_mixer REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET SDL3_ttf::SDL3_ttf)
|
||||
find_package(SDL3_ttf REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET akerror::akerror)
|
||||
find_package(akerror REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET akstdlib::akstdlib)
|
||||
find_package(akstdlib REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET jansson::jansson)
|
||||
find_package(jansson)
|
||||
endif()
|
||||
|
||||
if(NOT TARGET SDL3::SDL3)
|
||||
find_package(SDL3 REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET SDL3_image::SDL3_image)
|
||||
find_package(SDL3_image REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET SDL3_mixer::SDL3_mixer)
|
||||
find_package(SDL3_mixer REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET SDL3_ttf::SDL3_ttf)
|
||||
find_package(SDL3_ttf REQUIRED)
|
||||
endif()
|
||||
# No version here: libakerror ships no akerrorConfigVersion.cmake, so asking
|
||||
# for one makes find_package reject every install. The floor is enforced by
|
||||
# the #error in include/akgl/error.h instead, which feature-tests
|
||||
# AKERR_FIRST_CONSUMER_STATUS.
|
||||
if(NOT TARGET akerror::akerror)
|
||||
find_package(akerror REQUIRED)
|
||||
endif()
|
||||
# 0.2 rather than bare: libakstdlib 0.2.0 ships an akstdlibConfigVersion.cmake
|
||||
# with SameMinorVersion compatibility, mirroring its soname, so this accepts
|
||||
# any 0.2.x and refuses 0.3 and 1.0. Unversioned, this path would silently
|
||||
# accept an ABI-incompatible libakstdlib.
|
||||
if(NOT TARGET akstdlib::akstdlib)
|
||||
find_package(akstdlib 0.2 REQUIRED)
|
||||
endif()
|
||||
if(NOT TARGET jansson::jansson)
|
||||
find_package(jansson)
|
||||
endif()
|
||||
|
||||
set(GAMECONTROLLERDB_H "include/akgl/SDL_GameControllerDB.h")
|
||||
@@ -80,6 +160,8 @@ set(exec_prefix "\${prefix}")
|
||||
set(libdir "\${exec_prefix}/lib")
|
||||
set(includedir "\${prefix}/include")
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/include/akgl/version.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h @ONLY)
|
||||
|
||||
# Tests use both relative paths and SDL_GetBasePath(), so stage fixtures beside
|
||||
# test executables in every out-of-tree build.
|
||||
@@ -98,10 +180,12 @@ add_library(akgl SHARED
|
||||
deps/semver/semver.c
|
||||
src/actor.c
|
||||
src/actor_state_string_names.c
|
||||
src/audio.c
|
||||
src/text.c
|
||||
src/assets.c
|
||||
src/character.c
|
||||
src/draw.c
|
||||
src/error.c
|
||||
src/game.c
|
||||
src/controller.c
|
||||
src/heap.c
|
||||
@@ -113,48 +197,107 @@ add_library(akgl SHARED
|
||||
src/staticstring.c
|
||||
src/tilemap.c
|
||||
src/util.c
|
||||
src/version.c
|
||||
${GAMECONTROLLERDB_H}
|
||||
)
|
||||
|
||||
# While the major version is 0 the ABI is not stable across minor releases, so
|
||||
# the soname carries major.minor -- libakgl.so.0.1. A plain SOVERSION 0 would
|
||||
# claim 0.1.0 and 0.2.0 are interchangeable, which is exactly the silent
|
||||
# mispairing the soname is here to prevent. At 1.0.0 this becomes the major
|
||||
# alone, matching libakerror.
|
||||
if(PROJECT_VERSION_MAJOR EQUAL 0)
|
||||
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
||||
else()
|
||||
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}")
|
||||
endif()
|
||||
|
||||
set_target_properties(akgl PROPERTIES
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${AKGL_SOVERSION}
|
||||
)
|
||||
|
||||
add_library(akgl::akgl ALIAS akgl)
|
||||
|
||||
add_executable(charviewer util/charviewer.c)
|
||||
add_executable(test_semver_unit deps/semver/semver_unit.c)
|
||||
add_executable(akgl_test_semver_unit deps/semver/semver_unit.c)
|
||||
|
||||
# Every suite here is a standalone C program named tests/<name>.c, built as
|
||||
# test_<name> and registered with CTest under <name>.
|
||||
# akgl_test_<name> and registered with CTest under <name>.
|
||||
set(AKGL_TEST_SUITES
|
||||
actor
|
||||
audio
|
||||
bitmasks
|
||||
character
|
||||
controller
|
||||
draw
|
||||
error
|
||||
game
|
||||
headers
|
||||
heap
|
||||
json_helpers
|
||||
physics
|
||||
registry
|
||||
renderer
|
||||
sprite
|
||||
staticstring
|
||||
text
|
||||
tilemap
|
||||
util
|
||||
version
|
||||
)
|
||||
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
add_executable(test_${suite} tests/${suite}.c)
|
||||
add_test(NAME ${suite} COMMAND test_${suite})
|
||||
# The performance suites are built and registered exactly like the unit suites,
|
||||
# but they are benchmarks: they drive hot paths for millions of iterations, print
|
||||
# a table of nanoseconds per operation, and fail only when a measurement exceeds
|
||||
# a budget set at roughly ten times the recorded baseline. They carry the `perf`
|
||||
# label, so `ctest -L perf` runs only them and `ctest -LE perf` leaves them out
|
||||
# of an ordinary run, and they get a much longer timeout for obvious reasons.
|
||||
#
|
||||
# Set AKGL_BENCH_SCALE in the environment to change how long they run --
|
||||
# `AKGL_BENCH_SCALE=0.1 ctest -L perf` for a tenth of the iterations. Below 1.0
|
||||
# the budgets are measured and reported but not enforced, because a short run is
|
||||
# a noisy one.
|
||||
set(AKGL_PERF_SUITES
|
||||
perf
|
||||
perf_render
|
||||
)
|
||||
|
||||
# The executables carry an akgl_ prefix but the CTest names do not: a vendored
|
||||
# dependency is free to ship its own tests/version.c, and libakstdlib now does.
|
||||
# Its target is created by add_subdirectory even though EXCLUDE_FROM_ALL keeps
|
||||
# it from being built, so an unprefixed test_version here is a configure error.
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
||||
add_executable(akgl_test_${suite} tests/${suite}.c)
|
||||
add_test(NAME ${suite} COMMAND akgl_test_${suite})
|
||||
endforeach()
|
||||
|
||||
add_test(NAME semver_unit COMMAND test_semver_unit)
|
||||
add_test(NAME semver_unit COMMAND akgl_test_semver_unit)
|
||||
|
||||
# TIMEOUT is generous because CTest applies the same property to `ctest -T
|
||||
# memcheck`, where every suite runs under valgrind at something like twenty
|
||||
# times its normal cost. The unit suites finish in well under a second each
|
||||
# without it; the ceiling is there for the checked run, not the ordinary one.
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} semver_unit
|
||||
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 30
|
||||
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 300
|
||||
)
|
||||
|
||||
set_tests_properties(
|
||||
${AKGL_PERF_SUITES}
|
||||
PROPERTIES
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
|
||||
TIMEOUT 900
|
||||
LABELS perf
|
||||
)
|
||||
|
||||
# Specify include directories for the library's headers (if applicable)
|
||||
target_include_directories(akgl PUBLIC
|
||||
include/
|
||||
deps/semver/
|
||||
# akgl/version.h is generated by configure_file, so it lives in the build tree
|
||||
# rather than beside the headers it is included from.
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/
|
||||
)
|
||||
|
||||
if(AKGL_COVERAGE)
|
||||
@@ -173,7 +316,9 @@ if(AKGL_COVERAGE)
|
||||
)
|
||||
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
|
||||
# Any suite missing from this list runs outside the fixture and has its
|
||||
# counters discarded by coverage_reset.
|
||||
# counters discarded by coverage_reset. The perf suites are deliberately
|
||||
# outside it: an instrumented -O0 build measures gcov, not libakgl, and the
|
||||
# lines they cover are covered by the unit suites anyway.
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} semver_unit
|
||||
PROPERTIES FIXTURES_REQUIRED akgl_coverage
|
||||
@@ -208,9 +353,9 @@ target_link_libraries(akgl
|
||||
jansson::jansson
|
||||
)
|
||||
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
target_link_libraries(test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_include_directories(test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
||||
target_link_libraries(akgl_test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_include_directories(akgl_test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||
endforeach()
|
||||
|
||||
target_link_libraries(charviewer PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
@@ -219,8 +364,9 @@ target_link_libraries(charviewer PRIVATE akstdlib::akstdlib akerror::akerror akg
|
||||
# project subdirectories that are not on the loader's default search path, so a
|
||||
# freshly built test aborts before main() with "cannot open shared object file".
|
||||
# Bake those directories into the build-tree RPATH. Installed builds resolve the
|
||||
# same libraries through find_package and need no help.
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
# same libraries through find_package and need no help, so this keys on whether
|
||||
# anything was actually vendored rather than on being the top-level project.
|
||||
if(AKGL_VENDORED_DEPENDENCIES)
|
||||
set(AKGL_VENDORED_RPATH
|
||||
"$<TARGET_FILE_DIR:SDL3::SDL3>"
|
||||
"$<TARGET_FILE_DIR:SDL3_image::SDL3_image>"
|
||||
@@ -229,8 +375,8 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
"$<TARGET_FILE_DIR:akerror::akerror>"
|
||||
"$<TARGET_FILE_DIR:akstdlib::akstdlib>"
|
||||
)
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
set_target_properties(test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
||||
set_target_properties(akgl_test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
endforeach()
|
||||
set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
|
||||
@@ -253,13 +399,13 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
list(APPEND AKGL_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
||||
endforeach()
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES}
|
||||
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
||||
PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}"
|
||||
)
|
||||
else()
|
||||
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES}
|
||||
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
||||
PROPERTIES ENVIRONMENT "LD_LIBRARY_PATH=${AKGL_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
|
||||
)
|
||||
endif()
|
||||
@@ -285,14 +431,35 @@ if(Python3_FOUND)
|
||||
)
|
||||
endif()
|
||||
|
||||
set(main_lib_dest "lib/akgl-${MY_LIBRARY_VERSION}")
|
||||
# Memory checking runs the whole registered suite under valgrind. The wrapper
|
||||
# script exists because `ctest -T memcheck` records defects and still exits 0,
|
||||
# which cannot gate anything; it also forces the headless drivers, so the vendor
|
||||
# GPU stack is never loaded and never has to be suppressed.
|
||||
if(MEMORYCHECK_COMMAND)
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(AKGL_MEMCHECK_TARGET memcheck)
|
||||
else()
|
||||
set(AKGL_MEMCHECK_TARGET akgl_memcheck)
|
||||
endif()
|
||||
add_custom_target(${AKGL_MEMCHECK_TARGET}
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
AKGL_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/memcheck.sh
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
USES_TERMINAL
|
||||
COMMENT "Running every test suite under valgrind (slow; the perf suites scale themselves down)"
|
||||
)
|
||||
endif()
|
||||
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h DESTINATION "include/akgl/")
|
||||
install(TARGETS akgl DESTINATION "lib/")
|
||||
install(FILES "deps/semver/semver.h" DESTINATION "include/")
|
||||
install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/types.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/text.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/audio.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/character.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/error.h" DESTINATION "include/akgl/")
|
||||
install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/")
|
||||
|
||||
344
PERFORMANCE.md
Normal file
344
PERFORMANCE.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# libakgl performance baseline
|
||||
|
||||
This is where the library actually spends its time, measured rather than guessed.
|
||||
The numbers below are the first recorded baseline: libakgl 0.3.0, at commit
|
||||
`f35443e` plus the perf suites themselves. Everything here is reproducible with
|
||||
two commands, and the suites that produced it are checked in as
|
||||
`tests/perf.c` and `tests/perf_render.c`.
|
||||
|
||||
Read it in this order if you only want the short version: **the frame budget** is
|
||||
the part that matters and **what the numbers say** is the argument. The six
|
||||
defects the suites turned up on the way — two of them process-killing — are in
|
||||
`TODO.md` under **Performance**, along with the targets these numbers are
|
||||
measured against.
|
||||
|
||||
## Reproducing it
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build build --parallel
|
||||
ctest --test-dir build -L perf --output-on-failure
|
||||
```
|
||||
|
||||
Both suites print a table and exit non-zero if any measurement blew its budget.
|
||||
They are ordinary CTest tests, so `ctest --test-dir build` runs them along with
|
||||
everything else; `ctest --test-dir build -LE perf` leaves them out when you only
|
||||
want the unit suites. `AKGL_BENCH_SCALE` scales every iteration count —
|
||||
`AKGL_BENCH_SCALE=0.1` for a quick look, `10` for a long one. Below 1.0 the
|
||||
budgets are reported but not enforced, because a short run is a noisy one.
|
||||
|
||||
The whole thing takes about 30 seconds: 4 s for `perf`, 25 s for `perf_render`.
|
||||
|
||||
## The machine
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| CPU | AMD Ryzen 5 7535HS, 6 cores / 12 threads, 4.6 GHz max |
|
||||
| Memory | 62 GiB |
|
||||
| OS | Linux 6.8.0-136-generic |
|
||||
| Compiler | gcc 13.3.0, `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (`-O2 -g`) |
|
||||
| SDL | vendored SDL 3.4.8, `dummy` video driver, `software` renderer |
|
||||
| Target | 640x480, offscreen |
|
||||
|
||||
One machine, one build type, one afternoon. Treat the absolute numbers as this
|
||||
laptop's and the *ratios* as the library's.
|
||||
|
||||
## How it is measured
|
||||
|
||||
- **Best of five.** Each benchmark runs five times and the harness keeps the
|
||||
fastest. The mean is the wrong statistic on a shared machine: every source of
|
||||
noise makes a run slower and none makes it faster.
|
||||
- **No error checking inside the clock.** `PASS` and `CATCH` call
|
||||
`akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` — more work than
|
||||
several of the calls being measured. The timed loop stashes the context and
|
||||
checks it after the clock stops.
|
||||
- **The renderer is flushed inside the measurement.** SDL batches: a `draw_*`
|
||||
call queues a command and returns. The first version of this suite measured
|
||||
*queueing* and reported a tilemap frame at 65 µs; the deferred work then took
|
||||
114 seconds to come out at teardown, inside `SDL_DestroyTexture`. Every
|
||||
drawing benchmark now flushes before it stops the clock, and the numbers below
|
||||
are 250x larger and true.
|
||||
- **SDL's log output goes to a sink that discards it.** The library logs on
|
||||
paths this suite calls hundreds of thousands of times; timing a write to a
|
||||
terminal measures the terminal. What the *formatting* costs is measured
|
||||
deliberately, by the pair of actor-spawn benchmarks.
|
||||
- **Budgets are set at roughly 10x the measured baseline.** Loose enough that a
|
||||
busy machine does not turn CI red, tight enough that a linear scan becoming
|
||||
quadratic cannot hide. They are enforced only in an optimized build at full
|
||||
scale — a coverage build measures gcov, not libakgl.
|
||||
|
||||
**The renderer caveat, stated once and loudly.** These draw benchmarks run
|
||||
against SDL's *software* renderer. No shipped game does that. What a software
|
||||
renderer buys is that all the work stays in this process where it can be timed,
|
||||
and that the per-blit cost is at least honest about how much pixel traffic was
|
||||
asked for. Read every drawing number as *a count of work libakgl asked for*, not
|
||||
as a frame rate anyone would ship. That is exactly why the raw-SDL control rows
|
||||
exist: they do the same pixel work with none of the library in the path, so the
|
||||
difference between the two is libakgl's share, and that part *does* carry over
|
||||
to a GPU backend.
|
||||
|
||||
## Static footprint
|
||||
|
||||
libakgl does not call `malloc`. Every one of these arrays exists from process
|
||||
start whether the game uses one slot or all of them.
|
||||
|
||||
| Pool | Slots | Bytes each | Total |
|
||||
|---|---:|---:|---:|
|
||||
| `HEAP_ACTOR` | 64 | 400 | 25,600 |
|
||||
| `HEAP_SPRITE` | 1024 | 176 | 180,224 |
|
||||
| `HEAP_SPRITESHEET` | 1024 | 536 | 548,864 |
|
||||
| `HEAP_CHARACTER` | 256 | 184 | 47,104 |
|
||||
| `HEAP_STRING` | 256 | 4,100 | 1,049,600 |
|
||||
| **pools, total** | | | **1,851,392** |
|
||||
| `akgl_Tilemap` (one, as `_akgl_gamemap`) | 1 | 26,388,008 | 26,388,008 |
|
||||
| — of which layers | 16 | 1,120,296 | 17,924,736 |
|
||||
| — of which tilesets | 16 | 528,944 | 8,463,104 |
|
||||
|
||||
**28 MB of BSS before `main` runs, and 94% of it is one tilemap.** A layer is
|
||||
512x512 `int` cells whether the map is 512x512 or 2x2, and a tileset carries a
|
||||
65,536-entry offset table whether the image holds 65,536 tiles or 1,728. This is
|
||||
not a hypothetical cost: zeroing that struct is 1.37 ms, and `akgl_tilemap_load`
|
||||
pays it before it has read a byte of the map file.
|
||||
|
||||
## Results: everything that does not draw
|
||||
|
||||
`tests/perf.c`, full scale. `ns/op` is the best of five runs; `ops/sec` is its
|
||||
reciprocal.
|
||||
|
||||
| Benchmark | Unit | ns/op | ops/sec |
|
||||
|---|---|---:|---:|
|
||||
| `heap_next_actor`, empty pool | call | 4.0 | 248,392,158 |
|
||||
| `heap_next_actor`, one slot left | call | 36.9 | 27,077,696 |
|
||||
| heap string claim + release cycle | cycle | 49.8 | 20,084,967 |
|
||||
| `heap_next_string`, empty pool | call | 3.9 | 255,059,748 |
|
||||
| `heap_next_string`, one slot left | call | 250.9 | 3,986,007 |
|
||||
| `heap_release_string`, 4 KiB wipe | call | 47.2 | 21,191,918 |
|
||||
| `heap_init`, all five pools | call | 45,141 | 22,153 |
|
||||
| actor spawn + release, library logging on | actor | 202.3 | 4,942,862 |
|
||||
| actor spawn + release, logging suppressed | actor | 162.5 | 6,153,324 |
|
||||
| `akgl_actor_set_character`, registry lookup | call | 39.3 | 25,438,092 |
|
||||
| `akgl_set_property` | call | 96.9 | 10,316,516 |
|
||||
| `akgl_get_property`, 4 KiB copy | call | 85.3 | 11,722,134 |
|
||||
| `akgl_character_sprite_get`, state to sprite | call | 37.4 | 26,734,605 |
|
||||
| `akgl_actor_update`, animation advancing | actor | 68.4 | 14,609,892 |
|
||||
| `akgl_actor_update`, no sprite for state | actor | 616.5 | 1,621,937 |
|
||||
| `akgl_physics_simulate`, 64 live actors | frame | 1,216.8 | 821,841 |
|
||||
| `akgl_physics_simulate`, empty pool | frame | 63.9 | 15,650,101 |
|
||||
| logic frame: 64 updates + simulate | frame | 5,763.1 | 173,517 |
|
||||
| `akgl_rectangle_points` | call | 4.0 | 248,412,026 |
|
||||
| `akgl_collide_rectangles`, overlapping | call | 24.9 | 40,181,108 |
|
||||
| `akgl_collide_rectangles`, disjoint | call | 57.9 | 17,261,906 |
|
||||
| all-pairs collision sweep, 64 actors (2016 pairs) | sweep | 115,023.6 | 8,694 |
|
||||
| `akgl_string_initialize` | call | 32.1 | 31,197,432 |
|
||||
| `akgl_string_copy`, full length | call | 32.2 | 31,075,004 |
|
||||
| `json_load_file`, small document | load | 11,338.8 | 88,193 |
|
||||
| `akgl_get_json_string_value` | call | 40.7 | 24,552,958 |
|
||||
| `akgl_get_json_integer_value` | call | 13.8 | 72,704,562 |
|
||||
| `akgl_path_relative`, `realpath` on an existing file | call | 3,481.5 | 287,232 |
|
||||
|
||||
## Results: everything that draws
|
||||
|
||||
`tests/perf_render.c`, full scale, 640x480 software renderer. The two **control**
|
||||
rows are raw `SDL_RenderTexture` loops with no libakgl in the path.
|
||||
|
||||
| Benchmark | Unit | ns/op | ops/sec |
|
||||
|---|---|---:|---:|
|
||||
| `frame_start` + `frame_end` (clear + present) | frame | 23,161.7 | 43,175 |
|
||||
| `akgl_draw_point` | call | 132.5 | 7,547,141 |
|
||||
| `akgl_draw_line`, screen diagonal | call | 605.9 | 1,650,375 |
|
||||
| `akgl_draw_rect`, 200x150 outline | call | 520.8 | 1,920,055 |
|
||||
| `akgl_draw_filled_rect`, 200x150 | call | 43,695.1 | 22,886 |
|
||||
| `akgl_draw_circle`, radius 64 | call | 2,516.2 | 397,423 |
|
||||
| `akgl_draw_copy_region`, 64x64 readback | call | 954.7 | 1,047,471 |
|
||||
| `akgl_draw_paste_region`, 64x64 upload | call | 5,352.1 | 186,841 |
|
||||
| `akgl_draw_flood_fill`, full 640x480 target | call | 1,967,295.6 | 508 |
|
||||
| `akgl_text_measure`, 15 characters | call | 37.3 | 26,804,657 |
|
||||
| `akgl_text_rendertextat`, 15 characters | call | 12,601.7 | 79,354 |
|
||||
| `akgl_sprite_load_json`, sheet already loaded | load | 17,011.3 | 58,784 |
|
||||
| `akgl_character_load_json`, two mappings | load | 14,506.8 | 68,933 |
|
||||
| `akgl_tilemap_load` + release, fixture map | load | 11,881,190.8 | 84 |
|
||||
| zeroing one `akgl_Tilemap` | call | 1,372,396.7 | 729 |
|
||||
| `akgl_tilemap_compute_tileset_offsets`, 1728 tiles | call | 2,240.8 | 446,264 |
|
||||
| `akgl_tilemap_draw`, 40x30 tiles, 1 tileset | frame | 16,260,566.6 | 61 |
|
||||
| `akgl_tilemap_draw`, 40x30 tiles, 8 tilesets | frame | 16,395,182.4 | 61 |
|
||||
| **control**: raw SDL blits, one source tile | frame | 471,676.6 | 2,120 |
|
||||
| **control**: raw SDL blits, map order | frame | 16,229,501.0 | 62 |
|
||||
| `akgl_actor_render`, on camera | actor | 2,992.0 | 334,220 |
|
||||
| `draw_world`, 1200 tiles + 64 actors | frame | 16,484,493.6 | 61 |
|
||||
| `akgl_game_update`, full frame | frame | 16,576,902.2 | 60 |
|
||||
|
||||
## The frame budget
|
||||
|
||||
At 60 fps a frame is 16.67 ms. Here is where it goes for a 640x480 game with a
|
||||
full screen of 16-pixel tiles and 64 actors:
|
||||
|
||||
| Part of the frame | Cost | Share of 16.67 ms |
|
||||
|---|---:|---:|
|
||||
| Logic: 64 actor updates + one physics sweep | 0.006 ms | 0.03% |
|
||||
| All-pairs collision over 64 actors, if you do it | 0.115 ms | 0.7% |
|
||||
| Clear + present | 0.023 ms | 0.1% |
|
||||
| 64 actor renders (48x48 blits) | 0.191 ms | 1.1% |
|
||||
| 1200 tile blits | 16.26 ms | 97.6% |
|
||||
| Six lines of HUD text | 0.076 ms | 0.5% |
|
||||
|
||||
**Everything libakgl decides is free. The pixels are the whole frame.** Every
|
||||
piece of bookkeeping this library does — pool scans, registry lookups, state-to-
|
||||
sprite mapping, physics, the error-context machinery — adds up to well under 1%
|
||||
of a frame that is 97% software rasterization. On a GPU backend those blits get
|
||||
cheap and libakgl's own share rises, which is exactly why the per-operation
|
||||
numbers above matter more than the frame totals.
|
||||
|
||||
## What the numbers say
|
||||
|
||||
### The tilemap draw is SDL, not libakgl — and I can prove it
|
||||
|
||||
`akgl_tilemap_draw` takes 16.26 ms for a 1200-tile screen. A raw
|
||||
`SDL_RenderTexture` loop issuing the *same 1200 blits from the same scattered
|
||||
source tiles* takes 16.23 ms. The library's own per-tile work — the bounds
|
||||
arithmetic, the tileset scan, the offset-table lookup, the backend indirection,
|
||||
the error macros — is **0.03 ms per frame, under 0.2%**.
|
||||
|
||||
That took three attempts to measure honestly. A control that walked the sheet
|
||||
sequentially said libakgl cost 67%; a control that blitted one source tile over
|
||||
and over said it cost 3400%. Both were wrong, and both were wrong the same way:
|
||||
they changed the *memory access pattern of the source texture* rather than
|
||||
isolating the library. A 16x16 tile read from a random place in a 768x576 sheet
|
||||
costs about 13 µs on this software rasterizer; the same tile read from cache
|
||||
costs 0.4 µs. That factor of thirty is the whole story, and none of it is
|
||||
libakgl's.
|
||||
|
||||
Related: the `FIXME` in `src/tilemap.c` worrying that the per-tile tileset scan
|
||||
"is probably not very efficient" is, at eight tilesets, worth **0.8% of the
|
||||
frame** (16.40 ms vs 16.26 ms). It is a real O(tiles x tilesets) loop and it
|
||||
should still be fixed, but it is not where the time is, and nobody should
|
||||
reorganise the loader for it.
|
||||
|
||||
### The pools are linear scans, and only the string pool cares
|
||||
|
||||
Claiming an actor from an empty pool is 4.0 ns; claiming the last free slot is
|
||||
36.9 ns — nine times the cost, and still nothing.
|
||||
|
||||
The string pool is the exception, and it is instructive. Claiming from an empty
|
||||
string pool is 3.9 ns; claiming the *last free slot* is 250.9 ns, **64 times**
|
||||
slower. Same algorithm, same 256-ish entries. The difference is that each
|
||||
`akgl_String` is `PATH_MAX` + 4 bytes, so the scan touches one reference count
|
||||
every 4 KiB and takes a cache miss on every candidate. A pool of 256 strings is
|
||||
a megabyte, and walking it is walking a megabyte.
|
||||
|
||||
`akgl_heap_release_string` costs 47.2 ns because it `memset`s all 4,100 bytes
|
||||
whether the string held a path or one character. Same for `akgl_string_copy`
|
||||
(32.2 ns) and `akgl_get_property` (85.3 ns), which move `AKGL_MAX_STRING_LENGTH`
|
||||
bytes unconditionally. None of these is expensive in isolation; all of them are
|
||||
the same avoidable habit of paying for `PATH_MAX` when you used eleven bytes.
|
||||
|
||||
### Errors cost about ten times what success costs
|
||||
|
||||
`akgl_actor_update` on an actor whose character has a sprite for its state:
|
||||
68.4 ns. The same call on an actor whose character does *not*: **616.5 ns**.
|
||||
|
||||
That path is not an error in any meaningful sense — the library handles
|
||||
`AKERR_KEY` and carries on, and `akgl_actor_render` logs it and draws nothing.
|
||||
It is a normal condition on a partly authored character. But raising it means
|
||||
claiming a context out of `AKERR_ARRAY_ERROR`, formatting a message with
|
||||
`vsnprintf`, appending a stack-trace frame, walking the `PROCESS` switch, and
|
||||
releasing it again. Nine times the cost of the update it replaced.
|
||||
|
||||
The design conclusion is not "make errors cheaper". It is that a *routine*
|
||||
condition should not be reported as an error. A character that has no sprite for
|
||||
a state should answer that question with a boolean.
|
||||
|
||||
### `akgl_game_update` updates every actor sixteen times
|
||||
|
||||
`src/game.c:617` loops over `AKGL_TILEMAP_MAX_LAYERS`, and the actor sweep
|
||||
nested inside it does not filter by layer. Every live actor's `updatefunc` runs
|
||||
**16 times per frame**. At 68.4 ns per update and 64 actors that is 70 µs of
|
||||
work to do 4.4 µs of work.
|
||||
|
||||
It is invisible in the frame total here because the tilemap blits are three
|
||||
orders of magnitude larger. On a GPU backend, where the frame might be 2 ms, it
|
||||
is 3.5% of the frame doing nothing. Filed in `TODO.md`.
|
||||
|
||||
### Text has no cache at all
|
||||
|
||||
`akgl_text_rendertextat` is 12.6 µs for fifteen characters: it rasterizes the
|
||||
string, uploads it as a texture, blits it, and destroys the texture — every
|
||||
call, every frame, for a score that changes once a second. Measuring the string
|
||||
first with `akgl_text_measure` is 37.3 ns, i.e. free, which tells you the whole
|
||||
cost is the rasterize-and-upload.
|
||||
|
||||
Six HUD readouts is 76 µs a frame. That is fine at 60 fps on this machine and it
|
||||
is 4% of a 2 ms GPU frame. A one-line cache keyed on (font, string, colour)
|
||||
would take it to nothing, and it is the single clearest optimisation in the
|
||||
library.
|
||||
|
||||
### Loading is dominated by things that are not the file
|
||||
|
||||
A 2x2 fixture map with one tileset takes **11.9 ms** to load and release. Of
|
||||
that, 1.37 ms — 11.5% — is `memset`ing the 26 MB `akgl_Tilemap` before anything
|
||||
is read. Most of the rest is decoding the tileset PNG. The JSON is noise:
|
||||
parsing a small document is 11.3 µs, and the accessors are 14-41 ns each.
|
||||
|
||||
`akgl_path_relative` is 3.5 µs, because it is a `realpath(3)` syscall. A map
|
||||
naming twenty assets pays 70 µs. Also noise, but worth knowing it is a syscall
|
||||
and not a string operation.
|
||||
|
||||
### The collision helpers are fine; the missing broad phase is the problem
|
||||
|
||||
`akgl_collide_rectangles` is 24.9 ns when the rectangles overlap (it returns at
|
||||
the first corner that hits) and 57.9 ns when they do not (all eight corner tests
|
||||
run). Both are fine.
|
||||
|
||||
What the library does not provide is a broad phase, so a caller that wants
|
||||
collision writes the all-pairs loop: 2016 pairs for 64 actors, 115 µs a frame,
|
||||
0.7% of a 60 fps budget. That is affordable. It is also O(n²): raise
|
||||
`AKGL_MAX_HEAP_ACTOR` to 256 and the same loop is 32,640 pairs and 1.9 ms — over
|
||||
10% of the frame, for a game that has done nothing yet.
|
||||
|
||||
### Spawning is cheap, and a third of it is a log line
|
||||
|
||||
An actor spawn — pool claim, `memset`, registry insert, release — is 202.3 ns
|
||||
with the library's logging on and 162.5 ns with SDL's log priority raised so the
|
||||
message is never formatted. **20% of a spawn is formatting a log line nobody
|
||||
reads**, and that is with output going to a sink that throws it away; write it to
|
||||
a terminal and it is far worse. `akgl_actor_initialize` and
|
||||
`akgl_character_sprite_add` both log unconditionally at `INFO`.
|
||||
|
||||
## Defects these tests found
|
||||
|
||||
Six, and they are filed where defects live: `TODO.md`, under **Performance ->
|
||||
Defects the perf suites found**, items 28-33, each with its file, line,
|
||||
functional consequence, and what fixing it would touch. They are not repeated
|
||||
here.
|
||||
|
||||
Worth saying in this document, because it is the argument for having written the
|
||||
suites at all: stress testing breaks things unit tests do not reach. Two of the
|
||||
six are process-killing — an error-context leak that aborted on the 129th path
|
||||
resolution (fixed, with a regression test), and a pooled-string leak that turns
|
||||
into a segfault rather than an `AKGL_ERR_HEAP` around the 52nd map load. Neither
|
||||
is reachable by a suite that calls each function a handful of times, and neither
|
||||
had anything to do with speed. They came out of loops that ran the same call ten
|
||||
thousand times in one process, which is what a running game does and what
|
||||
nothing else in this tree did.
|
||||
|
||||
## What this report does not cover
|
||||
|
||||
- **One machine.** No ARM, no Raspberry Pi, no Windows, no macOS. The ratios
|
||||
should travel; the absolute numbers will not.
|
||||
- **No GPU backend.** Everything drawing-related is a software rasterizer under
|
||||
the dummy video driver. The share of a frame that belongs to libakgl is a
|
||||
floor, not an estimate.
|
||||
- **No audio.** `src/audio.c` is not benchmarked at all; the mixer under the
|
||||
dummy driver does not do the work a real device would.
|
||||
- **No controller input.** The event path is driven by SDL and has no gamepad to
|
||||
drive it under the dummy driver.
|
||||
- **No heap profiling beyond the static footprint.** Nothing here measures peak
|
||||
or steady-state heap usage. Correctness of the heap *is* covered now, by
|
||||
`cmake --build build --target memcheck`, which runs these same binaries under
|
||||
valgrind — `tests/benchutil.h` cuts the iteration counts by three orders of
|
||||
magnitude when it finds itself there, so a benchmark becomes a path-coverage
|
||||
program. That run found six more defects, including a leak of the parsed JSON
|
||||
document in every asset loader; they are in `TODO.md` under **Memory
|
||||
checking**. What is still missing is a profile: how much a running game
|
||||
actually holds at once, and how that grows over an hour.
|
||||
- **Single-threaded throughout.** The library is not thread-safe by design
|
||||
(`akgl_game_state_lock` guards one field), and nothing here tests contention.
|
||||
120
README.md
120
README.md
@@ -270,6 +270,98 @@ PASS(e, akgl_heap_release_string(width));
|
||||
```
|
||||
|
||||
|
||||
## Git hooks
|
||||
|
||||
The repository ships a `pre-commit` hook that keeps committed C sources in the
|
||||
project's canonical format (Emacs `cc-mode` "stroustrup"; see `AGENTS.md` for the
|
||||
full style guide). The hook lives in `scripts/hooks/` and is version controlled,
|
||||
but **Git configuration is not cloned**, so every clone has to be pointed at it
|
||||
once:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath scripts/hooks
|
||||
```
|
||||
|
||||
Confirm it took effect:
|
||||
|
||||
```sh
|
||||
git rev-parse --git-path hooks # should print: scripts/hooks
|
||||
```
|
||||
|
||||
That is the whole installation. The hook is already committed with its
|
||||
executable bit set, so nothing needs `chmod`.
|
||||
|
||||
### What the hook does
|
||||
|
||||
On each commit it looks at the **staged** content of any added, copied, modified,
|
||||
or renamed `.c`/`.h` file under `src/`, `include/`, `tests/`, or `util/`, and
|
||||
reindents it if it does not already match the canonical style. Checking the
|
||||
staged content rather than the working tree means what lands in the commit is
|
||||
what was actually verified.
|
||||
|
||||
When a file needs reindenting, the hook fixes it in the working tree and
|
||||
re-stages it — but only when the index and working tree agree for that file. If
|
||||
they differ, you have staged part of a file with `git add -p`, and re-staging
|
||||
would sweep your unstaged work into the commit. Rather than do that silently the
|
||||
hook stops and prints the commands to run yourself:
|
||||
|
||||
```sh
|
||||
scripts/reindent.sh path/to/file.c
|
||||
git add path/to/file.c
|
||||
```
|
||||
|
||||
`include/akgl/SDL_GameControllerDB.h` is generated and always skipped.
|
||||
|
||||
### Requirements
|
||||
|
||||
The hook drives Emacs in batch mode, because `cc-mode`'s indentation engine is
|
||||
the definition of the style and `clang-format` cannot reproduce it exactly. If
|
||||
`emacs` is not on `PATH` the hook prints a warning and allows the commit — a
|
||||
hook that refuses to run without an optional tool only teaches people to reach
|
||||
for `--no-verify`. Install Emacs to get the check; without it, formatting is on
|
||||
you.
|
||||
|
||||
### Bypassing and uninstalling
|
||||
|
||||
Skip the hook for a single commit:
|
||||
|
||||
```sh
|
||||
git commit --no-verify
|
||||
```
|
||||
|
||||
Remove it entirely:
|
||||
|
||||
```sh
|
||||
git config --unset core.hooksPath
|
||||
```
|
||||
|
||||
### If you already have local hooks
|
||||
|
||||
`core.hooksPath` **replaces** the hooks directory outright — once it is set, Git
|
||||
stops reading `.git/hooks/` altogether, so any hooks you keep there will silently
|
||||
stop firing. If that matters, leave `core.hooksPath` unset and symlink just this
|
||||
one hook instead:
|
||||
|
||||
```sh
|
||||
ln -s ../../scripts/hooks/pre-commit .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
### Formatting without the hook
|
||||
|
||||
The hook is a convenience, not the source of truth. The same check is available
|
||||
directly, and is what you would run in CI:
|
||||
|
||||
```sh
|
||||
scripts/reindent.sh --check # list non-conforming files; exit 1 if any
|
||||
scripts/reindent.sh # reindent every tracked C source in place
|
||||
scripts/reindent.sh src/game.c # reindent specific files
|
||||
```
|
||||
|
||||
`--check` exits `0` when everything conforms, `1` when a file needs reindenting,
|
||||
and `2` if Emacs is missing or the script cannot run — so a CI job that treats
|
||||
any non-zero status as failure will not mistake a broken toolchain for a clean
|
||||
tree.
|
||||
|
||||
## Mutation testing
|
||||
|
||||
The mutation harness makes one deliberate source-code change at a time in a scratch copy, then rebuilds and runs the passing CTest suite to measure whether tests detect the change. The known-failing `character` test is excluded by default.
|
||||
@@ -282,3 +374,31 @@ scripts/mutation_test.py --threshold 40 --junit mutation-junit.xml
|
||||
```
|
||||
|
||||
The default run covers all libakgl-owned files under `src/`. Use repeated `--target` options to narrow the scope. A surviving mutant identifies behavior that the current tests do not verify; the script prints its file, line, operator, and exact edit. The real working tree is never mutated.
|
||||
|
||||
## Performance testing
|
||||
|
||||
`tests/perf.c` and `tests/perf_render.c` are benchmarks rather than unit tests: they drive the hot paths — pool acquire and release, the registry, the physics sweep, the per-actor update and render, the tilemap draw, text, and asset loading — and print what each one costs per operation.
|
||||
|
||||
```sh
|
||||
ctest --test-dir build -L perf --output-on-failure # benchmarks only, about 30 seconds
|
||||
ctest --test-dir build -LE perf # everything except the benchmarks
|
||||
AKGL_BENCH_SCALE=0.1 ctest --test-dir build -L perf # a tenth of the iterations
|
||||
```
|
||||
|
||||
Each measurement is the best of five runs and is held to a budget set at roughly ten times the recorded baseline, so a suite that turns red means an algorithmic regression rather than a busy machine. Budgets are enforced only in an optimized build at full scale; below `AKGL_BENCH_SCALE=1.0`, and in a coverage build, they are reported without failing.
|
||||
|
||||
`PERFORMANCE.md` carries the recorded baseline, the frame budget it adds up to, and what the numbers say — including the raw-SDL control rows that separate what libakgl costs from what the rasterizer costs. The six defects the stress tests turned up, and the targets the numbers are measured against, are in `TODO.md` under **Performance**.
|
||||
|
||||
## Memory checking
|
||||
|
||||
`memcheck` runs the suites that already exist under valgrind rather than adding suites of its own. The perf binaries carry most of it: they are the only programs in the tree that load assets, draw a scene, and run a frame loop in one process, and `tests/benchutil.h` cuts their iteration counts by three orders of magnitude when it detects valgrind — a benchmark walks one path a hundred thousand times, a leak check wants every path walked once.
|
||||
|
||||
```sh
|
||||
cmake --build build --target memcheck # everything, about 30 seconds
|
||||
scripts/memcheck.sh -R tilemap # one suite; ctest selection flags pass through
|
||||
scripts/memcheck.sh -LE perf # skip the benchmarks
|
||||
```
|
||||
|
||||
The run forces `SDL_VIDEO_DRIVER=dummy`, `SDL_RENDER_DRIVER=software` and `SDL_AUDIO_DRIVER=dummy` so the vendor GPU stack is never loaded — that removes thousands of unfixable findings inside the driver without suppressing anything. Definite leaks, invalid accesses and uninitialised reads are counted and set the exit status; "still reachable" is not, because that is what SDL and FreeType keep for the process lifetime. Suppressions for genuine third-party findings are in `scripts/valgrind.supp`.
|
||||
|
||||
What it found the first time it ran is in `TODO.md` under **Memory checking**.
|
||||
|
||||
2
deps/libakerror
vendored
2
deps/libakerror
vendored
Submodule deps/libakerror updated: 0c0d81249f...5ff87908e7
2
deps/libakstdlib
vendored
2
deps/libakstdlib
vendored
Submodule deps/libakstdlib updated: a87cbfb26d...f8425b8729
@@ -1,9 +1,9 @@
|
||||
#ifndef _SDL_GAMECONTROLLERDB_H_
|
||||
#define _SDL_GAMECONTROLLERDB_H_
|
||||
|
||||
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on Tue Jun 2 04:45:53 PM EDT 2026
|
||||
// Taken from https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt on Fri Jul 31 01:11:07 PM EDT 2026
|
||||
|
||||
#define AKGL_SDL_GAMECONTROLLER_DB_LEN 2229
|
||||
#define AKGL_SDL_GAMECONTROLLER_DB_LEN 2253
|
||||
|
||||
const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000300f00000a01000000000000,3 In 1 Conversion Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b8,x:b3,y:b0,platform:Windows",
|
||||
@@ -86,6 +86,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000c82d00001b30000000000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000c82d00001c30000000000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b2,paddle2:b5,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000c82d00001d30000000000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000c82d00000a31000000000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000c82d00001530000000000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000c82d00001630000000000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000c82d00001730000000000000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
@@ -247,6 +248,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000ac0500001a06000000000000,GameSir T3 2.02,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000373500000410000000000000,GameSir T4 Kaleid,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows",
|
||||
"03000000373500009410000000000000,GameSir Tegenaria Lite,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000373500001611000000000000,GameSir X5 Lite,a:b0,b:b1,x:b3,y:b4,back:b10,guide:b12,start:b11,leftstick:b13,rightstick:b14,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:b8,righttrigger:b9,platform:Windows",
|
||||
"030000004c0e00001035000000000000,Gamester,a:b0,b:b1,back:b7,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b10,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b11,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Windows",
|
||||
"030000000d0f00001110000000000000,GameStick Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"0300000047530000616d000000000000,GameStop,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows",
|
||||
@@ -430,7 +432,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000380700008084000000000000,Mad Catz TE2 PS4 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
|
||||
"030000002a0600001024000000000000,Matricom,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b2,y:b3,platform:Windows",
|
||||
"030000009f000000adbb000000000000,MaxJoypad Virtual Controller,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows",
|
||||
"030000008f0e00001330000000000000,Mayflash Adapter,a:b1,b:b2,back:b8,dpdown:h0.8,dpleft:h0.2,dpright:h0.1,dpup:h0.4,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3~,righty:a2,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"030000008f0e00001330000000000000,Mayflash N64 Adapter,a:b1,b:b2,back:b8,dpdown:h0.8,dpleft:h0.2,dpright:h0.1,dpup:h0.4,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3~,righty:a2,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000250900000128000000000000,Mayflash Arcade Stick,a:b1,b:b2,back:b8,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b5,y:b6,platform:Windows",
|
||||
"03000000242f00003700000000000000,Mayflash F101,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000790000003018000000000000,Mayflash F300 Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
|
||||
@@ -518,6 +520,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"030000006f0e00000901000000000000,PDP PS3 Versus Fighting,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"030000006f0e00008901000000000000,PDP Realmz Nintendo Switch Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"030000008f0e00004100000000000000,PlaySega,a:b1,b:b0,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b8,x:b4,y:b3,platform:Windows",
|
||||
"03000000d620000020a7000000000000,PowerA Advantage Nintendo Switch 2 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,misc2:b16,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000d620000011a7000000000000,PowerA Core Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000dd62000015a7000000000000,PowerA Fusion Nintendo Switch Arcade Stick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000d620000012a7000000000000,PowerA Fusion Nintendo Switch Fight Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b13,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
|
||||
@@ -550,7 +553,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000250900000500000000000000,PS3 Controller,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows",
|
||||
"030000004c0500006802000000000000,PS3 Controller,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b10,lefttrigger:a3~,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:a4~,rightx:a2,righty:a5,start:b8,x:b3,y:b0,platform:Windows",
|
||||
"030000004f1f00000800000000000000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000632500007505000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
|
||||
"03000000632500007505000000000000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000888800000803000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b0,platform:Windows",
|
||||
"03000000888800000804000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows",
|
||||
"030000008f0e00000300000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b3,y:b0,platform:Windows",
|
||||
@@ -640,7 +643,8 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000921200004547000000000000,Retro Bit Sega Genesis Adapter,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b6,x:b3,y:b4,platform:Windows",
|
||||
"03000000790000001100000000000000,Retro Controller,a:b1,b:b2,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000830500006020000000000000,Retro Controller,a:b0,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b5,rightshoulder:b8,righttrigger:b9,start:b7,x:b2,y:b3,platform:Windows",
|
||||
"03000000632500007805000000000000,Retro Fighters Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
|
||||
"03000000632500007805000000000000,Retro Fighters D6,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b10,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
|
||||
"03000000790000001600000000000000,Retro Fighters D6,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"0300000003040000c197000000000000,Retrode Adapter,a:b0,b:b4,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Windows",
|
||||
"03000000bd12000013d0000000000000,Retrolink Sega Saturn Classic Controller,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b5,lefttrigger:b6,rightshoulder:b2,righttrigger:b7,start:b8,x:b3,y:b4,platform:Windows",
|
||||
"03000000bd12000015d0000000000000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows",
|
||||
@@ -687,7 +691,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000a306000020f6000000000000,Saitek PS2700 PlayStation Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000300f00001101000000000000,Saitek Rumble,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows",
|
||||
"03000000e804000000a0000000000000,Samsung EIGP20,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000bd12000010d1000000000000,Sanwa 4Button,a:b0,b:b1,x:b2,y:b3,dpup:-a1,dpdown:+a1,dpleft:-a0,dpright:+a0,platform:Windows",
|
||||
"03000000bd12000010d1000000000000,Sanwa 4Button,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,x:b2,y:b3,platform:Windows",
|
||||
"03000000c01100000252000000000000,Sanwa Easy Grip,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows",
|
||||
"03000000c01100004350000000000000,Sanwa Micro Grip P3,a:b1,b:b0,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,x:b3,y:b2,platform:Windows",
|
||||
"03000000411200004550000000000000,Sanwa Micro Grip Pro,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a1,righty:a2,start:b9,x:b1,y:b3,platform:Windows",
|
||||
@@ -701,14 +705,14 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"030000004f04000028b3000000000000,Score A,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"03000000952e00002577000000000000,Scuf PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
|
||||
"03000000a30c00002500000000000000,Sega Genesis Mini 3B Controller,a:b2,b:b1,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,righttrigger:b5,start:b9,platform:Windows",
|
||||
"03000000a30c00002400000000000000,Sega Mega Drive Mini 6B Controller,a:b2,b:b1,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,rightshoulder:b4,righttrigger:b5,leftshoulder:b6,lefttrigger:b7,back:b8,start:b9,x:b3,y:b0,platform:Windows",
|
||||
"03000000a30c00002400000000000000,Sega Mega Drive Mini 6B Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Windows",
|
||||
"03000000d804000086e6000000000000,Sega Multi Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:a2,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Windows",
|
||||
"0300000000050000289b000000000000,Sega Saturn Adapter,a:b1,b:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b4,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"0300000000f000000800000000000000,Sega Saturn Controller,a:b1,b:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b7,righttrigger:b3,start:b0,x:b5,y:b6,platform:Windows",
|
||||
"03000000730700000601000000000000,Sega Saturn Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b2,start:b9,x:b3,y:b4,platform:Windows",
|
||||
"03000000b40400000a01000000000000,Sega Saturn Controller,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,start:b8,x:b3,y:b4,platform:Windows",
|
||||
"030000003b07000004a1000000000000,SFX,a:b0,b:b2,back:b7,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b9,righttrigger:b5,start:b8,x:b1,y:b3,platform:Windows",
|
||||
"03000000632500002705000000000000,ShanWan Gamepad,a:b0,b:b1,x:b3,y:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftx:a0,-lefty:-a1,+lefty:+a2,leftshoulder:b6,lefttrigger:b8,rightshoulder:b7,righttrigger:b9,start:b11,leftstick:b13,rightstick:b14,platform:Windows",
|
||||
"03000000632500002705000000000000,ShanWan Gamepad,+lefty:+a2,-lefty:-a1,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,rightshoulder:b7,rightstick:b14,righttrigger:b9,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000f82100001900000000000000,Shogun Bros Chameleon X1,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows",
|
||||
"03000000120c00001c1e000000000000,SnakeByte 4S PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
|
||||
"03000000140300000918000000000000,SNES Controller,a:b0,b:b1,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b2,y:b3,platform:Windows",
|
||||
@@ -737,6 +741,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000120c0000160e000000000000,Steel Play Metaltech PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
|
||||
"03000000110100001914000000000000,SteelSeries,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftstick:b13,lefttrigger:b6,leftx:a0,lefty:a1,rightstick:b14,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000381000001214000000000000,SteelSeries Free,a:b0,b:b1,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000381000004114000000000000,SteelSeries Nimbus Cloud,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000110100003114000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows",
|
||||
"03000000381000003014000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows",
|
||||
"03000000381000003114000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows",
|
||||
@@ -780,7 +785,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"030000005f140000c501000000000000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
|
||||
"03000000b80500000210000000000000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows",
|
||||
"030000005f1400002a01000000000000,Trust Predator GM1200,a:b0,b:b1,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b5,righttrigger:b2,x:b3,y:b4,platform:Windows",
|
||||
"03000000580400000a10000000000000,Trust Sight Fighter,a:b0,b:b1,x:b3,y:b4,back:b5,start:b2,leftshoulder:b6,rightshoulder:b7,leftx:a0,lefty:a1,lefttrigger:b8,righttrigger:b9,platform:Windows",
|
||||
"03000000580400000a10000000000000,Trust Sight Fighter,a:b0,b:b1,back:b5,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,start:b2,x:b3,y:b4,platform:Windows",
|
||||
"030000004f04000087b6000000000000,TWCS Throttle,dpdown:b8,dpleft:b9,dpright:b7,dpup:b6,leftstick:b5,lefttrigger:-a5,leftx:a0,lefty:a1,righttrigger:+a5,platform:Windows",
|
||||
"03000000411200000450000000000000,Twin Shock,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a3,righty:a4,start:b11,x:b3,y:b0,platform:Windows",
|
||||
"03000000d90400000200000000000000,TwinShock PS2 Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows",
|
||||
@@ -804,6 +809,8 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"030000006f0e00000302000000000000,Victrix PS4 Pro Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
|
||||
"030000006f0e00000702000000000000,Victrix PS4 Pro Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,touchpad:b13,x:b0,y:b3,platform:Windows",
|
||||
"0300000034120000adbe000000000000,vJoy Device,a:b0,b:b1,back:b15,dpdown:b6,dpleft:b7,dpright:b8,dpup:b5,guide:b16,leftshoulder:b9,leftstick:b13,lefttrigger:b11,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b14,righttrigger:b12,rightx:a3,righty:a4,start:b4,x:b2,y:b3,platform:Windows",
|
||||
"03000000d23800001010000000000000,Void Genesis,a:b0,b:b1,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b2,leftstick:b9,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b18,misc2:b21,misc3:b20,paddle1:b22,paddle2:b15,paddle3:b23,paddle4:b17,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a5,start:b19,x:b3,y:b4,platform:Windows",
|
||||
"03000000d23800001210000000000000,Void Genesis,a:b0,b:b1,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b2,leftstick:b9,lefttrigger:a3,leftx:a0,lefty:a1,misc1:b18,misc2:b21,misc3:b20,paddle1:b22,paddle2:b15,paddle3:b23,paddle4:b17,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a5,start:b19,x:b3,y:b4,platform:Windows",
|
||||
"03000000120c0000ab57000000000000,Warrior Joypad JS083,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows",
|
||||
"030000007e0500003003000000000000,Wii U Pro,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,leftshoulder:b6,leftstick:b11,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b12,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Windows",
|
||||
"0300000032150000030a000000000000,Wildcat,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows",
|
||||
@@ -912,7 +919,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000c82d00000351000000010000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
|
||||
"03000000c82d00001290000001000000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X",
|
||||
"03000000c82d00004028000000010000,8BitDo SN30,a:b1,b:b0,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X",
|
||||
"03000000c82d00000160000001000000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
|
||||
"03000000c82d00000160000001000000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
|
||||
"03000000c82d00000161000000010000,8BitDo SN30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
|
||||
"03000000c82d00000260000001000000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
|
||||
"03000000c82d00000261000000010000,8BitDo SN30 Pro Plus,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X",
|
||||
@@ -942,6 +949,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000503200000110000045010000,Atari VCS Classic,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b3,start:b2,platform:Mac OS X",
|
||||
"03000000503200000110000047010000,Atari VCS Classic Controller,a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b3,start:b2,platform:Mac OS X",
|
||||
"03000000503200000210000047010000,Atari VCS Modern Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b6,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a4,rightx:a2,righty:a3,start:b8,x:b2,y:b3,platform:Mac OS X",
|
||||
"03000000e30500008106000000010000,Austgame GameCube Adapter,a:b0,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:+a4,leftx:a0,lefty:a1,misc3:b5,misc4:b6,rightshoulder:b4,righttrigger:-a4,rightx:a2,righty:a3,start:b7,x:b1,y:b3,platform:Mac OS X",
|
||||
"030000008a3500000102000000010000,Backbone One,a:b0,b:b1,back:b16,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b17,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b15,x:b2,y:b3,platform:Mac OS X",
|
||||
"030000008a3500000201000000010000,Backbone One,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X",
|
||||
"030000008a3500000202000000010000,Backbone One,a:b0,b:b1,back:b16,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b17,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b15,x:b2,y:b3,platform:Mac OS X",
|
||||
@@ -1119,6 +1127,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000110100002014000000000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X",
|
||||
"03000000110100002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X",
|
||||
"03000000381000002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X",
|
||||
"03000000381000004114000000010000,SteelSeries Nimbus Cloud,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X",
|
||||
"05000000484944204465766963650000,SteelSeries Nimbus Plus,a:b0,b:b1,back:b15,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b16,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b14,x:b2,y:b3,platform:Mac OS X",
|
||||
"050000004e696d6275732b0000000000,SteelSeries Nimbus Plus,a:b0,b:b1,back:b15,dpdown:b11,dpleft:b13,dpright:b12,dpup:b10,guide:b16,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3~,start:b14,x:b2,y:b3,platform:Mac OS X",
|
||||
"03000000381000003014000000000000,SteelSeries Stratus Duo,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X",
|
||||
@@ -1250,12 +1259,16 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000c82d00001230000011010000,8BitDo Ultimate,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b2,paddle2:b5,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000c82d00001330000011010000,8BitDo Ultimate,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b26,paddle1:b23,paddle2:b19,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"05000000c82d00001230000000010000,8BitDo Ultimate,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000c82d00000b31000014010000,8BitDo Ultimate 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000c82d00001260000011010000,8BitDo Ultimate 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b17,paddle2:b16,paddle3:b2,paddle4:b5,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"05000000c82d00001260000001000000,8BitDo Ultimate 2,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b17,paddle2:b16,paddle3:b2,paddle4:b5,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000c82d00000a31000014010000,8BitDo Ultimate 2C,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000c82d00001d30000011010000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"05000000c82d00001b30000001000000,8BitDo Ultimate 2C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000c82d00001530000011010000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000c82d00001630000011010000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000c82d00001730000011010000,8BitDo Ultimate C,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"06000000c82d00000320000007010000,8BitDo Ultimate Wired Controller for Xbox,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000c82d00000121000011010000,8BitDo Xbox One SN30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"05000000c82d00000121000000010000,8BitDo Xbox One SN30 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"05000000a00500003232000001000000,8BitDo Zero,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux",
|
||||
@@ -1277,6 +1290,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000020500000913000010010000,Anbernic RG P01,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000373500000710000010010000,Anbernic RG P01,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"05000000373500004610000001000000,Anbernic RG P01,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"19000000010000000100000000010000,Anbernic RG28XX Controller,a:b4,b:b3,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b11,leftshoulder:b7,leftstick:b12,misc1:b1,rightshoulder:b8,rightstick:b13,start:b10,x:b5,y:b6,platform:Linux",
|
||||
"03000000190e00000110000010010000,Aquaplus Piece,a:b1,b:b0,back:b3,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,start:b2,platform:Linux",
|
||||
"03000000790000003018000011010000,Arcade Fightstick F300,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"03000000a30c00002700000011010000,Astro City Mini,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b4,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux",
|
||||
@@ -1311,6 +1325,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"05000000bc2000000055000001000000,Betop AX1 BFM,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000bc2000006412000011010000,Betop Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b30,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux",
|
||||
"030000006b1400000209000011010000,Bigben,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"030000006b1400000055000011010000,Bigben Interactive Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
|
||||
"03000000120c0000300e000011010000,Brook Audio Fighting Board PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"03000000120c0000310e000011010000,Brook Audio Fighting Board PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
|
||||
"03000000120c0000200e000011010000,Brook Mars PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
|
||||
@@ -1348,6 +1363,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000558500001b06000010010000,GameSir G4 Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"05000000ac0500002d0200001b010000,GameSir G4s,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b33,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000ac0500007a05000011010000,GameSir G5,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b16,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000373500005810000001020000,GameSir K1,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000373500009710000001020000,GameSir Kaleid Flux,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000ac0500001a06000011010000,GameSir T3 2.02,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"03000000373500000410000044010000,GameSir T4 Kaleid,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
@@ -1447,6 +1463,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000242f00008a00000011010000,JYS Adapter,a:b1,b:b4,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:b8,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b0,y:b3,platform:Linux",
|
||||
"03000000314100001935000000010000,LeadJoy Xeno Plus,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:+a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:+a5,rightx:a4,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"030000006f0e00000103000000020000,Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"030000006f0e000001f5000010010000,Logic3 Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"030000006d040000d1ca000000000000,Logitech Chillstream,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"030000006d040000d1ca000011010000,Logitech Chillstream,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"030000006d04000016c2000010010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
|
||||
@@ -1537,10 +1554,10 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000853200000706000012010000,Nacon GC100,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"030000006b140000010c000010010000,Nacon GC400ES,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
|
||||
"05000000853200000503000000010000,Nacon MGX Pro,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"0300000085320000170d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
|
||||
"0300000085320000190d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
|
||||
"0300000085320000180d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"0300000085320000160d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
|
||||
"0300000085320000170d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
|
||||
"0300000085320000180d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"0300000085320000190d000011010000,Nacon Revolution 5 Pro,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,touchpad:b13,x:b0,y:b3,platform:Linux",
|
||||
"030000000d0f00000900000010010000,Natec Genesis P44,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"030000004f1f00000800000011010000,NeoGeo PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"0300000092120000474e000000010000,NeoGeo X Arcade Stick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,start:b9,x:b3,y:b2,platform:Linux",
|
||||
@@ -1619,6 +1636,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000d62000000520000050010000,PowerA Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000d62000000b20000001010000,PowerA Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000d62000000f20000001010000,PowerA Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b7,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000d62000006520000002010000,PowerA Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"030000006d040000d2ca000011010000,Precision Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"03000000250900000017000010010000,PS SS N64 Adapter,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b5,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b7,rightx:a2~,righty:a3,start:b8,platform:Linux",
|
||||
"03000000ff1100004133000010010000,PS2 Controller,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux",
|
||||
@@ -1702,6 +1720,8 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"0300000032150000140a000001010000,Razer Wolverine Ultimate Xbox,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"030000000d0f0000c100000010010000,Retro Bit Legacy16,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,guide:b12,leftshoulder:b4,lefttrigger:b6,misc1:b13,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"03000000790000001100000010010000,Retro Controller,a:b1,b:b2,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,lefttrigger:b7,rightshoulder:b4,righttrigger:b5,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"03000000632500007805000011010000,Retro Fighters D6,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux",
|
||||
"03000000790000001600000011010000,Retro Fighters D6,a:b1,b:b2,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,misc1:b10,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux",
|
||||
"0300000003040000c197000011010000,Retrode Adapter,a:b0,b:b4,back:b2,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Linux",
|
||||
"190000004b4800000111000000010000,RetroGame Joypad,a:b1,b:b0,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
|
||||
"0300000081170000990a000001010000,Retronic Adapter,a:b0,leftx:a0,lefty:a1,platform:Linux",
|
||||
@@ -1752,7 +1772,8 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"030000005e0400008e02000073050000,Speedlink Torid,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"030000005e0400008e02000020200000,SpeedLink Xeox Pro Analog,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000de280000ff11000001000000,Steam Virtual Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"05000000381000001214000001000000,SteelSeries Free,a:b0,b:b1,x:b3,y:b4,back:b10,guide:b12,start:b11,leftstick:b13,rightstick:b14,leftshoulder:b6,rightshoulder:b7,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a2,righttrigger:a3,platform:Linux",
|
||||
"05000000381000001214000001000000,SteelSeries Free,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a3,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"05000000381000004114000000010000,SteelSeries Nimbus Cloud,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,paddle1:b5,paddle2:b2,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"050000004e696d6275732b0000000000,SteelSeries Nimbus Plus,a:b0,b:b1,back:b10,guide:b11,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b12,x:b2,y:b3,platform:Linux",
|
||||
"03000000381000003014000075010000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000381000003114000075010000,SteelSeries Stratus Duo,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
@@ -1798,9 +1819,10 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"03000000e00d00000300000003000000,TRBot Virtual Joypad,a:b11,b:b12,back:b15,dpdown:b6,dpleft:b3,dpright:b4,dpup:b5,leftshoulder:b17,leftstick:b21,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b22,righttrigger:a2,rightx:a3,righty:a4,start:b16,x:b13,y:b14,platform:Linux",
|
||||
"03000000f00600000300000003000000,TRBot Virtual Joypad,a:b11,b:b12,back:b15,dpdown:b6,dpleft:b3,dpright:b4,dpup:b5,leftshoulder:b17,leftstick:b21,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b22,righttrigger:a2,rightx:a3,righty:a4,start:b16,x:b13,y:b14,platform:Linux",
|
||||
"030000005f140000c501000010010000,Trust Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux",
|
||||
"03000000f51000001370000000000000,Turtle Beach ReactR,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000f51000001370000000010000,Turtle Beach ReactR,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"06000000f51000000870000003010000,Turtle Beach Recon,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000f51000001370000000010000,Turtle Beach ReactR,a:b0,b:b1,x:b2,y:b3,back:b6,guide:b8,start:b7,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux",
|
||||
"03000000f51000001370000000000000,Turtle Beach ReactR,a:b0,b:b1,x:b2,y:b3,back:b6,guide:b8,start:b7,leftstick:b9,rightstick:b10,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,platform:Linux",
|
||||
"05000000f51000008f01000001000000,Turtle Beach Rematch Nintendo Switch Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
|
||||
"03000000100800000100000010010000,Twin PlayStation Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux",
|
||||
"03000000c0160000e105000010010000,Ultimate Atari Fight Stick,a:b1,b:b2,back:b9,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,rightshoulder:b4,righttrigger:b5,start:b8,x:b0,y:b3,platform:Linux",
|
||||
"03000000151900005678000010010000,Uniplay U6,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
@@ -1887,6 +1909,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"060000005e040000120b00000d050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"060000005e040000120b00000f050000,Xbox Series Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"050000005e040000130b000022050000,Xbox Series X Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"050000005e040000130b000023050000,Xbox Series X Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,misc1:b15,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux",
|
||||
"060000005e040000120b000011050000,Xbox Series X Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux",
|
||||
"03000000450c00002043000010010000,XEOX SL6556 BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux",
|
||||
"05000000172700004431000029010000,XiaoMi Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:a7,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Linux",
|
||||
@@ -2014,6 +2037,7 @@ const char *SDL_GAMECONTROLLER_DB[] = {
|
||||
"5553422c322d6178697320382d627574,iBuffalo Super Famicom Controller,a:b1,b:b0,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b17,rightshoulder:b18,start:b10,x:b3,y:b2,platform:Android",
|
||||
"64306137363261396266353433303531,InterAct GoPad,a:b24,b:b25,leftshoulder:b23,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,x:b21,y:b22,platform:Android",
|
||||
"532e542e442e20496e74657261637420,InterAct HammerHead FX,a:b23,b:b24,back:b30,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b26,leftstick:b22,lefttrigger:b28,leftx:a0,lefty:a1,rightshoulder:b27,rightstick:b25,righttrigger:b29,rightx:a2,righty:a3,start:b31,x:b20,y:b21,platform:Android",
|
||||
"05000000300f00001201000000780f00,Jess Tech Dual Analog Pad,a:b22,b:b23,back:b28,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b24,leftstick:b30,lefttrigger:b25,leftx:a0,lefty:a1,rightshoulder:b26,rightstick:b31,righttrigger:b27,rightx:a3,righty:a2,start:b29,x:b20,y:b21,platform:Android",
|
||||
"65346535636333663931613264643164,Joy-Con,a:b21,b:b22,back:b29,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b25,lefttrigger:b27,leftx:a0,lefty:a1,rightshoulder:b26,righttrigger:b28,rightx:a2,righty:a3,start:b30,x:b23,y:b24,platform:Android",
|
||||
"33346566643039343630376565326335,Joy-Con (L),a:b0,b:b1,back:b7,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b3,leftstick:b15,rightshoulder:b20,start:b17,x:b19,y:b2,platform:Android",
|
||||
"35313531613435623366313835326238,Joy-Con (L),a:b0,b:b1,back:b7,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b3,leftstick:b15,rightshoulder:b20,start:b17,x:b19,y:b2,platform:Android",
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
/**
|
||||
* @file actor.h
|
||||
* @brief Declares the public actor API.
|
||||
* @brief A live thing in the world: state bits, motion, hierarchy, and behaviour hooks.
|
||||
*
|
||||
* An actor is an instance; the akgl_Character it points at is its template. The
|
||||
* actor carries only what is unique to it -- where it is, which way it is
|
||||
* moving, which animation frame it is on -- and borrows speed, acceleration and
|
||||
* the state-to-sprite map from the character. That is what makes a hundred of
|
||||
* one kind of thing cheap.
|
||||
*
|
||||
* **State is a bitmask**, not an enum. An actor is facing left *and* moving left
|
||||
* *and* alive at the same time, and the whole combination is the key that
|
||||
* selects a sprite (see akgl_character_sprite_get). Adding a state means adding
|
||||
* a bit here *and* a name in `src/actor_state_string_names.c`, or character JSON
|
||||
* cannot refer to it.
|
||||
*
|
||||
* **Behaviour attaches as function pointers**, not by inheritance. Every actor
|
||||
* gets the library's default `updatefunc`, `renderfunc`, `facefunc`,
|
||||
* `movementlogicfunc`, `changeframefunc` and `addchild` from
|
||||
* akgl_actor_initialize; a game that wants a different flavour of anything
|
||||
* replaces the pointer on that one actor.
|
||||
*
|
||||
* **Motion is split four ways** -- thrust, environmental, acceleration, and the
|
||||
* velocity that is their sum -- so that a character's own walking and the
|
||||
* world's gravity and drag can be reasoned about separately. See physics.h for
|
||||
* the model and which fields mean what.
|
||||
*
|
||||
* Actors are pool objects (akgl_heap_next_actor) published in
|
||||
* #AKGL_REGISTRY_ACTOR under their name.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_ACTOR_H_
|
||||
@@ -48,214 +74,385 @@
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_30 1 << 30 // 1073741824 0100 0000 0000 0000
|
||||
#define AKGL_ACTOR_STATE_UNDEFINED_31 1 << 31 // 2147483648 1000 0000 0000 0000
|
||||
|
||||
/** @brief Bits in an actor's state word. Fixed by the width of `int32_t state`. */
|
||||
#define AKGL_ACTOR_MAX_STATES 32
|
||||
|
||||
// This is an array of strings equal to actor states from 1-32.
|
||||
// This is built by a utility script and not kept in git, see
|
||||
// the Makefile for lib_src/actor_state_string_names.c
|
||||
/** @brief Maps actor-state bit positions to symbolic names. */
|
||||
/**
|
||||
* @brief Bit position -> state name, as text. Index `i` names the bit `1 << i`.
|
||||
*
|
||||
* akgl_registry_init_actor_state_strings builds
|
||||
* #AKGL_REGISTRY_ACTOR_STATE_STRINGS out of this, which is what lets character
|
||||
* JSON write `"AKGL_ACTOR_STATE_FACE_LEFT"` instead of `2`. A bit whose name
|
||||
* here does not match its `#define` above cannot be referred to from JSON at
|
||||
* all.
|
||||
*
|
||||
* @warning Three known defects, all tracked in TODO.md items 24-26: the
|
||||
* definition in `src/actor_state_string_names.c` is 32 entries while
|
||||
* this declares 33, so reading index 32 reads past the object; bits 11
|
||||
* and 12 are named `UNDEFINED_11`/`UNDEFINED_12` rather than
|
||||
* `MOVING_IN`/`MOVING_OUT`; and the comment above about a generator
|
||||
* script is stale -- there is no such script, and the file is tracked
|
||||
* in git and maintained by hand.
|
||||
*/
|
||||
extern char *AKGL_ACTOR_STATE_STRING_NAMES[AKGL_ACTOR_MAX_STATES+1];
|
||||
|
||||
/** @brief Every facing bit. Clear this before setting one, so an actor faces exactly one way. */
|
||||
#define AKGL_ACTOR_STATE_FACE_ALL (AKGL_ACTOR_STATE_FACE_DOWN | AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_FACE_UP)
|
||||
/** @brief Every planar movement bit. The two depth bits, MOVING_IN and MOVING_OUT, are not in it. */
|
||||
#define AKGL_ACTOR_STATE_MOVING_ALL (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_MOVING_DOWN)
|
||||
|
||||
/** @brief Longest actor name, including the terminator. Names are truncated, not rejected. */
|
||||
#define AKGL_ACTOR_MAX_NAME_LENGTH 128
|
||||
/** @brief Children one actor can carry. A child moves with its parent rather than simulating. */
|
||||
#define AKGL_ACTOR_MAX_CHILDREN 8
|
||||
|
||||
/** @brief Actors in the pool. Override before including heap.h to change it; see heap.h. */
|
||||
#define AKGL_MAX_HEAP_ACTOR 64
|
||||
|
||||
/** @brief Represents a live actor, including state, motion, hierarchy, and behavior callbacks. */
|
||||
typedef struct akgl_Actor {
|
||||
uint8_t refcount;
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||
akgl_Character *basechar;
|
||||
uint8_t curSpriteFrameId;
|
||||
SDL_Time curSpriteFrameTimer;
|
||||
bool curSpriteReversing;
|
||||
uint32_t layer;
|
||||
int32_t state;
|
||||
bool movement_controls_face;
|
||||
void *actorData;
|
||||
bool visible;
|
||||
SDL_Time movetimer;
|
||||
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. One reference per parent, plus one for the registry. */
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
|
||||
akgl_Character *basechar; /**< The template this actor instantiates. Borrowed; no reference is taken. Required before the actor can update, render, or simulate. */
|
||||
uint8_t curSpriteFrameId; /**< Index into the current sprite's `frameids`, not a frame number on the sheet. */
|
||||
SDL_Time curSpriteFrameTimer; /**< When the current frame was shown. The next frame is due once the sprite's `speed` has elapsed. */
|
||||
bool curSpriteReversing; /**< Walking the animation backwards, for a sprite with `loopReverse` set. */
|
||||
uint32_t layer; /**< Which tilemap layer the actor is drawn and simulated on. Set from the object layer it was placed in. */
|
||||
int32_t state; /**< The `AKGL_ACTOR_STATE_*` bitmask. The whole value is the key that selects a sprite. */
|
||||
bool movement_controls_face; /**< When set, the default `facefunc` turns the actor to face whichever way it is moving. Clear it for an actor that aims independently. */
|
||||
void *actorData; /**< The game's own per-actor data. Never read or freed by the library. */
|
||||
bool visible; /**< Whether to draw at all. An actor off-camera is skipped separately, so this is for deliberate hiding. */
|
||||
SDL_Time movetimer; /**< Unused. Nothing in the library reads or writes it. */
|
||||
// Velocity. Combined effect of all forces acting on the actor resulting
|
||||
// in energy along an axis.
|
||||
float32_t vx;
|
||||
float32_t vy;
|
||||
float32_t vz;
|
||||
float32_t vx; /**< Velocity along x, world units per second. Recomputed each step as `ex + tx`; writing it directly is overwritten. */
|
||||
float32_t vy; /**< Velocity along y. On a child actor this is read as an offset from the parent instead. */
|
||||
float32_t vz; /**< Velocity along z. */
|
||||
// Environmental velocity. These are the forces acting on the actor by the
|
||||
// environment (such as gravity and atmospheric drag)
|
||||
float32_t ex;
|
||||
float32_t ey;
|
||||
float32_t ez;
|
||||
float32_t ex; /**< Environmental velocity along x: what gravity and drag have done. Accumulates across steps. */
|
||||
float32_t ey; /**< Environmental velocity along y. This is what carries a falling actor. */
|
||||
float32_t ez; /**< Environmental velocity along z. */
|
||||
// Thrust. Energy originating only from the actor's own acceleration on a
|
||||
// given axis, before the effects of gravity and drag.
|
||||
float32_t tx;
|
||||
float32_t ty;
|
||||
float32_t tz;
|
||||
float32_t tx; /**< Thrust along x: the actor's own effort. Capped at `sx`, which is why gravity can outrun top speed and walking cannot. */
|
||||
float32_t ty; /**< Thrust along y, capped at `sy`. */
|
||||
float32_t tz; /**< Thrust along z, capped at `sz`. */
|
||||
// Acceleration. These are borrowed from the base character object.
|
||||
// A given axis resets to 0 when the actor stops moving in a given axis.
|
||||
float32_t ax;
|
||||
float32_t ay;
|
||||
float32_t az;
|
||||
float32_t ax; /**< Acceleration along x, signed by the direction of travel. Copied from the character each step by the default movement logic. */
|
||||
float32_t ay; /**< Acceleration along y, signed the same way. */
|
||||
float32_t az; /**< Acceleration along z. Not set by the default movement logic. */
|
||||
// Max speed. These are borrowed from the base character object.
|
||||
float32_t sx;
|
||||
float32_t sy;
|
||||
float32_t sz;
|
||||
float32_t sx; /**< Maximum thrust along x, copied from the character. */
|
||||
float32_t sy; /**< Maximum thrust along y. */
|
||||
float32_t sz; /**< Maximum thrust along z. */
|
||||
// Position.
|
||||
float32_t x;
|
||||
float32_t y;
|
||||
float32_t z;
|
||||
float32_t scale;
|
||||
struct akgl_Actor *children[AKGL_ACTOR_MAX_CHILDREN];
|
||||
struct akgl_Actor *parent;
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*renderfunc)(struct akgl_Actor *obj);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*facefunc)(struct akgl_Actor *obj);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*movementlogicfunc)(struct akgl_Actor *obj, float32_t dt);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*changeframefunc)(struct akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child);
|
||||
float32_t x; /**< Position in map pixels. For a child, an offset from the parent. */
|
||||
float32_t y; /**< Position in map pixels. Also what the tilemap's perspective band scales against. */
|
||||
float32_t z; /**< Depth. Carried through the simulation but not used at draw time. */
|
||||
float32_t scale; /**< Draw scale, 1.0 for natural size. Written by akgl_tilemap_scale_actor, or forced to 1.0 when tilemap scaling is off. */
|
||||
struct akgl_Actor *children[AKGL_ACTOR_MAX_CHILDREN]; /**< Attached actors, `NULL` in unused slots. Each holds a reference and is released with the parent. */
|
||||
struct akgl_Actor *parent; /**< The actor this one is attached to, or `NULL`. A child is positioned relative to it and does not simulate on its own. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*updatefunc)(struct akgl_Actor *obj); /**< Per-frame logic: facing, then animation frame. Defaults to akgl_actor_update. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*renderfunc)(struct akgl_Actor *obj); /**< Per-frame draw. Defaults to akgl_actor_render. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*facefunc)(struct akgl_Actor *obj); /**< Chooses the facing bits. Defaults to akgl_actor_automatic_face. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*movementlogicfunc)(struct akgl_Actor *obj, float32_t dt); /**< Called by the physics step before gravity. Defaults to akgl_actor_logic_movement. May raise AKGL_ERR_LOGICINTERRUPT to skip the rest of the step. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*changeframefunc)(struct akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems); /**< Advances the animation. Defaults to akgl_actor_logic_changeframe. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*addchild)(struct akgl_Actor *obj, struct akgl_Actor *child); /**< Attaches a child. Defaults to akgl_actor_add_child. */
|
||||
} akgl_Actor;
|
||||
|
||||
/**
|
||||
* @brief Actor initialize.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @brief Zero a pooled actor, name it, wire up its default behaviour, and register it.
|
||||
*
|
||||
* Sets `scale` to 1.0 and `movement_controls_face` to true, installs all six
|
||||
* default function pointers, publishes the actor in #AKGL_REGISTRY_ACTOR, and
|
||||
* takes the first reference. It does *not* set a character: the actor cannot
|
||||
* update, render, or simulate until akgl_actor_set_character has run.
|
||||
*
|
||||
* @param obj Pooled actor to initialize, normally from akgl_heap_next_actor.
|
||||
* Required. Any previous contents are discarded -- including its
|
||||
* child list, so release an actor rather than reinitializing it.
|
||||
* @param name Registry key, NUL-terminated. Required. Truncated at
|
||||
* #AKGL_ACTOR_MAX_NAME_LENGTH. An existing entry with the same name
|
||||
* is silently replaced, and the actor it displaced becomes
|
||||
* unreachable rather than being released.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p name is `NULL`.
|
||||
* @throws AKERR_KEY If the actor cannot be written into #AKGL_REGISTRY_ACTOR --
|
||||
* in practice, because akgl_registry_init has not run.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_initialize(akgl_Actor *obj, char *name);
|
||||
/**
|
||||
* @brief Actor set character.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param basecharname Registry name of the character to assign.
|
||||
* @brief Bind an actor to a registered character, and copy its top speeds.
|
||||
*
|
||||
* Looks the character up by name and takes its `sx`/`sy` as this actor's speed
|
||||
* limits, zeroing `ax` and `ay` so the actor starts from rest. The character is
|
||||
* borrowed, not referenced: releasing it out from under a live actor leaves a
|
||||
* dangling pointer.
|
||||
*
|
||||
* @param obj The actor to bind. Required.
|
||||
* @param basecharname Registry name of the character. Required. Rebinding an
|
||||
* actor mid-life is allowed, and is how a character swaps
|
||||
* its whole sprite set at once.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p basecharname is `NULL`, or if no
|
||||
* character is registered under that name. The last case is a lookup
|
||||
* miss reported with a pointer status rather than AKERR_KEY.
|
||||
*
|
||||
* @note `sz` is not copied and `az` is not zeroed, so an actor's depth speed
|
||||
* keeps whatever it had. The default movement logic re-copies all three
|
||||
* each step, which papers over it for anything that simulates.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_set_character(akgl_Actor *obj, char *basecharname);
|
||||
/**
|
||||
* @brief Actor render.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @brief Draw an actor's current animation frame, if it is on camera.
|
||||
*
|
||||
* Selects the sprite for the actor's current state, checks it against the
|
||||
* camera, and blits the frame at the actor's position translated into screen
|
||||
* space. A child actor is drawn at its parent's position plus its own, which is
|
||||
* what makes an offset mean an offset.
|
||||
*
|
||||
* Several things are skipped rather than reported, because a frame is not the
|
||||
* place to fail: an actor with no sprite for its state, one off camera, one with
|
||||
* `visible` clear, and one whose frame index has run past its current sprite
|
||||
* are all simply not drawn.
|
||||
*
|
||||
* @param obj The actor to draw. Required, along with its `basechar`.
|
||||
* @return `NULL` on success -- including every skipped case above -- otherwise
|
||||
* an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
|
||||
* @throws AKERR_* Whatever akgl_sprite_sheet_coords_for_frame or the renderer's
|
||||
* `draw_texture` raises.
|
||||
*
|
||||
* @note The destination height is computed from the sprite's *width*, so a
|
||||
* non-square sprite is drawn square.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_render(akgl_Actor *obj);
|
||||
/**
|
||||
* @brief Actor update.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @brief Per-frame logic: turn the actor to face its direction, then advance its animation.
|
||||
*
|
||||
* Calls the actor's `facefunc`, then selects the sprite for the resulting state
|
||||
* and calls `changeframefunc` if that sprite's frame time has elapsed. An actor
|
||||
* with no sprite registered for its current state is left alone and reported as
|
||||
* success -- states change faster than art gets drawn, and a missing sprite
|
||||
* should not stop the frame.
|
||||
*
|
||||
* @param obj The actor to update. Required, along with its `basechar`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
|
||||
* @throws AKERR_* Whatever `facefunc` or `changeframefunc` raises. Note that an
|
||||
* AKERR_KEY from either is swallowed along with the missing-sprite case.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_update(akgl_Actor *obj);
|
||||
/**
|
||||
* @brief Actor logic movement.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @brief The default `movementlogicfunc`: turn movement bits into signed acceleration.
|
||||
*
|
||||
* Re-copies the character's speed limits onto the actor -- so a change to the
|
||||
* character takes effect next step -- and sets `ax`/`ay` to plus or minus the
|
||||
* character's acceleration according to which movement bits are set. It does not
|
||||
* integrate anything; the physics backend does that.
|
||||
*
|
||||
* Opposing bits do not cancel: `MOVING_LEFT` wins over `MOVING_RIGHT` because it
|
||||
* is tested first. An actor with no movement bits keeps its previous
|
||||
* acceleration rather than being zeroed, which is why the input handlers clear
|
||||
* it themselves on key release.
|
||||
*
|
||||
* @param obj The actor to compute acceleration for. Required, along with its
|
||||
* `basechar`.
|
||||
* @param dt Seconds since the previous step. Accepted for the hook's signature;
|
||||
* this implementation does not use it, since it sets an acceleration
|
||||
* rather than integrating one.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
|
||||
*
|
||||
* @note A replacement for this hook may raise AKGL_ERR_LOGICINTERRUPT to tell
|
||||
* the physics step to skip the rest of this actor's tick. This
|
||||
* implementation never does.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_movement(akgl_Actor *obj, float32_t dt);
|
||||
/**
|
||||
* @brief Actor logic changeframe.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param curSprite Sprite currently selected for the actor.
|
||||
* @param curtimems Current frame-clock value.
|
||||
* @brief The default `changeframefunc`: step to the next animation frame.
|
||||
*
|
||||
* Walks forward through the sprite's frames; at the last one it either wraps to
|
||||
* 0, or -- for a sprite with both `loop` and `loopReverse` -- turns round and
|
||||
* walks back down, turning again at frame 0. A sprite with `loop` clear also
|
||||
* wraps to 0 rather than holding the final frame.
|
||||
*
|
||||
* @param obj The actor whose frame index to advance. Required.
|
||||
* @param curSprite The sprite currently selected for the actor, whose frame
|
||||
* count and loop flags decide what happens at the end.
|
||||
* Required in practice, and **not** checked -- a `NULL` here is
|
||||
* a crash, not an error.
|
||||
* @param curtimems The current clock reading. Accepted for the hook's signature;
|
||||
* this implementation does not use it, because the caller has
|
||||
* already decided the frame is due.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *curSprite, SDL_Time curtimems);
|
||||
/**
|
||||
* @brief Actor automatic face.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @brief The default `facefunc`: turn the actor to face whichever way it is moving.
|
||||
*
|
||||
* Clears every facing bit and sets the one matching the first movement bit it
|
||||
* finds, in the order left, right, up, down -- so an actor moving diagonally
|
||||
* faces the horizontal component. An actor with `movement_controls_face` clear
|
||||
* is left alone entirely, which is the hook for something that aims
|
||||
* independently of the way it walks.
|
||||
*
|
||||
* @param obj The actor to turn. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
*
|
||||
* @note An actor that has stopped moving is left with no facing bit at all,
|
||||
* rather than keeping the way it was last facing -- so its state no longer
|
||||
* matches any "standing still facing left" sprite. The implementation
|
||||
* carries a TODO saying as much.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_automatic_face(akgl_Actor *obj);
|
||||
/**
|
||||
* @brief Actor add child.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param child Actor to attach as a child.
|
||||
* @brief Attach one actor to another so it moves with it.
|
||||
*
|
||||
* A child is positioned relative to its parent rather than simulated: the
|
||||
* physics step snaps it to the parent's position plus its own velocity fields,
|
||||
* used here as a fixed offset. That is what a carried lantern or a turret on a
|
||||
* tank wants.
|
||||
*
|
||||
* The parent takes a reference on the child, and releasing the parent releases
|
||||
* every child with it.
|
||||
*
|
||||
* @param obj The parent. Required.
|
||||
* @param child The actor to attach. Required. Must not already have a parent --
|
||||
* this builds a tree, not a graph. Its `x`/`y` become an offset
|
||||
* from the parent's position rather than a world position.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_RELATIONSHIP When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p child is `NULL`.
|
||||
* @throws AKERR_RELATIONSHIP If @p child already has a parent. Detach it first --
|
||||
* though note there is no detach function, so in practice this means
|
||||
* releasing and rebuilding it.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p obj already has #AKGL_ACTOR_MAX_CHILDREN
|
||||
* children.
|
||||
*
|
||||
* @warning Nothing checks for a cycle. Making an actor its own ancestor makes
|
||||
* akgl_heap_release_actor recurse until the stack runs out.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child);
|
||||
|
||||
/*
|
||||
* The control-map handler functions ("cmhf"). These are what
|
||||
* akgl_controller_default binds the arrow keys and the D-pad to, and what a
|
||||
* game's own akgl_Control bindings can point at. They are called with the
|
||||
* control map's target actor, not with a global "player", so they work for any
|
||||
* number of locally controlled actors.
|
||||
*
|
||||
* Each pair is symmetric: the `_on` handler starts movement in a direction and
|
||||
* turns the actor to face it, and the `_off` handler stops it dead -- zeroing
|
||||
* acceleration, thrust, environmental velocity and velocity on that axis, so
|
||||
* there is no coasting. That is an arcade feel rather than a physical one, and
|
||||
* it is deliberate; a game wanting momentum binds its own handlers instead.
|
||||
*
|
||||
* An `_on` handler clears *every* facing and movement bit before setting its
|
||||
* own, so holding two directions at once moves in whichever was pressed last
|
||||
* rather than diagonally.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Actor cmhf left on.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Start the actor moving left, and turn it to face left.
|
||||
* @param obj The actor to move, supplied by the control map. Required, along
|
||||
* with its `basechar`, whose acceleration is negated onto `ax`.
|
||||
* @param event The event that triggered this. Required, but not read -- the
|
||||
* binding has already decided what the event means.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf left off.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Stop the actor moving left, zeroing everything on the x axis.
|
||||
* @param obj The actor to stop. Required. `basechar` is not needed here.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
|
||||
* @note It clears the x axis whichever way the actor was going, so releasing
|
||||
* left while holding right also stops the rightward movement.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_left_off(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf right on.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Start the actor moving right, and turn it to face right.
|
||||
* @param obj The actor to move. Required, along with its `basechar`.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf right off.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Stop the actor moving right, zeroing everything on the x axis.
|
||||
* @param obj The actor to stop. Required.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_right_off(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf up on.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Start the actor moving up the screen, and turn it to face up.
|
||||
* @param obj The actor to move. Required, along with its `basechar`, whose y
|
||||
* acceleration is negated -- y grows downward.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf up off.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Stop the actor moving up, zeroing everything on the y axis.
|
||||
* @param obj The actor to stop. Required.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
|
||||
* @note This also zeroes `ey`, so an actor under gravity has its accumulated
|
||||
* fall cancelled by releasing a movement key.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_up_off(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf down on.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Start the actor moving down the screen, and turn it to face down.
|
||||
* @param obj The actor to move. Required, along with its `basechar`.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p event, or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_on(akgl_Actor *obj, SDL_Event *event);
|
||||
/**
|
||||
* @brief Actor cmhf down off.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Stop the actor moving down, zeroing everything on the y axis.
|
||||
* @param obj The actor to stop. Required.
|
||||
* @param event The event that triggered this. Required, but not read.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p event is `NULL`.
|
||||
* @note Zeroes `ey` as well; see akgl_Actor_cmhf_up_off.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_Actor_cmhf_down_off(akgl_Actor *obj, SDL_Event *event);
|
||||
|
||||
/**
|
||||
* @brief Registry iterate actor.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param registry SDL property registry being queried.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one registered actor.
|
||||
*
|
||||
* Runs the operations the iterator asks for, in a fixed order regardless of the
|
||||
* order the bits were set: layer filter, update, tilemap scaling, render. With
|
||||
* #AKGL_ITERATOR_OP_LAYERMASK set, an actor on any other layer is skipped
|
||||
* entirely; with #AKGL_ITERATOR_OP_TILEMAPSCALE clear, `scale` is forced to 1.0
|
||||
* rather than left alone.
|
||||
*
|
||||
* @param userdata The akgl_Iterator carrying the operation flags, passed through
|
||||
* by `SDL_EnumerateProperties`. Required despite the `void *`.
|
||||
* @param registry #AKGL_REGISTRY_ACTOR, supplied by SDL.
|
||||
* @param name The actor's registry key. Required.
|
||||
*
|
||||
* @warning This is an SDL callback, so it returns `void` and has nowhere to
|
||||
* propagate an error to. It ends in `FINISH_NORETURN`, which logs the
|
||||
* stack trace and then calls libakerror's unhandled-error handler --
|
||||
* whose default implementation **exits the process** with the error
|
||||
* status. An actor whose `updatefunc` fails therefore terminates the
|
||||
* game rather than skipping a frame. Install your own
|
||||
* `akerr_handler_unhandled_error` if that is not what you want.
|
||||
*/
|
||||
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file assets.h
|
||||
* @brief Declares the public assets API.
|
||||
* @brief Loads the game's startup assets into the global mixer and track table.
|
||||
*/
|
||||
|
||||
#ifndef _ASSETS_H_
|
||||
@@ -9,11 +9,35 @@
|
||||
#include <akerror.h>
|
||||
|
||||
/**
|
||||
* @brief Load start bgm.
|
||||
* @param fname Path to the input or output file.
|
||||
* @brief Load a music file, bind it to the background-music track, and start it playing.
|
||||
*
|
||||
* Loads @p fname through `akgl_mixer`, creates a track for it, stores that track
|
||||
* in `akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM]`, and starts playback. The loaded
|
||||
* audio is also published as the global `bgm`. This is a *startup* helper: it
|
||||
* assumes the mixer already exists, so `akgl_game_init` (or a bare
|
||||
* `akgl_audio_init`) has to have run first, and it overwrites whatever the BGM
|
||||
* track slot held without releasing it.
|
||||
*
|
||||
* Loading is deferred inside SDL_mixer (`MIX_LoadAudio(..., true)` predecodes),
|
||||
* so a large file costs its decode time here rather than at first play.
|
||||
*
|
||||
* On any failure after the audio loads, the audio is destroyed again before the
|
||||
* error is returned; the track, if one was created, is not.
|
||||
*
|
||||
* @param fname Path to a music file in any format SDL_mixer can open. Required.
|
||||
* Used verbatim -- it is not resolved against `SDL_GetBasePath()`,
|
||||
* so a relative path is relative to the process working directory.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p fname is `NULL`, if SDL_mixer cannot load the
|
||||
* file (missing, unreadable, or an unsupported format), or if it cannot
|
||||
* allocate a track for it. The message carries `SDL_GetError()`.
|
||||
* @throws AKGL_ERR_SDL If the loaded audio cannot be bound to the new track, or
|
||||
* if playback fails to start.
|
||||
*
|
||||
* @note The infinite-loop request does not currently take effect:
|
||||
* `MIX_PROP_PLAY_LOOPS_NUMBER` is set on property set 0, which is the
|
||||
* "no properties" sentinel rather than a set this function owns, so the
|
||||
* call is rejected and the music plays once.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_load_start_bgm(char *fname);
|
||||
|
||||
|
||||
321
include/akgl/audio.h
Normal file
321
include/akgl/audio.h
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* @file audio.h
|
||||
* @brief Declares the public audio API.
|
||||
*
|
||||
* A small tone generator: a fixed set of voices, each with a waveform, a
|
||||
* frequency -- held, or walking toward another one -- a gate length and an ADSR
|
||||
* envelope, mixed to one stream of float samples. This is what a synthesised-
|
||||
* voice vocabulary needs -- SOUND, PLAY, ENVELOPE and VOL all describe a note
|
||||
* rather than a recording -- and it is deliberately separate from the SDL3_mixer
|
||||
* side of the library, which loads and plays audio *assets*. Nothing here reads
|
||||
* a file.
|
||||
*
|
||||
* The voice table exists whether or not an audio device is open.
|
||||
* akgl_audio_init() connects it to one; without that a caller can still set up
|
||||
* voices and pull samples itself with akgl_audio_mix(), which is how the test
|
||||
* suite exercises the synthesis without depending on a sound card's timing.
|
||||
* What a caller cannot do is set up voices and expect to hear them with no
|
||||
* device open.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_AUDIO_H_
|
||||
#define _AKGL_AUDIO_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/types.h>
|
||||
|
||||
/**
|
||||
* @brief Voices that can sound at once.
|
||||
*
|
||||
* Three, because the machine this vocabulary comes from had three and its
|
||||
* music is written for three. A voice is addressed here by a zero-based index;
|
||||
* a language whose own voices are numbered from one maps them itself.
|
||||
*/
|
||||
#define AKGL_AUDIO_MAX_VOICES 3
|
||||
|
||||
/** @brief Sample rate of the generated stream, in frames per second. */
|
||||
#define AKGL_AUDIO_SAMPLE_RATE 44100
|
||||
|
||||
/** @brief Frames the device callback generates per pass through the mixer. */
|
||||
#define AKGL_AUDIO_MIX_FRAMES 512
|
||||
|
||||
/**
|
||||
* @brief How often a frequency sweep takes one step, in steps per second.
|
||||
*
|
||||
* Sixty, because the machine whose SOUND statement this serves advanced its
|
||||
* sweep on a 60 Hz interrupt and its tunes are written for that rate. It
|
||||
* divides #AKGL_AUDIO_SAMPLE_RATE exactly, so a step boundary always lands on a
|
||||
* whole frame.
|
||||
*/
|
||||
#define AKGL_AUDIO_SWEEP_TICK_HZ 60
|
||||
|
||||
/** @brief Frames between two steps of a sweep. #AKGL_AUDIO_SAMPLE_RATE / #AKGL_AUDIO_SWEEP_TICK_HZ. */
|
||||
#define AKGL_AUDIO_SWEEP_TICK_FRAMES (AKGL_AUDIO_SAMPLE_RATE / AKGL_AUDIO_SWEEP_TICK_HZ)
|
||||
|
||||
/**
|
||||
* @brief Shape of one voice's oscillator.
|
||||
*
|
||||
* The trailing comment on each is the waveform number the C128 SOUND statement
|
||||
* uses for the same shape, for a caller translating one to the other.
|
||||
*/
|
||||
typedef enum {
|
||||
AKGL_AUDIO_WAVE_TRIANGLE = 0, /**< SOUND waveform 0. Soft, flute-like. */
|
||||
AKGL_AUDIO_WAVE_SAWTOOTH = 1, /**< SOUND waveform 1. Bright, brassy. */
|
||||
AKGL_AUDIO_WAVE_SQUARE = 2, /**< SOUND waveform 2. Hollow, reedy. The default. */
|
||||
AKGL_AUDIO_WAVE_NOISE = 3, /**< SOUND waveform 3. Unpitched; percussion. */
|
||||
AKGL_AUDIO_WAVE_SINE = 4 /**< No SOUND equivalent. A pure tone. */
|
||||
} akgl_AudioWaveform;
|
||||
|
||||
/** @brief Holds one voice's oscillator, envelope, and how far through it is. */
|
||||
typedef struct {
|
||||
bool active; /**< Whether this voice is sounding. Cleared by the mixer once gate and release are both spent, so it goes quiet without being told to. */
|
||||
akgl_AudioWaveform waveform; /**< Oscillator shape. Persists across notes; set once with akgl_audio_waveform(). */
|
||||
float32_t hz; /**< Frequency of the current note. 0.0 when the voice has never sounded. */
|
||||
/**
|
||||
* @brief Position through one cycle, 0.0 to 1.0.
|
||||
*
|
||||
* Derived from `elapsed_frames` and `hz` each sample rather than
|
||||
* accumulated, so a long note does not drift off pitch. Reading it is
|
||||
* meaningful; writing it is not.
|
||||
*/
|
||||
float32_t phase;
|
||||
/** @brief Frames the gate stays open, before the release begins. */
|
||||
uint32_t duration_frames;
|
||||
/** @brief Frames generated since the tone started, gate and release. */
|
||||
uint32_t elapsed_frames;
|
||||
/** @brief Frames to rise from silence to full level. 0 starts at full level. */
|
||||
uint32_t attack_frames;
|
||||
/** @brief Frames to fall from full level to `sustain`. 0 drops to it at once. */
|
||||
uint32_t decay_frames;
|
||||
/** @brief Frames to fall to silence after the gate closes. 0 cuts off at once. */
|
||||
uint32_t release_frames;
|
||||
/** @brief Level the envelope decays to and holds, 0.0 to 1.0. */
|
||||
float32_t sustain;
|
||||
/** @brief Pitch the current sweep started from. Equal to `sweep_to_hz` when the voice is not sweeping. */
|
||||
float32_t sweep_from_hz;
|
||||
/** @brief Pitch the sweep is heading for. The pitch holds there once it arrives. */
|
||||
float32_t sweep_to_hz;
|
||||
/** @brief Hertz added or subtracted per sweep step. 0.0 means this voice holds one pitch. */
|
||||
float32_t sweep_step_hz;
|
||||
} akgl_AudioVoice;
|
||||
|
||||
/** @brief The process-wide voice table. */
|
||||
extern akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
|
||||
|
||||
/**
|
||||
* @brief Open an audio device and start pulling samples from the voice table.
|
||||
*
|
||||
* Requires SDL's audio subsystem to be initialized. Repeating the call is a
|
||||
* no-op, so a program that cannot order its initialization precisely may call
|
||||
* it more than once. The voice table is reset only on the first call, so this
|
||||
* does not silence a voice that is already sounding.
|
||||
*
|
||||
* The device is resumed immediately rather than left paused, which is SDL's
|
||||
* default -- a caller who set up a voice and heard nothing would have no error
|
||||
* to explain it.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL If no playback device can be opened -- none present, none
|
||||
* permitted, or SDL's audio subsystem never initialized -- or if the
|
||||
* device cannot be resumed. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void);
|
||||
|
||||
/**
|
||||
* @brief Close the audio device and silence every voice.
|
||||
*
|
||||
* Safe to call when no device is open, and safe to call twice. It also puts the
|
||||
* voice table back to its defaults, so a subsequent akgl_audio_init() starts
|
||||
* from a known state rather than from whatever was left sounding.
|
||||
*
|
||||
* @return `NULL`. There is no failure path -- SDL's stream teardown reports
|
||||
* nothing.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void);
|
||||
|
||||
/**
|
||||
* @brief Sound a note on one voice for a fixed time.
|
||||
*
|
||||
* The voice's envelope and waveform are whatever akgl_audio_envelope() and
|
||||
* akgl_audio_waveform() last set them to. @p ms is the length of the gate: the
|
||||
* voice's release runs *after* it, so a voice with a release stays audible
|
||||
* slightly longer than @p ms. Sounding a voice that is already sounding
|
||||
* restarts it from the beginning of its envelope, and clears any sweep
|
||||
* akgl_audio_sweep() left on it -- this is one pitch, held.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param hz Frequency in hertz. Must be greater than 0. There is no upper
|
||||
* bound check, so a frequency above half the sample rate aliases
|
||||
* rather than being refused.
|
||||
* @param ms Gate length in milliseconds. Must be non-zero -- a zero-length
|
||||
* tone is refused rather than treated as "stop", because
|
||||
* akgl_audio_stop() already means that. Rounded down to a whole
|
||||
* number of frames, so a duration under ~0.023 ms rounds to
|
||||
* nothing.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, if @p hz is not
|
||||
* positive, or if @p ms is 0. Each message says which.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms);
|
||||
|
||||
/**
|
||||
* @brief Sound a note whose pitch walks from one frequency toward another.
|
||||
*
|
||||
* A siren, a laser, a falling bomb: akgl_audio_tone() with the pitch moving.
|
||||
* The note starts at @p from_hz and steps by @p step_hz every
|
||||
* 1/#AKGL_AUDIO_SWEEP_TICK_HZ of a second toward @p to_hz, stopping when it
|
||||
* arrives and holding there for whatever is left of @p ms. Direction comes from
|
||||
* the two frequencies, not from the sign of @p step_hz -- a sweep to a lower
|
||||
* pitch is @p to_hz below @p from_hz, and @p step_hz stays positive.
|
||||
*
|
||||
* The step is taken on the mixer's own frame counter, which is the whole reason
|
||||
* this is here rather than in a caller's step loop: a caller re-issuing tones
|
||||
* ties audible pitch to how often it happens to run, so the same siren changes
|
||||
* shape with the frame rate. Nothing outside this library can see that counter.
|
||||
*
|
||||
* Everything else matches akgl_audio_tone(): the envelope, the waveform, the
|
||||
* gate, and the release that follows it. Equal frequencies are legal and are
|
||||
* simply a held tone, so a caller translating a statement that computes its own
|
||||
* limits does not have to special-case them.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param from_hz Pitch the note starts at. Must be greater than 0.
|
||||
* @param to_hz Pitch the sweep walks toward and then holds. Must be greater
|
||||
* than 0. Below @p from_hz sweeps down, above it sweeps up,
|
||||
* equal to it holds.
|
||||
* @param step_hz Hertz per step. Must be greater than 0; the last step is
|
||||
* clamped to @p to_hz rather than overshooting it. A step larger
|
||||
* than the whole interval arrives in one tick.
|
||||
* @param ms Gate length in milliseconds. Must be non-zero. A gate shorter
|
||||
* than the sweep needs is not an error -- the note ends partway
|
||||
* through, which is what a short siren sounds like.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, if @p from_hz,
|
||||
* @p to_hz or @p step_hz is not positive, or if @p ms is 0. Each
|
||||
* message says which.
|
||||
*
|
||||
* @note A swept voice accumulates its phase one frame at a time instead of
|
||||
* deriving it from the frame counter the way a held note does. Deriving
|
||||
* it assumes a constant frequency, and using it here would jump the
|
||||
* waveform -- an audible click -- at every step. The cost is the small
|
||||
* drift the derived form exists to avoid, which a note that is changing
|
||||
* pitch anyway cannot be said to suffer from.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms);
|
||||
|
||||
/**
|
||||
* @brief Silence one voice immediately, skipping its release.
|
||||
*
|
||||
* A hard cut, not a note-off: the release stage is not run, so the sound stops
|
||||
* on the next sample. Stopping a voice that is already silent is a no-op.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice);
|
||||
|
||||
/**
|
||||
* @brief Choose the oscillator shape one voice will use.
|
||||
*
|
||||
* Takes effect on the next akgl_audio_tone(); it does not reshape a note that
|
||||
* is already sounding.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param waveform One of the ::akgl_AudioWaveform shapes. The setting persists
|
||||
* across notes until changed.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p waveform is
|
||||
* not one of the five defined shapes.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform);
|
||||
|
||||
/**
|
||||
* @brief Set one voice's ADSR envelope.
|
||||
*
|
||||
* @p attack, @p decay and @p release are milliseconds; @p sustain is the level
|
||||
* the envelope decays to and holds while the gate is open, from 0.0 to 1.0. A
|
||||
* zero-length stage is skipped rather than divided by.
|
||||
*
|
||||
* Takes effect on the next akgl_audio_tone().
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param attack Milliseconds to rise from silence to full level. 0 starts at
|
||||
* full level.
|
||||
* @param decay Milliseconds to fall from full level to @p sustain. 0 drops to
|
||||
* it at once.
|
||||
* @param sustain Held level while the gate is open, 0.0 to 1.0 inclusive. 0.0 is
|
||||
* legal and means the note is audible only through its attack and
|
||||
* decay -- a plucked sound.
|
||||
* @param release Milliseconds to fall to silence once the gate closes. 0 cuts
|
||||
* off at once. An attack plus decay longer than the note's gate
|
||||
* is not an error: the release simply starts partway up the ramp.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table, or @p sustain is
|
||||
* outside 0.0 to 1.0. The three durations are unbounded.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release);
|
||||
|
||||
/**
|
||||
* @brief Set the level every voice is scaled by.
|
||||
*
|
||||
* Applied after the voices are summed and before the mix is clamped, so it
|
||||
* takes effect on notes already sounding. akgl_audio_shutdown() puts it back
|
||||
* to 1.0.
|
||||
*
|
||||
* @param level Master level, 0.0 to 1.0 inclusive. 0.0 is silence.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p level is outside 0.0 to 1.0. The message
|
||||
* reports it.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level);
|
||||
|
||||
/**
|
||||
* @brief Report whether a voice is still sounding.
|
||||
*
|
||||
* A voice goes quiet on its own when its gate and release have both elapsed, so
|
||||
* this is how a caller waits out a note without keeping its own clock.
|
||||
*
|
||||
* @param voice Zero-based voice index, 0 to #AKGL_AUDIO_MAX_VOICES - 1.
|
||||
* @param active Receives `true` while the voice is sounding, including during
|
||||
* its release. Required -- the return value is the error context.
|
||||
* The flag is only cleared by the mixer, so a voice whose time is
|
||||
* up still reads active until samples are next generated; with no
|
||||
* device open and nobody calling akgl_audio_mix(), it never
|
||||
* changes.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p voice is outside the table.
|
||||
* @throws AKERR_NULLPOINTER If @p active is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active);
|
||||
|
||||
/**
|
||||
* @brief Generate the next @p frames mono samples from the voice table.
|
||||
*
|
||||
* The device callback installed by akgl_audio_init() is a loop around this. A
|
||||
* host that owns its own audio pipeline can call it directly instead and never
|
||||
* open a device here at all. Samples are single-precision, one channel, in the
|
||||
* range -1.0 to 1.0, and every active voice is advanced by @p frames.
|
||||
*
|
||||
* @param dest Receives @p frames samples. Required, and must have room for all
|
||||
* of them -- the count is trusted, not checked against anything.
|
||||
* Overwritten, not accumulated into.
|
||||
* @param frames How many samples to generate. 0 is a no-op, not an error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p frames is negative.
|
||||
*
|
||||
* @warning This mutates the voice table -- it advances each voice's frame
|
||||
* counter and clears `active` on voices that have finished. Calling it
|
||||
* while a device opened by akgl_audio_init() is running means two
|
||||
* threads advancing the same voices, and this path does not take the
|
||||
* stream lock. Use one or the other, not both.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames);
|
||||
|
||||
#endif // _AKGL_AUDIO_H_
|
||||
@@ -1,6 +1,15 @@
|
||||
/**
|
||||
* @file character.h
|
||||
* @brief Declares the public character API.
|
||||
* @brief The reusable half of an actor: movement constants and a state-to-sprite map.
|
||||
*
|
||||
* A character is the template; an akgl_Actor is the instance. Everything that is
|
||||
* the same for every goblin on the map -- top speed, acceleration, frame dwell
|
||||
* time, and which sprite to draw for which combination of actor-state bits --
|
||||
* lives here and is shared by pointer, so a hundred actors cost one character.
|
||||
*
|
||||
* Characters are pool objects (akgl_heap_next_character) and are published in
|
||||
* the #AKGL_REGISTRY_CHARACTER property registry under their name, which is how
|
||||
* akgl_actor_set_character finds them. akgl_registry_init must have run first.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_CHARACTER_H_
|
||||
@@ -15,64 +24,140 @@
|
||||
|
||||
/** @brief Defines reusable movement parameters and actor-state sprite bindings. */
|
||||
typedef struct akgl_Character {
|
||||
uint8_t refcount;
|
||||
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH];
|
||||
SDL_PropertiesID state_sprites;
|
||||
uint64_t speedtime;
|
||||
float32_t ax;
|
||||
float32_t ay;
|
||||
float32_t az;
|
||||
float32_t sx;
|
||||
float32_t sy;
|
||||
float32_t sz;
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_add)(struct akgl_Character *, akgl_Sprite *, int);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_get)(struct akgl_Character *, int, akgl_Sprite **);
|
||||
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */
|
||||
char name[AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH]; /**< Registry key. Truncated, not rejected, if the source name is longer. */
|
||||
SDL_PropertiesID state_sprites; /**< State bitmask (decimal, as a string) -> akgl_Sprite *. */
|
||||
uint64_t speedtime; /**< Nanoseconds one sprite frame is held before advancing. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */
|
||||
float32_t ax; /**< Acceleration along x, world units per second squared. Copied into an actor by akgl_actor_set_character. */
|
||||
float32_t ay; /**< Acceleration along y. */
|
||||
float32_t az; /**< Acceleration along z. Not read from JSON; stays 0 unless set by hand. */
|
||||
float32_t sx; /**< Maximum speed along x, world units per second. */
|
||||
float32_t sy; /**< Maximum speed along y. */
|
||||
float32_t sz; /**< Maximum speed along z. Not read from JSON; stays 0 unless set by hand. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_add)(struct akgl_Character *, akgl_Sprite *, int); /**< Bound to akgl_character_sprite_add by akgl_character_initialize. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*sprite_get)(struct akgl_Character *, int, akgl_Sprite **); /**< Bound to akgl_character_sprite_get by akgl_character_initialize. */
|
||||
} akgl_Character;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Character initialize.
|
||||
* @param basechar Character whose state-to-sprite map is accessed.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @brief Zero a pooled character, name it, and publish it in the character registry.
|
||||
*
|
||||
* Wipes the struct, copies @p name into it, creates the empty state-to-sprite
|
||||
* property set, binds the `sprite_add`/`sprite_get` function pointers, inserts
|
||||
* the character into #AKGL_REGISTRY_CHARACTER under @p name, and takes the first
|
||||
* reference. Everything numeric (speeds, accelerations, `speedtime`) is left at
|
||||
* zero for the caller -- or akgl_character_load_json -- to fill in.
|
||||
*
|
||||
* @param basechar Pooled character to initialize, normally straight from
|
||||
* akgl_heap_next_character. Required. Any previous contents are
|
||||
* discarded without releasing the sprites they referenced.
|
||||
* @param name Registry key, NUL-terminated. Required. Truncated at
|
||||
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH. A name already in the
|
||||
* registry is silently replaced, and the character it displaced
|
||||
* becomes unreachable rather than being released.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p basechar or @p name is `NULL`, or if SDL
|
||||
* cannot create the state-to-sprite property set (out of memory).
|
||||
* @throws AKERR_KEY If the character cannot be written into
|
||||
* #AKGL_REGISTRY_CHARACTER -- in practice, because akgl_registry_init
|
||||
* has not run and the registry id is still 0.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_initialize(akgl_Character *basechar, char *name);
|
||||
/**
|
||||
* @brief Character sprite add.
|
||||
* @param basechar Character whose state-to-sprite map is accessed.
|
||||
* @param ref Sprite reference to associate with the character.
|
||||
* @param state Actor-state bit mask used as a sprite-map key.
|
||||
* @brief Bind a sprite to one exact combination of actor-state bits.
|
||||
*
|
||||
* The map key is the decimal spelling of @p state, so lookups match the *whole*
|
||||
* value: a sprite added for `FACE_LEFT|MOVING_LEFT` is not found by a lookup for
|
||||
* `FACE_LEFT` alone. Callers are expected to register every combination they
|
||||
* intend to draw. Adding a sprite takes a reference on it.
|
||||
*
|
||||
* @param basechar Character to add the mapping to. Required. Must already have
|
||||
* been through akgl_character_initialize.
|
||||
* @param ref Sprite to draw for @p state. Required. Its `refcount` is
|
||||
* incremented, so the character keeps it alive.
|
||||
* @param state The exact actor-state bitmask (`AKGL_ACTOR_STATE_*`) this
|
||||
* sprite is for. 0 is accepted and is a usable key.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p basechar or @p ref is `NULL`.
|
||||
*
|
||||
* @note Re-adding a different sprite for a @p state that is already mapped
|
||||
* replaces the entry without releasing the sprite it displaced, so the
|
||||
* displaced sprite's reference is never given back.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_add(akgl_Character *basechar, akgl_Sprite *ref, int state);
|
||||
/**
|
||||
* @brief Character sprite get.
|
||||
* @param basechar Character whose state-to-sprite map is accessed.
|
||||
* @param state Actor-state bit mask used as a sprite-map key.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Look up the sprite bound to one exact combination of actor-state bits.
|
||||
*
|
||||
* The counterpart to akgl_character_sprite_add, and matched the same way: an
|
||||
* exact match on the whole @p state value, with no fallback to a subset of the
|
||||
* bits and no default sprite. Unlike most lookups in this codebase, *not finding
|
||||
* one is an error* rather than a successful "nothing here" -- an actor with no
|
||||
* sprite for its current state cannot be drawn.
|
||||
*
|
||||
* @param basechar Character to search. Required.
|
||||
* @param state The exact actor-state bitmask to look up.
|
||||
* @param dest Receives the mapped sprite. Required -- the return value is
|
||||
* spoken for by the error context. Set to `NULL` when there is
|
||||
* no mapping, alongside the AKERR_KEY error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p basechar or @p dest is `NULL`.
|
||||
* @throws AKERR_KEY If no sprite is bound to exactly that @p state. The message
|
||||
* carries the state both as a decimal and as a bit pattern.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_sprite_get(akgl_Character *basechar, int state, akgl_Sprite **dest);
|
||||
|
||||
// This is an SDL iterator so we can't return our error state from it.
|
||||
/**
|
||||
* @brief Character state sprites iterate.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param props SDL property collection being iterated.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @brief `SDL_EnumerateProperties` callback that applies an akgl_Iterator to one state-to-sprite entry.
|
||||
*
|
||||
* Currently implements exactly one operation: with #AKGL_ITERATOR_OP_RELEASE
|
||||
* set, it hands the mapped sprite back to its heap layer. Every other flag is
|
||||
* ignored here. Used to tear a character's sprite map down entry by entry.
|
||||
*
|
||||
* @param userdata The akgl_Iterator carrying the operation flags, passed through
|
||||
* by `SDL_EnumerateProperties`. Required despite the `void *` --
|
||||
* a `NULL` here is an error, not "no operations".
|
||||
* @param props The character's `state_sprites` set, supplied by SDL.
|
||||
* @param name The property key: one state bitmask in decimal. Required.
|
||||
*
|
||||
* @warning This is an SDL callback, so it returns `void` and has nowhere to
|
||||
* propagate an error to. It ends in `FINISH_NORETURN`, which logs the
|
||||
* stack trace and then calls libakerror's unhandled-error handler --
|
||||
* whose default implementation **exits the process** with the error
|
||||
* status. A missing sprite or a `NULL` @p userdata therefore terminates
|
||||
* the game rather than skipping an entry. Install your own
|
||||
* `akerr_handler_unhandled_error` if that is not what you want.
|
||||
*/
|
||||
void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID props, const char *name);
|
||||
|
||||
/**
|
||||
* @brief Character load json.
|
||||
* @param filename Path to the source asset or JSON document.
|
||||
* @brief Build a character from a JSON definition file and register it.
|
||||
*
|
||||
* Claims a character from the pool, then reads: `name` (the registry key),
|
||||
* `sprite_mappings` (an array of `{ "sprite": <name>, "state": [<state names>] }`
|
||||
* objects), `speedtime` in seconds, `speed_x`, `speed_y`, `acceleration_x`, and
|
||||
* `acceleration_y`. Each mapping's `state` array is OR-ed together into one
|
||||
* bitmask by looking every name up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS, so
|
||||
* `["FACE_LEFT", "MOVING_LEFT"]` becomes a single key.
|
||||
*
|
||||
* Every referenced sprite must already be in #AKGL_REGISTRY_SPRITE: this loads
|
||||
* characters, not sprites, so akgl_sprite_load_json runs first. On any failure
|
||||
* the pooled character is released again.
|
||||
*
|
||||
* @param filename Path to the JSON document. Required. Used verbatim -- it is
|
||||
* not resolved against `SDL_GetBasePath()`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p filename is `NULL`; if the file cannot be
|
||||
* opened or does not parse (the message carries jansson's line number
|
||||
* and text); or if a mapping names a sprite that is not in the sprite
|
||||
* registry.
|
||||
* @throws AKERR_KEY If a `state` array contains a name that is not a known
|
||||
* actor state, if a required top-level key is absent, or if the
|
||||
* character cannot be added to the registry.
|
||||
* @throws AKERR_TYPE If a key is present but holds the wrong JSON type -- for
|
||||
* example `speedtime` as a string. The message names the key.
|
||||
* @throws AKERR_OUTOFBOUNDS If a `state` array is indexed past its end.
|
||||
* @throws AKGL_ERR_HEAP If the character pool or the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_character_load_json(char *filename);
|
||||
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
/**
|
||||
* @file controller.h
|
||||
* @brief Declares the public controller API.
|
||||
* @brief Input binding: SDL events in, actor state changes out.
|
||||
*
|
||||
* A control map ties one actor to one keyboard and one gamepad, and holds up to
|
||||
* #AKGL_MAX_CONTROLS bindings. A binding says "when event X arrives from device
|
||||
* Y carrying button or key Z, call this handler". Eight maps means up to eight
|
||||
* locally controlled players, each on its own device.
|
||||
*
|
||||
* The host pumps SDL events into akgl_controller_handle_event(), which scans the
|
||||
* maps in order and stops at the first binding that claims the event -- so a key
|
||||
* bound in two maps only fires once, in the lower-numbered one.
|
||||
*
|
||||
* Alongside that, and independent of it, every key press is pushed into a small
|
||||
* ring buffer that akgl_controller_poll_key() and akgl_controller_poll_keystroke()
|
||||
* drain. That serves a caller that wants "is there a key waiting" without owning
|
||||
* an event loop, and it sees keys whether or not a control map also claimed them.
|
||||
*
|
||||
* The two pollers read the same buffer and differ in what they hand back. A game
|
||||
* asking "was the up arrow pressed" wants a keycode and nothing else, which is
|
||||
* akgl_controller_poll_key(). A line editor asking "what did the user type" needs
|
||||
* the modifier state and the character SDL composed -- there is no `"` keycode,
|
||||
* and no keycode at all for a dead key or an IME commit -- which is
|
||||
* akgl_controller_poll_keystroke().
|
||||
*/
|
||||
|
||||
#ifndef _CONTROLLER_H_
|
||||
@@ -9,112 +30,308 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include "types.h"
|
||||
// The binding handlers below take an akgl_Actor *, so this header cannot be
|
||||
// included before akgl/actor.h without one. akgl_Actor is a typedef of a named
|
||||
// struct, and repeating a typedef is not C99, so it is included rather than
|
||||
// forward-declared. actor.h reaches only types.h and character.h, so this does
|
||||
// not close a cycle.
|
||||
#include <akgl/actor.h>
|
||||
|
||||
/** @brief How many control maps exist -- effectively the local player limit. */
|
||||
#define AKGL_MAX_CONTROL_MAPS 8
|
||||
/** @brief Bindings per control map. The default map installed by akgl_controller_default uses 8 of them. */
|
||||
#define AKGL_MAX_CONTROLS 32
|
||||
|
||||
/**
|
||||
* @brief Keystrokes akgl_controller_handle_event() will hold for a poller.
|
||||
*
|
||||
* The Commodore keyboard buffer this serves held ten. Thirty-two is enough that
|
||||
* a program which polls once per frame never loses a key to a fast typist, and
|
||||
* small enough that the buffer stays a fixed-size object in the library's data
|
||||
* segment.
|
||||
*/
|
||||
#define AKGL_CONTROLLER_KEY_BUFFER 32
|
||||
|
||||
/**
|
||||
* @brief Bytes of composed text one buffered keystroke can carry, NUL included.
|
||||
*
|
||||
* A UTF-8 code point is at most four bytes, so this holds one character and its
|
||||
* terminator with room to spare. An input event carrying more than fits -- an
|
||||
* IME committing a whole word at once -- is truncated on a code point boundary
|
||||
* rather than split through the middle of one.
|
||||
*/
|
||||
#define AKGL_CONTROLLER_KEYSTROKE_TEXT 8
|
||||
|
||||
/** @brief One buffered keystroke: which key, which modifiers, and what it typed. */
|
||||
typedef struct {
|
||||
SDL_Keycode key; /**< The keycode, or 0 for an entry that carries only composed text -- an IME commit, or a character finished by a dead key. */
|
||||
SDL_Keymod mod; /**< Modifier state when the key went down. 0 on a text-only entry, whose text already reflects them. */
|
||||
char text[AKGL_CONTROLLER_KEYSTROKE_TEXT]; /**< What the keystroke composed to, UTF-8. The empty string for a key that produces no character, such as an arrow or a bare modifier. */
|
||||
} akgl_Keystroke;
|
||||
|
||||
/** @brief Maps one SDL input to pressed and released callbacks. */
|
||||
typedef struct {
|
||||
uint32_t event_on;
|
||||
uint32_t event_off;
|
||||
uint8_t button;
|
||||
SDL_Keycode key;
|
||||
uint8_t axis;
|
||||
uint8_t axis_range_min;
|
||||
uint8_t axis_range_max;
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*handler_on)(akgl_Actor *obj, SDL_Event *event);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event);
|
||||
uint32_t event_on; /**< SDL event type that fires `handler_on`, e.g. `SDL_EVENT_KEY_DOWN`. */
|
||||
uint32_t event_off; /**< SDL event type that fires `handler_off`, e.g. `SDL_EVENT_KEY_UP`. */
|
||||
uint8_t button; /**< Gamepad button (`SDL_GamepadButton`) this binding is for. Only consulted for gamepad events. */
|
||||
SDL_Keycode key; /**< Keycode this binding is for. Only consulted for keyboard events. */
|
||||
uint8_t axis; /**< Analogue axis. Declared but not yet consulted by akgl_controller_handle_event. */
|
||||
uint8_t axis_range_min; /**< Low end of the axis range that counts as "on". Not yet consulted. */
|
||||
uint8_t axis_range_max; /**< High end of that range. Not yet consulted. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*handler_on)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_on`. Required if `event_on` can fire. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*handler_off)(akgl_Actor *obj, SDL_Event *event); /**< Called with the map's target on `event_off`. */
|
||||
} akgl_Control;
|
||||
|
||||
/** @brief Groups input bindings for one actor and its input devices. */
|
||||
typedef struct {
|
||||
akgl_Actor *target;
|
||||
uint16_t nextMap;
|
||||
akgl_Control controls[AKGL_MAX_CONTROLS];
|
||||
SDL_KeyboardID kbid;
|
||||
SDL_JoystickID jsid;
|
||||
SDL_MouseID mouseid;
|
||||
SDL_PenID penid;
|
||||
akgl_Actor *target; /**< The actor these bindings drive. A `NULL` target makes the whole map inert, which is how an unused slot is spelled. */
|
||||
uint16_t nextMap; /**< Number of bindings in use; the index akgl_controller_pushmap writes to next. */
|
||||
akgl_Control controls[AKGL_MAX_CONTROLS]; /**< The bindings, scanned in order. */
|
||||
SDL_KeyboardID kbid; /**< Keyboard this map listens to. A keyboard event from any other id is ignored, which is what keeps two players on two keyboards apart. */
|
||||
SDL_JoystickID jsid; /**< Gamepad this map listens to, matched the same way. */
|
||||
SDL_MouseID mouseid; /**< Mouse this map listens to. Declared but not yet consulted. */
|
||||
SDL_PenID penid; /**< Pen this map listens to. Declared but not yet consulted. */
|
||||
} akgl_ControlMap;
|
||||
|
||||
/** @brief Stores all process-wide actor input maps. */
|
||||
/** @brief Every control map. Zeroed by akgl_game_init; index it by the same id the functions below take. */
|
||||
extern akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
||||
|
||||
/**
|
||||
* @brief Controller list keyboards.
|
||||
* @brief Log every attached keyboard and its SDL id.
|
||||
*
|
||||
* A diagnostic, not a query: it writes to the SDL log rather than returning
|
||||
* anything. Its use is finding the `kbid` to hand akgl_controller_default when
|
||||
* more than one keyboard is attached.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot enumerate keyboards. The message
|
||||
* carries `SDL_GetError()`; note that "no keyboards attached" is
|
||||
* reported by SDL as an empty list, not as a failure.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void);
|
||||
|
||||
/**
|
||||
* @brief Controller handle event.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Dispatch one SDL event to whichever control map binds it.
|
||||
*
|
||||
* The entry point for the whole subsystem -- call it for every event the host
|
||||
* pumps. A key press is recorded in the poll buffer first, whether or not any
|
||||
* map wants it, and an `SDL_EVENT_TEXT_INPUT` is attached to the press it
|
||||
* belongs to; then the maps are scanned in index order and, within a map,
|
||||
* bindings in the order they were pushed. The **first** binding whose event type
|
||||
* and device id and button/key all match wins, and the scan stops there.
|
||||
*
|
||||
* An event nothing binds is not an error: it returns success having done
|
||||
* nothing, which is what lets a host pass every event through unconditionally.
|
||||
*
|
||||
* @param appstate Passed through from SDL's callback. Required -- but only as a
|
||||
* non-`NULL` token: nothing here reads it. Pass any non-`NULL`
|
||||
* pointer if your program has no app state.
|
||||
* @param event The event to dispatch. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
|
||||
* @throws AKERR_* Whatever the matched binding's handler raises.
|
||||
*
|
||||
* @warning A matched binding's handler pointer is not checked, so a control
|
||||
* pushed with a `NULL` `handler_on` or `handler_off` crashes when its
|
||||
* event arrives.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_event(void *appstate, SDL_Event *event);
|
||||
|
||||
/**
|
||||
* @brief Controller handle button down.
|
||||
* @brief Declared but not defined under this name. Do not call.
|
||||
*
|
||||
* The implementation exists as `gamepad_handle_button_down` in `src/controller.c`
|
||||
* and is `static`-in-spirit -- it is not declared anywhere -- so a caller that
|
||||
* uses this declaration compiles and then fails to link. TODO.md, "Known and
|
||||
* still open" item 10.
|
||||
*
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @param event SDL input event to process.
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_down(void *appstate, SDL_Event *event);
|
||||
/**
|
||||
* @brief Controller handle button up.
|
||||
* @brief Declared but not defined under this name. Do not call.
|
||||
*
|
||||
* See akgl_controller_handle_button_down. The implementation is
|
||||
* `gamepad_handle_button_up`.
|
||||
*
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @param event SDL input event to process.
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_button_up(void *appstate, SDL_Event *event);
|
||||
/**
|
||||
* @brief Controller handle added.
|
||||
* @brief Declared but not defined under this name. Do not call.
|
||||
*
|
||||
* See akgl_controller_handle_button_down. The implementation is
|
||||
* `gamepad_handle_added`, which opens a newly plugged-in gamepad.
|
||||
*
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @param event SDL input event to process.
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_added(void *appstate, SDL_Event *event);
|
||||
/**
|
||||
* @brief Controller handle removed.
|
||||
* @brief Declared but not defined under this name. Do not call.
|
||||
*
|
||||
* See akgl_controller_handle_button_down. The implementation is
|
||||
* `gamepad_handle_removed`, which closes an unplugged gamepad.
|
||||
*
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @param event SDL input event to process.
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_handle_removed(void *appstate, SDL_Event *event);
|
||||
|
||||
/**
|
||||
* @brief Controller pushmap.
|
||||
* @param controlmapid Index of the control map to update.
|
||||
* @param control Control binding to append to the selected map.
|
||||
* @brief Append a binding to a control map.
|
||||
*
|
||||
* The binding is copied, so the caller's `akgl_Control` can be a stack local
|
||||
* reused across pushes -- which is exactly what akgl_controller_default does.
|
||||
* Bindings can only be appended; there is no remove, and no way to reset a map
|
||||
* short of zeroing it in ::GAME_ControlMaps directly.
|
||||
*
|
||||
* @param controlmapid Which map to append to, 0 through
|
||||
* #AKGL_MAX_CONTROL_MAPS - 1.
|
||||
* @param control The binding to copy in. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p control is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above
|
||||
* #AKGL_MAX_CONTROL_MAPS, or if the map already holds
|
||||
* #AKGL_MAX_CONTROLS bindings.
|
||||
*
|
||||
* @warning A **negative** @p controlmapid is not rejected -- only the upper
|
||||
* bound is checked -- and indexes before the start of
|
||||
* ::GAME_ControlMaps. TODO.md, "Known and still open" item 11.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akgl_Control *control);
|
||||
|
||||
/**
|
||||
* @brief Controller default.
|
||||
* @param controlmapid Index of the control map to update.
|
||||
* @param actorname Registry name of the controlled actor.
|
||||
* @param kbid SDL keyboard identifier assigned to the map.
|
||||
* @param jsid SDL joystick or gamepad identifier assigned to the map.
|
||||
* @brief Bind an actor to the arrow keys and the D-pad, in one call.
|
||||
*
|
||||
* Points the map at the named actor and pushes eight bindings: the four arrow
|
||||
* keys and the four D-pad directions, each wired to the matching
|
||||
* `akgl_Actor_cmhf_*_on`/`_off` pair. It is the "just give me something that
|
||||
* works" path -- a game wanting different keys builds its own bindings with
|
||||
* akgl_controller_pushmap.
|
||||
*
|
||||
* @param controlmapid Which map to configure, 0 through
|
||||
* #AKGL_MAX_CONTROL_MAPS - 1.
|
||||
* @param actorname Registry name of the actor to drive. Required in practice,
|
||||
* though a `NULL` is reported as "not found" rather than as
|
||||
* a null pointer.
|
||||
* @param kbid SDL keyboard id to listen to. Only events from this
|
||||
* keyboard match; see akgl_controller_list_keyboards for
|
||||
* how to find it.
|
||||
* @param jsid SDL gamepad id to listen to, matched the same way.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_REGISTRY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p controlmapid is at or above
|
||||
* #AKGL_MAX_CONTROL_MAPS, or if the map cannot hold eight more bindings.
|
||||
* @throws AKGL_ERR_REGISTRY If @p actorname is not in #AKGL_REGISTRY_ACTOR --
|
||||
* usually because the actor has not been created yet.
|
||||
*
|
||||
* @warning A negative @p controlmapid is not rejected. See
|
||||
* akgl_controller_pushmap.
|
||||
* @note It appends rather than replaces, so calling it twice on the same map
|
||||
* leaves sixteen bindings and the first eight are the ones that fire.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid);
|
||||
|
||||
/**
|
||||
* @brief Controller open gamepads.
|
||||
* @brief Open every gamepad currently attached.
|
||||
*
|
||||
* SDL will not deliver button events from a gamepad nobody has opened, so this
|
||||
* runs once at startup -- akgl_game_init calls it. Devices plugged in later are
|
||||
* SDL_EVENT_GAMEPAD_ADDED events, handled elsewhere.
|
||||
*
|
||||
* No gamepads attached is success, not an error.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL reports gamepads present but cannot
|
||||
* enumerate them, or if one of them cannot be opened. The message
|
||||
* carries `SDL_GetError()`.
|
||||
*
|
||||
* @note The enumeration array is only freed on the success path, so a failure
|
||||
* part-way through leaks it.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_open_gamepads(void);
|
||||
|
||||
/**
|
||||
* @brief Take the oldest waiting keystroke, if there is one.
|
||||
*
|
||||
* The rest of this header is built around the host pumping SDL events into
|
||||
* akgl_controller_handle_event(), which suits a game loop and does not suit an
|
||||
* embedded interpreter asking "is there a key waiting, yes or no" without
|
||||
* owning the event loop itself. Every key press that reaches
|
||||
* akgl_controller_handle_event() is recorded in a fixed ring buffer first,
|
||||
* whether or not a control map claims it, and this drains that buffer one
|
||||
* keystroke per call.
|
||||
*
|
||||
* When no key is waiting the call still succeeds: @p available is set to
|
||||
* `false` and @p keycode to 0. The caller polls, it does not block.
|
||||
*
|
||||
* A full buffer drops the *newest* keystroke rather than the oldest, so what
|
||||
* was typed first is what is read first. This runs on whichever thread pumps
|
||||
* events; it is not synchronized.
|
||||
*
|
||||
* Entries that carry only composed text and no keycode are discarded on the way
|
||||
* past rather than reported as keycode 0: this form is for a caller that acts on
|
||||
* keys, and there is no key to report. A caller that wants those characters uses
|
||||
* akgl_controller_poll_keystroke() instead.
|
||||
*
|
||||
* @param keycode Receives the SDL keycode, or 0 when nothing was waiting.
|
||||
* Required -- the return value is the error context.
|
||||
* @param available Receives `true` when a keystroke was taken, `false` when the
|
||||
* buffer was empty. Required. Check this rather than testing
|
||||
* @p keycode against 0.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p keycode or @p available is `NULL`. Both are
|
||||
* required; there is no "I only want to know whether one is waiting"
|
||||
* form.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available);
|
||||
|
||||
/**
|
||||
* @brief Take the oldest waiting keystroke whole: key, modifiers and text.
|
||||
*
|
||||
* akgl_controller_poll_key() with nothing thrown away. A line editor needs all
|
||||
* three: the keycode to recognise Backspace and Return, the modifier state to
|
||||
* tell Ctrl-C from a `c`, and the composed text because a keycode cannot express
|
||||
* what a shifted key produces on the user's own layout. `"`, `!`, `(`, `)`, `:`
|
||||
* and `;` are all unreachable from a keycode alone, and so is every lower-case
|
||||
* letter.
|
||||
*
|
||||
* The text is what SDL composed, taken from the `SDL_EVENT_TEXT_INPUT` that
|
||||
* follows the key press -- the only correct way to get a character out of SDL,
|
||||
* and what makes a keyboard layout, a compose key and a dead key work. **SDL
|
||||
* only sends those events while text input is started**, so a host that wants
|
||||
* the `text` field populated calls `SDL_StartTextInput()` on its window first.
|
||||
* Without it, `key` and `mod` still arrive and `text` is always empty.
|
||||
*
|
||||
* The two pollers drain the same buffer, so a keystroke taken by one is not
|
||||
* waiting for the other.
|
||||
*
|
||||
* @param dest Receives the keystroke. Required. Written only when a
|
||||
* keystroke was waiting; zeroed otherwise, so `key` is 0 and
|
||||
* `text` is the empty string.
|
||||
* @param available Receives `true` when a keystroke was taken, `false` when the
|
||||
* buffer was empty. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p available is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available);
|
||||
|
||||
/**
|
||||
* @brief Discard every keystroke waiting in the buffer.
|
||||
*
|
||||
* For a caller that has been ignoring input and does not want a backlog acted
|
||||
* on the moment it starts polling again.
|
||||
*
|
||||
* This empties only the polling buffer. Keys already dispatched to control maps
|
||||
* have had their effect and cannot be taken back.
|
||||
*
|
||||
* @return `NULL`. There is no failure path -- it resets two counters.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void);
|
||||
#endif // _CONTROLLER_H_
|
||||
|
||||
@@ -1,16 +1,236 @@
|
||||
/**
|
||||
* @file draw.h
|
||||
* @brief Declares the public draw API.
|
||||
*
|
||||
* Immediate-mode plotting against whichever renderer the caller hands in. This
|
||||
* is the shape a BASIC-style graphics vocabulary needs -- DRAW, BOX, CIRCLE,
|
||||
* PAINT, SSHAPE and GSHAPE all say "put this on the screen now" rather than
|
||||
* "add this to the scene" -- and it sits alongside the actor and tilemap
|
||||
* rendering rather than replacing it.
|
||||
*
|
||||
* Every entry point takes its color as an argument instead of reading a
|
||||
* current-color global. A caller that has a notion of a current color (a BASIC
|
||||
* COLOR statement, say) already owns that state and does not need the library
|
||||
* to keep a second copy that can disagree with it. The renderer's own draw
|
||||
* color is saved and restored around each call, so drawing a line never changes
|
||||
* what the next SDL_RenderClear() paints.
|
||||
*/
|
||||
|
||||
#ifndef _DRAW_H_
|
||||
#define _DRAW_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/types.h>
|
||||
|
||||
/**
|
||||
* @brief Draw background.
|
||||
* @param w Destination width.
|
||||
* @param h Destination height.
|
||||
* @brief Spans akgl_draw_flood_fill() will hold while walking a region.
|
||||
*
|
||||
* The fill keeps a fixed stack of horizontal runs still to be examined rather
|
||||
* than recursing per pixel. A region complicated enough to need more than this
|
||||
* many pending runs at once reports AKERR_OUTOFBOUNDS instead of overflowing;
|
||||
* an ordinary convex or moderately concave shape needs a few dozen.
|
||||
*/
|
||||
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
|
||||
|
||||
/**
|
||||
* @brief Paint an 8x8 grey checkerboard over a region, the way an image editor shows transparency.
|
||||
*
|
||||
* A diagnostic backdrop, not a general primitive -- `charviewer` uses it so a
|
||||
* sprite's transparent pixels are visible rather than blending into black. It
|
||||
* is the one function in this file that does not follow the file's conventions:
|
||||
* it draws through the *global* `renderer` rather than a backend the caller
|
||||
* passes in, it leaves the renderer's draw colour changed, and it reports
|
||||
* nothing.
|
||||
*
|
||||
* @param w Width of the region to cover, in pixels, starting at x = 0.
|
||||
* @param h Height of the region, starting at y = 0. Both round up to whole 8px
|
||||
* cells, so a 12-pixel height paints 16.
|
||||
*/
|
||||
void akgl_draw_background(int w, int h);
|
||||
|
||||
/**
|
||||
* @brief Plot a single pixel.
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param x Horizontal position in render-target pixels. Outside the target
|
||||
* is clipped by SDL, not reported.
|
||||
* @param y Vertical position.
|
||||
* @param color Colour to plot in, including alpha. Blending follows the
|
||||
* renderer's current blend mode, which this does not change.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the plot
|
||||
* itself fails. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Draw a line between two points, endpoints included.
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param x1 Horizontal position of the first endpoint.
|
||||
* @param y1 Vertical position of the first endpoint.
|
||||
* @param x2 Horizontal position of the second endpoint.
|
||||
* @param y2 Vertical position of the second endpoint. Coincident endpoints
|
||||
* draw a single pixel rather than nothing.
|
||||
* @param color Colour to draw in, including alpha.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the line
|
||||
* cannot be drawn.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Draw the outline of a rectangle.
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param rect The rectangle, in render-target pixels. Required. A zero or
|
||||
* negative width or height draws nothing and is not reported.
|
||||
* @param color Colour to draw in, including alpha.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p rect is
|
||||
* `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the
|
||||
* outline cannot be drawn.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Fill a rectangle, outline included.
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param rect The rectangle, in render-target pixels. Required. A zero or
|
||||
* negative width or height fills nothing and is not reported.
|
||||
* @param color Colour to fill with, including alpha.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p rect is
|
||||
* `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if the fill
|
||||
* cannot be drawn.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Draw the outline of a circle.
|
||||
*
|
||||
* SDL3 has no circle primitive, so this plots one with the midpoint circle
|
||||
* algorithm -- integer arithmetic, eight-way symmetry, one pass per octant. A
|
||||
* radius of zero draws the center pixel and nothing else.
|
||||
*
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param x Horizontal position of the centre. Rounded to the nearest whole
|
||||
* pixel -- the algorithm is integer-only.
|
||||
* @param y Vertical position of the centre, rounded the same way.
|
||||
* @param radius Radius in pixels, rounded to the nearest whole pixel. Must not
|
||||
* be negative; 0 draws the centre pixel alone.
|
||||
* @param color Colour to draw in, including alpha. The outline is one pixel
|
||||
* wide and is not anti-aliased.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p radius is negative. The message reports it.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if any of
|
||||
* the plotting passes fails. Failures inside the loop are recorded and
|
||||
* reported once at the end rather than aborting mid-circle, so a partial
|
||||
* arc may already be on the target.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Flood the connected region containing one pixel with a color.
|
||||
*
|
||||
* SDL3 has no flood fill either, and unlike the shape primitives it cannot be
|
||||
* done on the GPU side: the region is defined by what is already on the screen.
|
||||
* This reads the render target back, walks the region on the CPU with a
|
||||
* bounded span stack, and blits the result over the area it touched.
|
||||
*
|
||||
* Filling a region that already holds @p color is a no-op rather than an error.
|
||||
* A seed outside the render target reports AKERR_OUTOFBOUNDS.
|
||||
*
|
||||
* The region is four-connected -- it spreads up, down, left and right, not
|
||||
* diagonally -- and its boundary is any pixel whose colour differs from the
|
||||
* seed's, exactly. There is no tolerance, so an anti-aliased edge stops the fill
|
||||
* at its first blended pixel and leaves a fringe.
|
||||
*
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param x Horizontal position of the seed pixel, in render-target pixels.
|
||||
* Must be inside the target.
|
||||
* @param y Vertical position of the seed pixel.
|
||||
* @param color Colour to fill with. Written over the region rather than blended,
|
||||
* since the pixels going back are the ones just read out.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If the seed is outside the render target -- the
|
||||
* message reports both the seed and the target size -- or if the region
|
||||
* needs more than #AKGL_DRAW_MAX_FLOOD_SPANS pending spans, in which
|
||||
* case **it is left partially filled**.
|
||||
* @throws AKGL_ERR_SDL If the render target cannot be read back, converted,
|
||||
* uploaded, or blitted.
|
||||
*
|
||||
* @warning Not reentrant, and not safe from two threads: the span stack is a
|
||||
* single file-scope array. Nothing that touches an `SDL_Renderer` is
|
||||
* thread-safe either way.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color);
|
||||
|
||||
/**
|
||||
* @brief Read a rectangle of the render target into a surface.
|
||||
*
|
||||
* The save half of SSHAPE/GSHAPE. When `*dest` is `NULL` the function allocates
|
||||
* the surface and the caller owns it from then on -- release it with
|
||||
* SDL_DestroySurface(). When `*dest` already points at a surface of exactly
|
||||
* @p src's dimensions the pixels are copied into it instead, so a caller
|
||||
* saving the same region repeatedly does not churn allocations.
|
||||
*
|
||||
* @param self The backend to read from. Required, along with its `sdl_renderer`.
|
||||
* @param src Rectangle of the render target to read, in pixels. Required. It
|
||||
* must fit *entirely* inside the target: SDL would otherwise clip
|
||||
* the read and hand back a smaller surface than was asked for, which
|
||||
* a caller pasting it back would not notice.
|
||||
* @param dest Address of the destination surface. Required, and `*dest` must be
|
||||
* initialized -- `NULL` to have one allocated (the caller then owns
|
||||
* it and frees it with `SDL_DestroySurface`), or an existing surface
|
||||
* of exactly @p src's dimensions to reuse.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, @p src, or @p dest
|
||||
* is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p src has no area, if it does not fit inside the
|
||||
* render target, or if a supplied `*dest` is a different size from
|
||||
* @p src. Each message reports both sets of dimensions.
|
||||
* @throws AKGL_ERR_SDL If the target size cannot be queried, the pixels cannot
|
||||
* be read, or the copy into a supplied `*dest` fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest);
|
||||
|
||||
/**
|
||||
* @brief Draw a saved surface back onto the render target.
|
||||
*
|
||||
* The restore half of SSHAPE/GSHAPE, taking what akgl_draw_copy_region()
|
||||
* produced. The surface is not consumed and may be pasted as many times as the
|
||||
* caller likes.
|
||||
*
|
||||
* @param self The backend to draw through. Required, along with its
|
||||
* `sdl_renderer`.
|
||||
* @param src The surface to paste. Required. Drawn at its own size -- there is
|
||||
* no scaling -- and *replacing* what is on the target rather than
|
||||
* blending with it, which is GSHAPE's default behaviour and means a
|
||||
* saved region's transparent pixels come back as transparent rather
|
||||
* than letting the background show through.
|
||||
* @param x Horizontal position of the paste's left edge.
|
||||
* @param y Vertical position of its top edge. Parts falling outside the
|
||||
* target are clipped by SDL, not reported.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p src is
|
||||
* `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the surface cannot be uploaded as a texture, its blend
|
||||
* mode cannot be set, or the draw fails.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
|
||||
|
||||
#endif //_DRAW_H_
|
||||
|
||||
@@ -6,21 +6,73 @@
|
||||
#ifndef _ERROR_H_
|
||||
#define _ERROR_H_
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/*
|
||||
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
|
||||
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
|
||||
* registry entry points raise akerr_ErrorContext * instead of returning int, and
|
||||
* the library gained an soname -- so a translation unit that pairs this header
|
||||
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
|
||||
*
|
||||
* libakerror publishes no version macro, so this feature-tests on
|
||||
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
|
||||
* version number that does not exist. Without the guard an embedded build is
|
||||
* fine but a stale installed header fails much further in, on the AKGL_ERR_*
|
||||
* codes below and again inside src/heap.c.
|
||||
*
|
||||
* See deps/libakerror/UPGRADING.md.
|
||||
*/
|
||||
#ifndef AKERR_FIRST_CONSUMER_STATUS
|
||||
#error "libakgl requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
|
||||
#endif
|
||||
|
||||
// This macro is used to silence warnings on string concatenation operations that may fail.
|
||||
// e.g., combining two element of PATH_MAX into a string buffer of AKGL_STRING_MAX_LENGTH.
|
||||
// We have to draw a line in the sand somewhere or we will just let our buffers grow forever
|
||||
// to keep the compiler happy.
|
||||
#define DISABLE_GCC_WARNING_FORMAT_TRUNCATION \
|
||||
_Pragma("GCC diagnostic push") \
|
||||
#define DISABLE_GCC_WARNING_FORMAT_TRUNCATION \
|
||||
_Pragma("GCC diagnostic push") \
|
||||
_Pragma("GCC diagnostic ignored \"-Wformat-truncation\"")
|
||||
|
||||
#define RESTORE_GCC_WARNINGS \
|
||||
#define RESTORE_GCC_WARNINGS \
|
||||
_Pragma("GCC diagnostic pop")
|
||||
|
||||
#define AKGL_ERR_SDL (AKERR_LAST_ERRNO_VALUE + 18)
|
||||
#define AKGL_ERR_REGISTRY (AKERR_LAST_ERRNO_VALUE + 19)
|
||||
#define AKGL_ERR_HEAP (AKERR_LAST_ERRNO_VALUE + 20)
|
||||
#define AKGL_ERR_BEHAVIOR (AKERR_LAST_ERRNO_VALUE + 21)
|
||||
#define AKGL_ERR_LOGICINTERRUPT (AKERR_LAST_ERRNO_VALUE + 22)
|
||||
// libakerror reserves statuses 0-255 for the host's errno values and its own
|
||||
// AKERR_* codes; consumers allocate from AKERR_FIRST_CONSUMER_STATUS upward.
|
||||
// These are fixed offsets from that base rather than from AKERR_LAST_ERRNO_VALUE
|
||||
// so that a libc which grows an errno cannot move them out from under us.
|
||||
//
|
||||
// akgl_error_init() reserves this whole band in one call and registers a name
|
||||
// for every code below. Add a code here and you must name it there, or it
|
||||
// prints as "Unknown Error" in every stack trace that carries it.
|
||||
#define AKGL_ERR_OWNER "libakgl"
|
||||
#define AKGL_ERR_BASE AKERR_FIRST_CONSUMER_STATUS
|
||||
|
||||
#define AKGL_ERR_SDL (AKGL_ERR_BASE + 0) /**< An SDL call failed; the message carries SDL_GetError() */
|
||||
#define AKGL_ERR_REGISTRY (AKGL_ERR_BASE + 1) /**< A registry property or lookup operation failed */
|
||||
#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /**< A heap pool has no free object left to hand out */
|
||||
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
|
||||
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
|
||||
|
||||
// One past the last libakgl status. The reservation is all-or-nothing -- a
|
||||
// subset or superset of an existing one is refused -- so this must stay one
|
||||
// past the highest code above.
|
||||
#define AKGL_ERR_LIMIT (AKGL_ERR_BASE + 5)
|
||||
#define AKGL_ERR_COUNT (AKGL_ERR_LIMIT - AKGL_ERR_BASE)
|
||||
|
||||
/**
|
||||
* @brief Claim the libakgl status band and register a name for every code in it.
|
||||
*
|
||||
* Call this before anything else in libakgl. Every other subsystem raises
|
||||
* AKGL_ERR_* codes, and a code raised before this runs carries no name into its
|
||||
* stack trace. Repeating the call is a no-op, so a program that cannot order its
|
||||
* initialization precisely may call it more than once.
|
||||
*
|
||||
* @throws AKERR_STATUS_RANGE_OVERLAP When another component already owns part of the band.
|
||||
* @throws AKERR_STATUS_RANGE_FULL When libakerror has no reservation slots left.
|
||||
* @throws AKERR_STATUS_NAME_FULL When libakerror's name registry is full.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_error_init(void);
|
||||
|
||||
#endif // _ERROR_H_
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
/**
|
||||
* @file game.h
|
||||
* @brief Declares the public game API.
|
||||
* @brief Process-wide game state, the startup sequence, and the per-frame tick.
|
||||
*
|
||||
* This is the top of the library. `akgl_game_init` brings up SDL, the pools, the
|
||||
* registries and the audio and font engines; `akgl_game_update` is one frame --
|
||||
* update every actor, step the physics, draw the world.
|
||||
*
|
||||
* There is exactly one of everything. `renderer`, `physics`, `camera`, and
|
||||
* `gamemap` are globals pointing at the `_akgl_*` storage below them, so a
|
||||
* program can swap in its own instance by reassigning the pointer without the
|
||||
* rest of the library knowing. That is the whole extent of the indirection:
|
||||
* there is no notion of two worlds at once.
|
||||
*
|
||||
* The startup order that actually works:
|
||||
*
|
||||
* 1. fill in `game.name`, `game.version`, and `game.uri` -- akgl_game_init
|
||||
* refuses to run without them;
|
||||
* 2. akgl_game_init();
|
||||
* 3. akgl_registry_load_properties() or akgl_set_property(), to configure
|
||||
* screen size, physics constants and so on;
|
||||
* 4. akgl_render_init2d(renderer) and akgl_physics_factory(physics, ...), both
|
||||
* of which read that configuration;
|
||||
* 5. load assets, then loop on akgl_game_update().
|
||||
*
|
||||
* @warning None of this is thread-safe beyond the `game.state` mutex, and that
|
||||
* mutex protects the state flags, not the pools or the registries.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_GAME_H_
|
||||
@@ -12,62 +36,75 @@
|
||||
#include "tilemap.h"
|
||||
#include "renderer.h"
|
||||
#include "physics.h"
|
||||
// AKGL_VERSION used to be defined here by hand, which is how akgl.pc came to
|
||||
// ship an empty Version field: nothing tied the two together.
|
||||
#include <akgl/version.h>
|
||||
|
||||
#define AKGL_VERSION "0.1.0"
|
||||
|
||||
/** @brief Slot in ::akgl_tracks reserved for background music. Note that slot 0 is unused. */
|
||||
#define AKGL_GAME_AUDIO_TRACK_BGM 1
|
||||
/** @brief Size of the ::akgl_tracks table. Every simultaneous sound needs its own slot. */
|
||||
#define AKGL_GAME_AUDIO_MAX_TRACKS 64
|
||||
|
||||
/** @brief Nanoseconds in one second. The unit `SDL_GetTicksNS` reports in. */
|
||||
#define AKGL_TIME_ONESEC_NS 1000000000
|
||||
/**
|
||||
* @brief Misnamed: this is nanoseconds per **millisecond**, not milliseconds per second.
|
||||
*
|
||||
* One second in milliseconds is 1000. 1000000 is one millisecond in
|
||||
* nanoseconds, which is how sprite and character load actually use it -- as a
|
||||
* milliseconds-to-nanoseconds scale factor -- and is not how
|
||||
* akgl_game_state_lock uses it. TODO.md item 6 proposes renaming it
|
||||
* `AKGL_TIME_ONEMS_NS`.
|
||||
*/
|
||||
#define AKGL_TIME_ONESEC_MS 1000000
|
||||
|
||||
/* ==================== GAME STATE VARIABLES =================== */
|
||||
|
||||
/** @brief Describes a renderable frame. */
|
||||
/** @brief Describes a renderable frame. Declared but not used by anything in the library. */
|
||||
typedef struct {
|
||||
float32_t w;
|
||||
float32_t h;
|
||||
SDL_Texture *texture;
|
||||
float32_t w; /**< Width in pixels. */
|
||||
float32_t h; /**< Height in pixels. */
|
||||
SDL_Texture *texture; /**< The frame's texture. */
|
||||
} akgl_Frame;
|
||||
|
||||
/** @brief Stores application-defined game-state flags. */
|
||||
typedef struct {
|
||||
int32_t flags;
|
||||
int32_t flags; /**< Meaning is entirely the application's; the library never reads it. Guard changes with akgl_game_state_lock. */
|
||||
} akgl_GameState;
|
||||
|
||||
/** @brief Stores game metadata, timing, synchronization, and FPS accounting. */
|
||||
typedef struct {
|
||||
char libversion[32];
|
||||
char version[32];
|
||||
char name[256];
|
||||
char uri[256];
|
||||
akgl_GameState state;
|
||||
SDL_Mutex *statelock;
|
||||
int16_t fps;
|
||||
SDL_Time gameStartTime;
|
||||
SDL_Time lastIterTime;
|
||||
SDL_Time lastFPSTime;
|
||||
int16_t framesSinceUpdate;
|
||||
void (*lowfpsfunc)(void);
|
||||
char libversion[32]; /**< libakgl's version, stamped by akgl_game_init. Compared on load to refuse a save from another build. */
|
||||
char version[32]; /**< The *application's* version. Caller-supplied, required, and must be a semver string. */
|
||||
char name[256]; /**< Application name. Caller-supplied and required; also becomes SDL's app metadata. */
|
||||
char uri[256]; /**< Application URI, e.g. a reverse-DNS identifier. Caller-supplied, required, and used as the window title. */
|
||||
akgl_GameState state; /**< The application's own state flags. */
|
||||
SDL_Mutex *statelock; /**< Guards `state`. Created by akgl_game_init. */
|
||||
int16_t fps; /**< Frames drawn during the last completed second. Recomputed once per second, not per frame. */
|
||||
SDL_Time gameStartTime; /**< `SDL_GetTicksNS()` at akgl_game_init. */
|
||||
SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_updateFPS call. */
|
||||
SDL_Time lastFPSTime; /**< When `fps` was last recomputed. */
|
||||
int16_t framesSinceUpdate; /**< Frames counted so far in the current second. */
|
||||
void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. Defaults to akgl_game_lowfps; replace it to do something more useful than log. */
|
||||
} akgl_Game;
|
||||
|
||||
/** @brief SDL window used by the active renderer. */
|
||||
/** @brief The SDL window, created by akgl_render_init2d. `NULL` until then. */
|
||||
extern SDL_Window *window;
|
||||
/** @brief Loaded background-music resource. */
|
||||
/** @brief The background music, loaded by akgl_load_start_bgm. `NULL` until then. */
|
||||
extern MIX_Audio *bgm;
|
||||
/** @brief Mixer used for game audio playback. */
|
||||
/** @brief The mixer device, created by akgl_game_init. Everything audio goes through it. */
|
||||
extern MIX_Mixer *akgl_mixer;
|
||||
/** @brief Process-wide audio-track table. */
|
||||
/** @brief Playback tracks by slot. #AKGL_GAME_AUDIO_TRACK_BGM is the music track; the rest are the application's to assign. */
|
||||
extern MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
||||
/** @brief Storage for the default camera. */
|
||||
/** @brief Storage behind the default `camera`. Point `camera` elsewhere rather than reaching for this. */
|
||||
extern SDL_FRect _akgl_camera;
|
||||
/** @brief Process-wide game metadata and timing state. */
|
||||
/** @brief The one game object: metadata, timing, and FPS accounting. */
|
||||
extern akgl_Game game;
|
||||
/** @brief Storage for the default renderer. */
|
||||
/** @brief Storage behind the default `renderer`. */
|
||||
extern akgl_RenderBackend _akgl_renderer;
|
||||
/** @brief Storage for the default physics backend. */
|
||||
/** @brief Storage behind the default `physics`. */
|
||||
extern akgl_PhysicsBackend _akgl_physics;
|
||||
/** @brief Storage for the default tilemap. */
|
||||
/** @brief Storage behind the default `gamemap`. */
|
||||
extern akgl_Tilemap _akgl_gamemap;
|
||||
|
||||
/** @brief Currently active tilemap. */
|
||||
@@ -79,66 +116,184 @@ extern akgl_PhysicsBackend *physics;
|
||||
/** @brief Currently active camera. */
|
||||
extern SDL_FRect *camera;
|
||||
|
||||
/**
|
||||
* @brief True when every bit of `y` is set in `x`. Not "any of them" -- all of them.
|
||||
* @warning Unparenthesized: it expands to `(x & y) == y`, so `!AKGL_BITMASK_HAS(a, b)`
|
||||
* parses as `!(a & b) == b`. Use #AKGL_BITMASK_HASNOT rather than
|
||||
* negating this. TODO.md item 21.
|
||||
*/
|
||||
#define AKGL_BITMASK_HAS(x, y) (x & y) == y
|
||||
/** @brief True when at least one bit of `y` is missing from `x`. Same parenthesization caveat as #AKGL_BITMASK_HAS. */
|
||||
#define AKGL_BITMASK_HASNOT(x, y) (x & y) != y
|
||||
/** @brief Set every bit of `y` in `x`. Modifies `x`. */
|
||||
#define AKGL_BITMASK_ADD(x, y) x |= y
|
||||
/** @brief Clear every bit of `y` in `x`. Modifies `x`. */
|
||||
#define AKGL_BITMASK_DEL(x, y) x &= ~(y)
|
||||
/** @brief Clear every bit of `x`. Carries its own trailing semicolon, so do not add another. */
|
||||
#define AKGL_BITMASK_CLEAR(x) x = 0;
|
||||
|
||||
/**
|
||||
* @brief Game init.
|
||||
* @brief Bring the whole library up: error codes, pools, registries, SDL, audio, fonts, gamepads.
|
||||
*
|
||||
* In order: claim the libakgl status band (so every later error has a name),
|
||||
* stamp the library version, start the frame clock, create the state mutex,
|
||||
* check that the caller filled in the three required `game` fields, zero the
|
||||
* pools, create the registries, hand SDL the app metadata, clear the control
|
||||
* maps, `SDL_Init` video/gamepad/audio, load the bundled controller database,
|
||||
* open any attached gamepads, start SDL_mixer and SDL_ttf, and finally point
|
||||
* `renderer`, `physics`, `camera`, and `gamemap` at their default storage.
|
||||
*
|
||||
* What it does *not* do: create the window, choose a physics backend, or load
|
||||
* any configuration. Those read properties, so they come after the caller has
|
||||
* set them. See the sequence at the top of this file.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If `game.name`, `game.version`, or `game.uri` is
|
||||
* empty. All three are required and there are no defaults -- the
|
||||
* window title, SDL's app metadata, and the savegame compatibility
|
||||
* check are all built from them.
|
||||
* @throws AKGL_ERR_SDL If the state mutex cannot be created, if `SDL_Init`
|
||||
* fails, if a controller-database entry is rejected, if SDL_mixer
|
||||
* cannot start or open the default playback device, or if SDL_ttf
|
||||
* cannot start. Each message carries `SDL_GetError()`.
|
||||
* @throws AKERR_STATUS_RANGE_OVERLAP If another component already owns part of
|
||||
* the libakgl status band. See akgl_error_init.
|
||||
* @throws AKERR_* Whatever the heap, registry, and controller initializers raise.
|
||||
*
|
||||
* @note It takes the state lock on the way in and releases it on the way out, so
|
||||
* a failure part-way through leaves the mutex held.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init();
|
||||
/**
|
||||
* @brief Game init screen.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @brief Declared but never defined. Do not call.
|
||||
*
|
||||
* There is no definition anywhere in the library, so a translation unit that
|
||||
* calls this compiles and then fails to link. Screen setup is akgl_render_init2d.
|
||||
* Tracked in TODO.md under header/implementation surface drift.
|
||||
*
|
||||
* @return Nothing; it cannot be called.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init_screen();
|
||||
/**
|
||||
* @brief Game updatefps.
|
||||
* @brief Count this frame, and recompute the frame rate once a second has passed.
|
||||
*
|
||||
* Called at the top of akgl_game_update, so a caller running its own loop needs
|
||||
* to call it itself. `game.fps` is only refreshed when a full second has
|
||||
* elapsed, so it is a completed-second average rather than an instantaneous
|
||||
* figure -- and it reads 0 for the first second of the process, which is under
|
||||
* the low-FPS threshold and so fires `lowfpsfunc` on every frame until the first
|
||||
* second is up.
|
||||
*/
|
||||
void akgl_game_updateFPS();
|
||||
/**
|
||||
* @brief Game save.
|
||||
* @param fpath Path to the input or output file.
|
||||
* @brief Write the game state and the name-to-pointer tables to a save file.
|
||||
*
|
||||
* Writes the `akgl_Game` struct verbatim, then four name tables -- actors,
|
||||
* sprites, spritesheets, characters -- each mapping a registered name to the
|
||||
* address the object had at save time, and each terminated by a zeroed name and
|
||||
* a zeroed pointer. The tables are what let akgl_game_load reconnect pointers
|
||||
* between objects that will be at different addresses next run.
|
||||
*
|
||||
* @param fpath Path to write. Required. Opened with `"wb"`; an existing file is
|
||||
* truncated.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p fpath is `NULL`.
|
||||
* @throws ENOENT, EACCES Or whatever else `fopen(3)` reports, with the path in
|
||||
* the message.
|
||||
* @throws AKERR_IO On a stream error or a short write.
|
||||
*
|
||||
* @note This is a partial implementation: the name tables are written but the
|
||||
* objects themselves are not, so the file is not yet enough to restore a
|
||||
* session. See also TODO.md, "Known and still open" item 7 -- the writer
|
||||
* and the reader disagree on the name-field widths, so a save with any
|
||||
* registered spritesheet cannot be read back.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath);
|
||||
/**
|
||||
* @brief Game load.
|
||||
* @param fpath Path to the input or output file.
|
||||
* @brief Read a save file back, refusing one that does not match this build.
|
||||
*
|
||||
* Reads the saved `akgl_Game`, then rejects the file unless the library version,
|
||||
* the game version, the game name, and the game URI all match the running
|
||||
* program -- versions by exact semver equality, name and URI by string compare.
|
||||
* Only then does it copy the saved state over `game` and rebuild the four
|
||||
* old-address-to-current-object maps.
|
||||
*
|
||||
* @param fpath Path to read. Required. Opened with `"rb"`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p fpath is `NULL`.
|
||||
* @throws ENOENT, EACCES Or whatever else `fopen(3)` reports.
|
||||
* @throws AKERR_EOF If a read runs off the end of the file -- which is what a
|
||||
* truncated or corrupt name table looks like.
|
||||
* @throws AKERR_IO On a stream error.
|
||||
* @throws AKERR_VALUE If either version string in the save file, or either in
|
||||
* the running game, is not valid semver.
|
||||
* @throws AKERR_API If the save file is from a different library version, game
|
||||
* version, game name, or game URI.
|
||||
*
|
||||
* @note Like akgl_game_save, this is partial: it rebuilds the pointer maps but
|
||||
* does not yet read back any objects. The four `SDL_CreateProperties`
|
||||
* sets it builds are never destroyed, so each call leaks them.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath);
|
||||
/**
|
||||
* @brief Game lowfps.
|
||||
* @brief The default `game.lowfpsfunc`: log the current frame rate.
|
||||
*
|
||||
* Called from akgl_game_updateFPS on every frame where `game.fps` is under 30.
|
||||
* It is a placeholder -- the point of the hook is that a game can replace it
|
||||
* with something that actually sheds work.
|
||||
*/
|
||||
void akgl_game_lowfps(void);
|
||||
/**
|
||||
* @brief Game state lock.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
* @brief Take the game-state mutex, retrying rather than blocking.
|
||||
*
|
||||
* Polls with `SDL_TryLockMutex` on a 100 ms cadence instead of blocking
|
||||
* outright, so a deadlock reports an error rather than hanging the process.
|
||||
*
|
||||
* @return `NULL` once the lock is held, otherwise an error context owned by the
|
||||
* caller.
|
||||
* @throws AKGL_ERR_SDL If the budget runs out with the lock still held
|
||||
* elsewhere. The message carries `SDL_GetError()`, which after a failed
|
||||
* `SDL_TryLockMutex` is usually stale or empty -- the status is the
|
||||
* signal, not the text.
|
||||
*
|
||||
* @note The budget is meant to be one second but is not: the loop counts against
|
||||
* #AKGL_TIME_ONESEC_MS, which is nanoseconds-per-millisecond (1000000)
|
||||
* rather than milliseconds-per-second, so it retries 10,000 times at 100 ms
|
||||
* and gives up after roughly 16 minutes. TODO.md item 6.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_lock(void);
|
||||
/**
|
||||
* @brief Game state unlock.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @brief Release the game-state mutex.
|
||||
*
|
||||
* @return `NULL`. `SDL_UnlockMutex` reports nothing, so there is no failure path
|
||||
* -- including for the case that matters, unlocking a mutex this thread
|
||||
* does not hold, which is undefined behaviour in SDL rather than an
|
||||
* error here.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_state_unlock(void);
|
||||
/**
|
||||
* @brief Game update.
|
||||
* @param opflags Optional iterator operation flags; `NULL` selects defaults.
|
||||
* @brief One frame: update every actor layer by layer, step the physics, draw the world.
|
||||
*
|
||||
* Takes the state lock, counts the frame, then walks layers 0 through
|
||||
* #AKGL_TILEMAP_MAX_LAYERS calling each live actor's `updatefunc`, optionally
|
||||
* rescaling it to the tilemap first. Then it steps `physics` and draws through
|
||||
* `renderer`, and releases the lock.
|
||||
*
|
||||
* @param opflags Iterator flags. Optional -- `NULL` selects a default set that
|
||||
* sweeps one layer at a time, in which case `layerid` is advanced
|
||||
* by the loop. Only #AKGL_ITERATOR_OP_TILEMAPSCALE is read here;
|
||||
* with it clear every actor's `scale` is forced to 1.0. Note that
|
||||
* the flags are *not* forwarded to the physics or render calls,
|
||||
* both of which are passed `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @throws AKGL_ERR_SDL If the state lock cannot be taken.
|
||||
* @throws AKERR_* Whatever an actor's `updatefunc`, akgl_tilemap_scale_actor,
|
||||
* the physics backend, or the renderer raises.
|
||||
*
|
||||
* @warning Every failure path returns with the state lock still held, and a live
|
||||
* actor's `updatefunc` is called without a `NULL` check -- a hand-built
|
||||
* actor that never went through akgl_actor_initialize crashes here.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags);
|
||||
|
||||
|
||||
#endif //_AKGL_GAME_H_
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
/**
|
||||
* @file heap.h
|
||||
* @brief Declares the public heap API.
|
||||
* @brief The object pools. This library does not call `malloc`, it claims slots.
|
||||
*
|
||||
* Every runtime object -- actors, sprites, spritesheets, characters, strings --
|
||||
* comes out of a fixed, statically allocated array declared here. A "heap layer"
|
||||
* is one such array plus its `next`/`release` pair. Allocation is a linear scan
|
||||
* for a slot whose `refcount` is 0; release decrements, and the slot is zeroed
|
||||
* and unregistered when the count reaches 0.
|
||||
*
|
||||
* The consequence to design around is that exhaustion is a *normal* error, not
|
||||
* an out-of-memory catastrophe: AKGL_ERR_HEAP means the pool is full, and the
|
||||
* fix is usually a missing release rather than a bigger pool. Every ceiling
|
||||
* below is overridable at compile time, so a game that needs 256 actors defines
|
||||
* `AKGL_MAX_HEAP_ACTOR` before including this -- but the arrays are sized at
|
||||
* compile time, so the library and everything linking it must agree.
|
||||
*
|
||||
* If you need a new kind of runtime object, add a layer here. Do not reach for
|
||||
* the allocator.
|
||||
*
|
||||
* @warning The acquire functions are asymmetric: akgl_heap_next_string takes the
|
||||
* reference for you, and the other four do not -- their caller is
|
||||
* expected to take it, which in practice the `*_initialize` function
|
||||
* does. Until one is taken the slot is still free and the next
|
||||
* allocation hands out the same pointer. TODO.md, "Known and still
|
||||
* open" item 8.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_HEAP_H_
|
||||
@@ -28,102 +51,169 @@
|
||||
#define AKGL_MAX_HEAP_STRING 256
|
||||
#endif
|
||||
|
||||
/** @brief Fixed-capacity actor object pool. */
|
||||
/** @brief The actor pool. Public so the render and physics sweeps can walk it directly instead of going through the registry. */
|
||||
extern akgl_Actor HEAP_ACTOR[AKGL_MAX_HEAP_ACTOR];
|
||||
/** @brief Fixed-capacity sprite object pool. */
|
||||
/** @brief The sprite pool. 16 per actor, on the assumption of one sprite per state combination. */
|
||||
extern akgl_Sprite HEAP_SPRITE[AKGL_MAX_HEAP_SPRITE];
|
||||
/** @brief Fixed-capacity spritesheet object pool. */
|
||||
/** @brief The spritesheet pool. Sized like the sprite pool, though sharing means far fewer are used in practice. */
|
||||
extern akgl_SpriteSheet HEAP_SPRITESHEET[AKGL_MAX_HEAP_SPRITESHEET];
|
||||
/** @brief Fixed-capacity character object pool. */
|
||||
/** @brief The character pool. */
|
||||
extern akgl_Character HEAP_CHARACTER[AKGL_MAX_HEAP_CHARACTER];
|
||||
/** @brief Fixed-capacity static-string object pool. */
|
||||
/** @brief The string pool. Every entry is PATH_MAX bytes, so this is the largest of the five by a wide margin. */
|
||||
extern akgl_String HEAP_STRING[AKGL_MAX_HEAP_STRING];
|
||||
|
||||
/**
|
||||
* @brief Heap init.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_BEHAVIOR When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_LOGICINTERRUPT When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_REGISTRY When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
* @brief Zero every pool, marking every slot free.
|
||||
*
|
||||
* Call once at startup, before anything allocates. Calling it again is a reset,
|
||||
* not a refresh: it does not release textures, clear registries, or consult
|
||||
* reference counts, so every live object becomes a dangling pointer and every
|
||||
* registry entry points at a zeroed slot. akgl_game_init calls it for you.
|
||||
*
|
||||
* @return `NULL`. There is no failure path today -- it is a series of `memset`s
|
||||
* -- but check it anyway; the signature exists so a layer that needs
|
||||
* real setup has somewhere to report from.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init();
|
||||
/**
|
||||
* @brief Heap init actor.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @brief Zero the actor pool only.
|
||||
*
|
||||
* Split out from akgl_heap_init so a caller can reset the actors between levels
|
||||
* while keeping the loaded sprites, sheets, and characters -- those are the
|
||||
* expensive ones, since they own textures. Pair it with
|
||||
* akgl_registry_init_actor, which clears the matching registry.
|
||||
*
|
||||
* @return `NULL`. No failure path today; see akgl_heap_init.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor();
|
||||
/**
|
||||
* @brief Heap next actor.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Claim a free actor slot.
|
||||
* @param dest Receives a pointer to the free slot. Required, and **not**
|
||||
* checked -- a `NULL` here is a crash, not an error context. The
|
||||
* slot is *not* zeroed and its `refcount` is not incremented; it
|
||||
* stays free until akgl_actor_initialize takes the reference.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_ACTOR slots is in use.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_actor(akgl_Actor **dest);
|
||||
/**
|
||||
* @brief Heap next sprite.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Claim a free sprite slot.
|
||||
* @param dest Receives a pointer to the free slot. Required and unchecked; not
|
||||
* zeroed, and no reference taken. See akgl_heap_next_actor.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITE slots is in use.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_sprite(akgl_Sprite **dest);
|
||||
/**
|
||||
* @brief Heap next spritesheet.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Claim a free spritesheet slot.
|
||||
* @param dest Receives a pointer to the free slot. Required and unchecked; not
|
||||
* zeroed, and no reference taken. See akgl_heap_next_actor.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITESHEET slots is
|
||||
* in use.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest);
|
||||
/**
|
||||
* @brief Heap next character.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Claim a free character slot.
|
||||
* @param dest Receives a pointer to the free slot. Required and unchecked; not
|
||||
* zeroed, and no reference taken. See akgl_heap_next_actor.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_CHARACTER slots is in
|
||||
* use.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_character(akgl_Character **dest);
|
||||
/**
|
||||
* @brief Heap next string.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Claim a free string slot, and take the reference on it.
|
||||
*
|
||||
* The odd one out: this *does* increment `refcount`, so the slot is yours the
|
||||
* moment it returns and stays yours until you call akgl_heap_release_string.
|
||||
* That is what makes the scratch-buffer idiom -- claim, use, release in
|
||||
* `CLEANUP` -- safe. The contents are whatever the last holder left; call
|
||||
* akgl_string_initialize if you need it clean.
|
||||
*
|
||||
* @param dest Receives a pointer to the claimed slot. Required and **not**
|
||||
* checked -- a `NULL` here is a crash, not an error context.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_HEAP When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_STRING slots is in
|
||||
* use. In practice this means a missing release somewhere, not a pool
|
||||
* that is genuinely too small.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_string(akgl_String **dest);
|
||||
|
||||
/**
|
||||
* @brief Heap release actor.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @brief Drop a reference to an actor, tearing it down when the last one goes.
|
||||
*
|
||||
* At zero it releases every child recursively, clears the actor's entry from
|
||||
* #AKGL_REGISTRY_ACTOR, and zeroes the slot.
|
||||
*
|
||||
* @param ptr The actor to release. Required. A slot whose `refcount` is already
|
||||
* 0 is re-torn-down rather than rejected, which is harmless on a
|
||||
* zeroed slot and destructive on a live one that was never
|
||||
* registered.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*
|
||||
* @warning Children are released unconditionally, and the recursion has no cycle
|
||||
* check: an actor reachable from its own child list recurses until the
|
||||
* stack runs out.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_actor(akgl_Actor *ptr);
|
||||
/**
|
||||
* @brief Heap release sprite.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @brief Drop a reference to a sprite, tearing it down when the last one goes.
|
||||
*
|
||||
* At zero it clears the sprite's entry from #AKGL_REGISTRY_SPRITE and zeroes the
|
||||
* slot. The spritesheet it pointed at is *not* released -- the sprite only
|
||||
* borrowed it.
|
||||
*
|
||||
* @param ptr The sprite to release. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_sprite(akgl_Sprite *ptr);
|
||||
/**
|
||||
* @brief Heap release spritesheet.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @brief Drop a reference to a spritesheet, destroying its texture when the last one goes.
|
||||
*
|
||||
* The only release that frees a resource outside the pool: at zero it clears the
|
||||
* entry from #AKGL_REGISTRY_SPRITESHEET, destroys the `SDL_Texture`, and zeroes
|
||||
* the slot. Any sprite still pointing at the sheet is left with a dangling
|
||||
* pointer, since nothing takes a reference on a sheet on a sprite's behalf.
|
||||
*
|
||||
* @param ptr The spritesheet to release. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*
|
||||
* @note Destroying a texture is a main-thread operation in SDL, so this must be
|
||||
* called from the thread that owns the renderer.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr);
|
||||
/**
|
||||
* @brief Heap release character.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @brief Drop a reference to a character, tearing it down when the last one goes.
|
||||
*
|
||||
* At zero it clears the entry from #AKGL_REGISTRY_CHARACTER and zeroes the slot.
|
||||
*
|
||||
* @param ptr The character to release. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*
|
||||
* @note It does not walk the state-to-sprite map, so the references the
|
||||
* character took in akgl_character_sprite_add are never given back, and
|
||||
* the SDL property set holding the map is leaked. Release the sprites
|
||||
* first with akgl_character_state_sprites_iterate and
|
||||
* #AKGL_ITERATOR_OP_RELEASE if you need them back.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_character(akgl_Character *ptr);
|
||||
/**
|
||||
* @brief Heap release string.
|
||||
* @param ptr Heap object whose reference count is decremented.
|
||||
* @brief Drop a reference to a pooled string, zeroing it when the last one goes.
|
||||
*
|
||||
* Strings are not registered anywhere, so this is just the reference count and a
|
||||
* wipe. It is safe to call on a string a function may or may not have claimed,
|
||||
* which is why the `CLEANUP` blocks in this library call it unconditionally.
|
||||
*
|
||||
* @param ptr The string to release. Required -- unlike the pattern elsewhere, a
|
||||
* `NULL` here is an error rather than a no-op, so `CLEANUP` blocks
|
||||
* wrap it in `IGNORE()`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_string(akgl_String *ptr);
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
/**
|
||||
* @file iterator.h
|
||||
* @brief Declares the public iterator API.
|
||||
* @brief The work order handed to a registry sweep.
|
||||
*
|
||||
* There is no iterator object with a `next()` on it. Traversal is SDL's --
|
||||
* `SDL_EnumerateProperties` over a registry -- and this struct is the `userdata`
|
||||
* carried into each callback, telling it *which* entries to touch and *what* to
|
||||
* do to each one. akgl_registry_iterate_actor and
|
||||
* akgl_registry_iterate_character are the callbacks that read it;
|
||||
* akgl_game_update, akgl_physics_simulate, and akgl_render_2d_draw_world are the
|
||||
* entry points that take one.
|
||||
*
|
||||
* The operations are independent bits, not an enum: a single sweep can update,
|
||||
* scale, and render, and they run in that fixed order regardless of how the bits
|
||||
* were set. Passing `NULL` where an `akgl_Iterator *` is expected is not an
|
||||
* error at the top-level entry points -- each substitutes its own default set --
|
||||
* but it *is* an error once inside a callback.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_ITERATOR_H_
|
||||
@@ -8,15 +22,15 @@
|
||||
|
||||
/** @brief Selects operations and an optional layer for actor traversal. */
|
||||
typedef struct {
|
||||
uint32_t flags;
|
||||
uint8_t layerid;
|
||||
uint32_t flags; /**< Bitwise OR of the `AKGL_ITERATOR_OP_*` values below. */
|
||||
uint8_t layerid; /**< Layer to restrict the sweep to. Read only when #AKGL_ITERATOR_OP_LAYERMASK is set. */
|
||||
} akgl_Iterator;
|
||||
|
||||
#define AKGL_ITERATOR_OP_UPDATE 1 // 1
|
||||
#define AKGL_ITERATOR_OP_RENDER 1 << 1 // 2
|
||||
#define AKGL_ITERATOR_OP_RELEASE 1 << 2 // 4
|
||||
#define AKGL_ITERATOR_OP_LAYERMASK 1 << 3 // 8
|
||||
#define AKGL_ITERATOR_OP_TILEMAPSCALE 1 << 4 // 16
|
||||
#define AKGL_ITERATOR_OP_UPDATE 1 // 1 Call the actor's updatefunc
|
||||
#define AKGL_ITERATOR_OP_RENDER 1 << 1 // 2 Call the actor's renderfunc
|
||||
#define AKGL_ITERATOR_OP_RELEASE 1 << 2 // 4 Release the object back to its heap layer
|
||||
#define AKGL_ITERATOR_OP_LAYERMASK 1 << 3 // 8 Skip anything whose layer != layerid
|
||||
#define AKGL_ITERATOR_OP_TILEMAPSCALE 1 << 4 // 16 Scale actors to the tilemap; otherwise force scale 1.0
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_5 1 << 5 // 32
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_6 1 << 6 // 64
|
||||
#define AKGL_ITERATOR_OP_UNDEFINED_7 1 << 7 // 128
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
/**
|
||||
* @file json_helpers.h
|
||||
* @brief Declares the public json helpers API.
|
||||
* @brief Typed jansson accessors that report "missing" and "wrong type" as errors.
|
||||
*
|
||||
* Every asset in this library is described by a JSON document, and jansson's own
|
||||
* accessors answer "missing key", "wrong type", and "index past the end" all
|
||||
* with the same `NULL`. These wrappers split those apart -- AKERR_KEY,
|
||||
* AKERR_TYPE, AKERR_OUTOFBOUNDS -- so a malformed asset file produces a message
|
||||
* naming the key and what was wrong with it, instead of a `NULL` dereference
|
||||
* three frames later.
|
||||
*
|
||||
* Conventions that run through the whole set, so they need not be repeated per
|
||||
* function:
|
||||
*
|
||||
* - **Absence is an error here.** Unlike the search functions in libakstdlib,
|
||||
* these treat a missing key as AKERR_KEY. An *optional* key is expressed by
|
||||
* passing the resulting error to akgl_get_json_with_default() rather than by
|
||||
* the accessor staying quiet.
|
||||
* - **The result comes back through @p dest**, because the return value is the
|
||||
* error context.
|
||||
* - **`json_t *` results are borrowed, not owned.** Objects and arrays are
|
||||
* returned as pointers into the document; they are valid until the document is
|
||||
* freed and must not be `json_decref`'d.
|
||||
* - **@p dest is not `NULL`-checked** except where noted, so a `NULL` there is a
|
||||
* crash rather than an error context.
|
||||
* - **The document itself is never modified.**
|
||||
*/
|
||||
|
||||
#ifndef _JSON_HELPERS_H_
|
||||
@@ -10,126 +33,203 @@
|
||||
#include "staticstring.h"
|
||||
|
||||
/**
|
||||
* @brief Get json object value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a nested object out of a JSON object.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up. A `NULL` key is reported as a missing
|
||||
* key rather than as a `NULL` pointer.
|
||||
* @param dest Receives a borrowed pointer to the nested object; not written on
|
||||
* any failure path. Not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent. The message names it.
|
||||
* @throws AKERR_TYPE If @p key is present but is not an object.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest);
|
||||
/**
|
||||
* @brief Get json boolean value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a boolean out of a JSON object.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives the value; not written on any failure path. Not checked
|
||||
* for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not `true` or `false`. A `0`
|
||||
* or `1` is a number in JSON, and is refused here.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest);
|
||||
/**
|
||||
* @brief Get json integer value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read an integer out of a JSON object.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives the value, narrowed to `int`. Not written on any failure
|
||||
* path, and not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not an integer. This is strict:
|
||||
* `3.0` is a real in JSON and is refused, not truncated. Use
|
||||
* akgl_get_json_number_value() where either spelling should be accepted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_integer_value(json_t *obj, char *key, int *dest);
|
||||
/**
|
||||
* @brief Get json number value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a number out of a JSON object as a `float`.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives the value, narrowed to `float`. Not written on any
|
||||
* failure path, and not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not a number. Integers and
|
||||
* reals are both accepted, so `1` and `1.0` behave alike.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_number_value(json_t *obj, char *key, float *dest);
|
||||
/**
|
||||
* @brief Get json double value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a number out of a JSON object as a `double`.
|
||||
*
|
||||
* The full-precision form of akgl_get_json_number_value(), for the physics
|
||||
* constants, which are `double`.
|
||||
*
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives the value. Not written on any failure path, and not
|
||||
* checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not a number.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest);
|
||||
/**
|
||||
* @brief Get json string value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a string out of a JSON object into a pooled akgl_String.
|
||||
*
|
||||
* Unlike the other accessors this copies, because the caller wants a buffer it
|
||||
* can keep rather than a pointer into the document. `*dest` doubles as an input:
|
||||
* `NULL` means "claim one for me", non-`NULL` means "write into this one".
|
||||
*
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up. Required -- checked here, unlike the
|
||||
* other accessors in this file.
|
||||
* @param dest Address of the destination string. Required, and it must be
|
||||
* *initialized*: set `*dest` to `NULL` to have a pool string claimed
|
||||
* for you, or to a claimed string to write in place. An
|
||||
* indeterminate `*dest` is dereferenced. Either way the caller
|
||||
* releases it with akgl_heap_release_string().
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj, @p key, or @p dest is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not a string.
|
||||
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
|
||||
*
|
||||
* @note A value longer than #AKGL_MAX_STRING_LENGTH is truncated silently, and
|
||||
* is left without a terminator -- this is `strncpy`, not `strlcpy`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest);
|
||||
/**
|
||||
* @brief Get json array value.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read an array out of a JSON object.
|
||||
* @param obj The object to read from. Required.
|
||||
* @param key The member name to look up.
|
||||
* @param dest Receives a borrowed pointer to the array; not written on any
|
||||
* failure path. Not checked for `NULL`. Use `json_array_size()` on
|
||||
* it and the `akgl_get_json_array_index_*` family to walk it.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
* @throws AKERR_KEY If @p key is absent.
|
||||
* @throws AKERR_TYPE If @p key is present but is not an array.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest);
|
||||
/**
|
||||
* @brief Get json array index object.
|
||||
* @param array JSON array to read.
|
||||
* @param index Zero-based element index.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read one element of a JSON array as an object.
|
||||
* @param array The array to read from. Required.
|
||||
* @param index Zero-based element index. A negative index is reported the same
|
||||
* way as one past the end.
|
||||
* @param dest Receives a borrowed pointer to the element; not written on any
|
||||
* failure path. Not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p array is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array. The message
|
||||
* reports the index.
|
||||
* @throws AKERR_TYPE If the element is not an object.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest);
|
||||
/**
|
||||
* @brief Get json array index integer.
|
||||
* @param array JSON array to read.
|
||||
* @brief Read one element of a JSON array as an integer.
|
||||
* @param array The array to read from. Required.
|
||||
* @param index Zero-based element index.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param dest Receives the value, narrowed to `int`; not written on any failure
|
||||
* path. Not checked for `NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p array is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
|
||||
* @throws AKERR_TYPE If the element is not an integer. Strict, as in
|
||||
* akgl_get_json_integer_value().
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_integer(json_t *array, int index, int *dest);
|
||||
/**
|
||||
* @brief Get json array index string.
|
||||
* @param array JSON array to read.
|
||||
* @brief Read one element of a JSON array into a pooled akgl_String.
|
||||
* @param array The array to read from. Required.
|
||||
* @param index Zero-based element index.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param dest Address of the destination string, with the same claim-or-reuse
|
||||
* contract as akgl_get_json_string_value(): required, must be
|
||||
* initialized, and released by the caller.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p array or @p dest is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p index is outside the array.
|
||||
* @throws AKERR_TYPE If the element is not a string.
|
||||
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
|
||||
*
|
||||
* @note Truncates silently at #AKGL_MAX_STRING_LENGTH, as
|
||||
* akgl_get_json_string_value() does.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest);
|
||||
|
||||
/**
|
||||
* @brief Get json with default.
|
||||
* @param e Error context to inspect and optionally consume.
|
||||
* @param defval Fallback value copied when the supplied error is eligible.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param defsize Size in bytes of the fallback value.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_INDEX When the corresponding validation or operation fails.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @brief Turn a "key not found" error from one of the accessors above into a default value.
|
||||
*
|
||||
* This is how an optional key is spelled. Run the accessor, hand its error
|
||||
* context here along with the fallback, and a missing key becomes @p dest
|
||||
* holding @p defval and a `NULL` return; anything else propagates untouched:
|
||||
*
|
||||
* ```c
|
||||
* int width = 0;
|
||||
* int width_default = 32;
|
||||
* PASS(errctx,
|
||||
* akgl_get_json_with_default(
|
||||
* akgl_get_json_integer_value(json, "width", &width),
|
||||
* &width_default,
|
||||
* &width,
|
||||
* sizeof(int)
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* A `NULL` @p e -- the accessor succeeded -- is the ordinary case and returns at
|
||||
* once without touching @p dest. When the error *is* consumed it is also
|
||||
* released, so the caller must not release it again.
|
||||
*
|
||||
* @param e The error context to inspect, straight from an accessor.
|
||||
* `NULL` means "no error", which is not itself an error.
|
||||
* @param defval The fallback value to copy. Required when @p e is non-`NULL`.
|
||||
* @param dest Where to copy it. Required when @p e is non-`NULL`. Must be the
|
||||
* same destination the accessor was given, and at least
|
||||
* @p defsize bytes.
|
||||
* @param defsize Bytes to copy out of @p defval. Trusted, not derived -- it must
|
||||
* match the type both sides actually are, since this is a
|
||||
* `memcpy` through `void *` with no type information.
|
||||
* @return `NULL` when the error was consumed or there was none, otherwise an
|
||||
* error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p e is non-`NULL` and @p defval or @p dest is
|
||||
* `NULL`.
|
||||
* @throws AKERR_* Whatever @p e carried, if it is not one of the statuses this
|
||||
* defaults on -- an AKERR_TYPE from a key that exists but is the wrong
|
||||
* type propagates, which is right: that is a malformed document, not an
|
||||
* omitted setting.
|
||||
*
|
||||
* @note It defaults on AKERR_KEY and AKERR_INDEX, but *not* on
|
||||
* AKERR_OUTOFBOUNDS -- which is the status the
|
||||
* `akgl_get_json_array_index_*` family actually raises for a short array.
|
||||
* So this pairs with the object accessors and does not currently give an
|
||||
* array index a default.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *e, void *defval, void *dest, uint32_t defsize);
|
||||
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
/**
|
||||
* @file physics.h
|
||||
* @brief Declares the public physics API.
|
||||
* @brief The pluggable physics backend, and the two implementations that ship.
|
||||
*
|
||||
* Same shape as the renderer: a record of function pointers plus an initializer
|
||||
* that fills it in. `null` accepts every call and changes nothing, which is what
|
||||
* a menu screen or a test harness wants; `arcade` applies gravity, drag, thrust
|
||||
* and a speed cap. Selecting between them is akgl_physics_factory, driven by the
|
||||
* `physics.engine` configuration property -- not a branch in the simulation.
|
||||
*
|
||||
* The model an actor is simulated under, and the fields on akgl_Actor that carry
|
||||
* it:
|
||||
*
|
||||
* | Term | Fields | Comes from |
|
||||
* |-----------------------|--------------|-----------------------------------------------|
|
||||
* | thrust | `tx, ty, tz` | the actor's own acceleration while it is moving |
|
||||
* | environmental | `ex, ey, ez` | gravity, less drag |
|
||||
* | velocity | `vx, vy, vz` | thrust + environmental |
|
||||
* | max speed | `sx, sy, sz` | the character; caps thrust, not velocity |
|
||||
* | acceleration | `ax, ay, az` | the character |
|
||||
*
|
||||
* Each step: movement logic, then gravity, then drag, then velocity, then move.
|
||||
* Because the cap is applied to thrust rather than to velocity, gravity can
|
||||
* carry an actor faster than its stated top speed -- which is the point, for a
|
||||
* falling one.
|
||||
*/
|
||||
|
||||
#ifndef _PHYSICS_H_
|
||||
@@ -14,110 +36,202 @@
|
||||
|
||||
/** @brief Defines a pluggable physics backend and its environmental parameters. */
|
||||
typedef struct akgl_PhysicsBackend {
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*simulate)(struct akgl_PhysicsBackend *self, akgl_Iterator *opflags); /**< Step the whole world. Both backends point this at akgl_physics_simulate. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*gravity)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Apply environmental acceleration to one actor. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*collide)(struct akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2); /**< Resolve a collision between two actors. Not called by the simulation loop yet. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt); /**< Commit one actor's velocity to its position. */
|
||||
|
||||
double drag_x;
|
||||
double drag_y;
|
||||
double drag_z;
|
||||
double gravity_x;
|
||||
double gravity_y;
|
||||
double gravity_z;
|
||||
SDL_Time gravity_time;
|
||||
SDL_Time timer_gravity;
|
||||
double drag_x; /**< Fraction of environmental velocity shed per second along x. 0 disables. From `physics.drag.x`. */
|
||||
double drag_y; /**< Drag along y. From `physics.drag.y`. */
|
||||
double drag_z; /**< Drag along z. From `physics.drag.z`. */
|
||||
double gravity_x; /**< Acceleration along x, world units per second squared. Applied toward screen left. From `physics.gravity.x`. */
|
||||
double gravity_y; /**< Acceleration along y. Applied down the screen, since y grows downward. From `physics.gravity.y`. */
|
||||
double gravity_z; /**< Acceleration along z. Applied away from the camera. From `physics.gravity.z`. */
|
||||
SDL_Time gravity_time; /**< Timestamp of the previous step, in nanoseconds. The simulation's `dt` is measured from this. */
|
||||
SDL_Time timer_gravity; /**< Unused. Nothing in the library reads or writes it. */
|
||||
} akgl_PhysicsBackend;
|
||||
|
||||
/**
|
||||
* @brief Physics null gravity.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @brief Gravity for the null backend: accept the call and change nothing.
|
||||
* @param self The backend. Required -- the one thing this does check.
|
||||
* @param actor The actor that would have been accelerated. Ignored; `NULL` is
|
||||
* accepted.
|
||||
* @param dt Seconds since the previous step. Ignored.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Physics null collide.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param a1 First actor supplied to collision handling.
|
||||
* @param a2 Second actor supplied to collision handling.
|
||||
* @brief Collision for the null backend: accept the call and change nothing.
|
||||
*
|
||||
* Note that this *succeeds* where the arcade backend's collide refuses -- the
|
||||
* null backend's contract is "nothing collides", which is an answer, not a gap.
|
||||
*
|
||||
* @param self The backend. Required.
|
||||
* @param a1 First actor. Ignored; `NULL` is accepted.
|
||||
* @param a2 Second actor. Ignored; `NULL` is accepted.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||
/**
|
||||
* @brief Physics null move.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @brief Movement for the null backend: accept the call and change nothing.
|
||||
*
|
||||
* Actors under the null backend do not move on their own. Anything that sets an
|
||||
* actor's `x`/`y` directly still works -- this only declines to integrate
|
||||
* velocity.
|
||||
*
|
||||
* @param self The backend. Required.
|
||||
* @param actor The actor that would have moved. Ignored; `NULL` is accepted.
|
||||
* @param dt Seconds since the previous step. Ignored.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Physics init null.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @brief Install the null backend's methods.
|
||||
*
|
||||
* Reads no configuration and touches no gravity or drag fields, so whatever was
|
||||
* in them stays -- harmlessly, since none of the null methods look.
|
||||
*
|
||||
* @param self The backend to initialize. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *self);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Physics arcade gravity.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @brief Accelerate one actor's environmental velocity by gravity.
|
||||
*
|
||||
* Adds `gravity * dt` to the actor's `ex`/`ey`/`ez` on each axis whose gravity
|
||||
* is non-zero, with the sign chosen for screen space: x pulls left, y pulls
|
||||
* down, z pulls away from the camera. It touches only the environmental term, so
|
||||
* an actor's own thrust is unaffected and the speed cap does not apply -- which
|
||||
* is why a falling actor keeps accelerating past its character's top speed.
|
||||
*
|
||||
* @param self The backend supplying the gravity constants. Required.
|
||||
* @param actor The actor to accelerate. Required.
|
||||
* @param dt Seconds since the previous step. A `dt` of 0 is a no-op; a
|
||||
* negative one accelerates backwards.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self or @p actor is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Physics arcade collide.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param a1 First actor supplied to collision handling.
|
||||
* @param a2 Second actor supplied to collision handling.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @brief Collision for the arcade backend. Not implemented.
|
||||
*
|
||||
* Deliberately loud rather than silently permissive: unlike
|
||||
* akgl_physics_null_collide, which means "nothing collides", this means "nobody
|
||||
* has written this yet", and a caller that reaches it should find out.
|
||||
*
|
||||
* @param self The backend. Required, and checked before the refusal.
|
||||
* @param a1 First actor. Never examined.
|
||||
* @param a2 Second actor. Never examined.
|
||||
* @return Never `NULL`.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
* @throws AKERR_API Otherwise, always, with the message "Not implemented".
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2);
|
||||
/**
|
||||
* @brief Physics arcade move.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @param dt Elapsed simulation time in seconds.
|
||||
* @brief Commit an actor's velocity to its position.
|
||||
*
|
||||
* The last step of a simulation tick: `position += velocity * dt` on all three
|
||||
* axes. It does not clamp to the map, consult the tilemap, or test for
|
||||
* collisions -- an actor will walk straight through a wall and off the edge of
|
||||
* the world.
|
||||
*
|
||||
* @param self The backend. Required, though nothing on it is read.
|
||||
* @param actor The actor to move. Required. Its `vx`/`vy`/`vz` must already have
|
||||
* been computed; akgl_physics_simulate does that immediately
|
||||
* before calling this.
|
||||
* @param dt Seconds since the previous step.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self or @p actor is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt);
|
||||
/**
|
||||
* @brief Physics init arcade.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @brief Install the arcade backend's methods and read its constants from configuration.
|
||||
*
|
||||
* Reads `physics.gravity.x`, `.y`, `.z` and `physics.drag.x`, `.y`, `.z` from
|
||||
* the property registry, all defaulting to `"0.0"` -- so an arcade backend with
|
||||
* no configuration behaves like the null one until something is set.
|
||||
*
|
||||
* @param self The backend to initialize. Required. Its method pointers are
|
||||
* installed before the properties are read, so a failure part-way
|
||||
* leaves a usable backend with some constants still at their
|
||||
* previous values.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
* @throws AKERR_VALUE If one of the six properties is set to something that is
|
||||
* not a number.
|
||||
* @throws ERANGE If one of them does not fit a `double`.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @note With #AKGL_REGISTRY_PROPERTIES uninitialized every property reads back
|
||||
* as its default, so this silently configures zero gravity and zero drag
|
||||
* rather than reporting anything. See the warning in registry.h.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend *self);
|
||||
|
||||
/**
|
||||
* @brief Physics factory.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param type Expected JSON property type or backend type name.
|
||||
* @brief Select and initialize a physics backend by name.
|
||||
*
|
||||
* The dispatch point for the whole subsystem: `"null"` and `"arcade"` are the
|
||||
* two names, and akgl_game_init passes whatever the `physics.engine` property
|
||||
* holds. Adding a backend means adding a name here, not a branch in the
|
||||
* simulation.
|
||||
*
|
||||
* @param self The backend to initialize. Required.
|
||||
* @param type The backend name. Required. Matched on its leading characters, so
|
||||
* `"nullify"` selects `null` and `"arcadia"` selects `arcade`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self or @p type is `NULL`.
|
||||
* @throws AKERR_KEY If @p type matches neither name. The message quotes what was
|
||||
* asked for.
|
||||
* @throws AKERR_* Whatever the selected initializer raises.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type);
|
||||
|
||||
/**
|
||||
* @brief Physics simulate.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param opflags Optional iterator operation flags; `NULL` selects defaults.
|
||||
* @brief Step every live actor forward by the time elapsed since the previous call.
|
||||
*
|
||||
* Shared by both backends -- what differs is the `gravity` and `move` they point
|
||||
* at. For each live actor, in pool order:
|
||||
*
|
||||
* - a child actor is snapped to its parent's position plus its own `v` as an
|
||||
* offset, and skipped entirely -- children do not simulate;
|
||||
* - an actor with no character is skipped, since the speed and acceleration
|
||||
* constants live there;
|
||||
* - thrust accumulates along an axis the actor is moving on, then is clamped to
|
||||
* the character's maximum speed;
|
||||
* - the backend's `gravity` runs, drag is applied to the environmental term,
|
||||
* velocity becomes environmental plus thrust, and the backend's `move`
|
||||
* commits it.
|
||||
*
|
||||
* `dt` is measured from `self->gravity_time`, which is stamped at the end. The
|
||||
* first call after initialization therefore measures from 0 and produces an
|
||||
* enormous `dt` -- initialize `gravity_time` from `SDL_GetTicksNS()` before the
|
||||
* first step if that matters.
|
||||
*
|
||||
* @param self The backend. Required, along with its `move` pointer.
|
||||
* @param opflags Iterator flags. Optional -- `NULL` means "simulate everything".
|
||||
* Only #AKGL_ITERATOR_OP_LAYERMASK is honoured, restricting the
|
||||
* step to actors on `layerid`; the update, render, and release
|
||||
* bits are ignored here.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_LOGICINTERRUPT When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self or `self->move` is `NULL`.
|
||||
* @throws AKERR_* Whatever an actor's `movementlogicfunc`, or the backend's
|
||||
* `gravity` or `move`, raises. The first failure aborts the whole step,
|
||||
* leaving the actors already processed advanced and the rest not.
|
||||
*
|
||||
* @note An actor's `movementlogicfunc` can raise AKGL_ERR_LOGICINTERRUPT to say
|
||||
* "skip the rest of the simulation for me this tick". It is handled here
|
||||
* and does not propagate -- it is a control signal wearing an error's
|
||||
* clothes, not a failure. Only `movementlogicfunc` gets that treatment:
|
||||
* the `gravity` and `move` calls return straight out of the loop on any
|
||||
* status, LOGICINTERRUPT included, so a backend must not use it to opt an
|
||||
* actor out from there.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags);
|
||||
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
/**
|
||||
* @file registry.h
|
||||
* @brief Declares the public registry API.
|
||||
* @brief The name-to-object lookup tables, and the string configuration store.
|
||||
*
|
||||
* Nothing in this library is passed around by pointer where a name will do. An
|
||||
* actor names the character it instantiates, a character names the sprites it
|
||||
* draws, a sprite names the sheet it cuts frames from -- all resolved at load
|
||||
* time through these eight registries. That is what lets the whole asset graph
|
||||
* be described in JSON files that reference each other by name.
|
||||
*
|
||||
* They are SDL property sets, not akgl types, so a caller can enumerate one with
|
||||
* `SDL_EnumerateProperties` -- which is exactly what akgl_registry_iterate_actor
|
||||
* and akgl_character_state_sprites_iterate are for.
|
||||
*
|
||||
* Every id starts at 0, which SDL treats as "no such property set": reads return
|
||||
* the default and writes are silently dropped. So an uninitialized registry does
|
||||
* not fail loudly, it fails quietly, and akgl_registry_init has to run first.
|
||||
*
|
||||
* @warning akgl_registry_init does *not* initialize
|
||||
* #AKGL_REGISTRY_PROPERTIES -- akgl_registry_init_properties is a
|
||||
* separate call, made by akgl_game_init but by nothing else. A program
|
||||
* that builds its own startup path and skips it gets a silently
|
||||
* no-op akgl_set_property and an akgl_get_property that always hands
|
||||
* back the caller's default, which in turn means akgl_render_init2d
|
||||
* and akgl_physics_init_arcade quietly ignore their configuration.
|
||||
* TODO.md, "Known and still open" item 3.
|
||||
*/
|
||||
|
||||
#ifndef _REGISTRY_H_
|
||||
@@ -9,99 +32,186 @@
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
/** @brief Registry of actors keyed by name. */
|
||||
/** @brief Actor name -> `akgl_Actor *`. Written by akgl_actor_initialize, cleared when an actor's last reference goes. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR;
|
||||
/** @brief Registry of sprites keyed by name. */
|
||||
/** @brief Sprite name -> `akgl_Sprite *`. The `name` field from a sprite JSON. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITE;
|
||||
/** @brief Registry of spritesheets keyed by asset path. */
|
||||
/** @brief Resolved image path -> `akgl_SpriteSheet *`. Keyed by path so two sprites sharing an image share the texture. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET;
|
||||
/** @brief Registry of characters keyed by name. */
|
||||
/** @brief Character name -> `akgl_Character *`. What akgl_actor_set_character resolves against. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_CHARACTER;
|
||||
/** @brief Registry mapping actor-state names to bit values. */
|
||||
/** @brief Actor-state name -> its bit value, as a number. Lets character JSON say "AKGL_ACTOR_STATE_FACE_LEFT" instead of 2. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR_STATE_STRINGS;
|
||||
/** @brief Registry of fonts keyed by name. */
|
||||
/** @brief Font name -> `TTF_Font *`. Names are chosen by the caller of akgl_text_loadfont, not derived from the file. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_FONT;
|
||||
/** @brief Registry of music assets keyed by name. */
|
||||
/** @brief Music name -> audio handle. Created but not yet populated by anything in the library. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_MUSIC;
|
||||
/** @brief Registry of string configuration properties. */
|
||||
/** @brief Configuration key -> string value. Read through akgl_get_property; everything is a string, including numbers. */
|
||||
extern SDL_PropertiesID AKGL_REGISTRY_PROPERTIES;
|
||||
|
||||
/**
|
||||
* @brief Registry init.
|
||||
* @brief Create the seven asset registries, in dependency order.
|
||||
*
|
||||
* Spritesheet, sprite, character, actor, actor-state-strings, font, music. Call
|
||||
* it once at startup, before loading anything.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate any one of the property sets;
|
||||
* the message names which registry failed. The registries created before
|
||||
* the failure are left in place rather than torn down.
|
||||
*
|
||||
* @warning This does not create #AKGL_REGISTRY_PROPERTIES -- see the warning on
|
||||
* this file.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init();
|
||||
/**
|
||||
* @brief Registry init music.
|
||||
* @brief Create the music registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set, so calling it
|
||||
* twice leaks the first one along with everything registered in it.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_music();
|
||||
/**
|
||||
* @brief Registry init font.
|
||||
* @brief Create the font registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set. See
|
||||
* akgl_registry_init_music.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_font();
|
||||
/**
|
||||
* @brief Registry init actor.
|
||||
* @brief Create the actor registry, destroying any previous one first.
|
||||
*
|
||||
* The one initializer here that cleans up after itself, which is what makes it
|
||||
* safe to call between levels: it destroys the old property set before creating
|
||||
* the replacement, so the actors from the previous map are unregistered in one
|
||||
* step. Note that it destroys the *registry*, not the actors -- releasing those
|
||||
* is akgl_heap_release_actor's job.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set. The old one
|
||||
* has already been destroyed at that point, so the registry is left at 0.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor();
|
||||
/**
|
||||
* @brief Registry init sprite.
|
||||
* @brief Create the sprite registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set. See
|
||||
* akgl_registry_init_music.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_sprite();
|
||||
/**
|
||||
* @brief Registry init spritesheet.
|
||||
* @brief Create the spritesheet registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set. See
|
||||
* akgl_registry_init_music.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_spritesheet();
|
||||
/**
|
||||
* @brief Registry init character.
|
||||
* @brief Create the character registry.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set. See
|
||||
* akgl_registry_init_music.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_character();
|
||||
/**
|
||||
* @brief Registry init properties.
|
||||
* @brief Create the configuration-property registry.
|
||||
*
|
||||
* Deliberately separate from akgl_registry_init, because configuration has to be
|
||||
* in place before the subsystems that read it start up. akgl_game_init creates
|
||||
* it; filling it in -- with akgl_registry_load_properties or akgl_set_property --
|
||||
* is the caller's job, and has to happen before akgl_render_init2d or
|
||||
* akgl_physics_init_arcade run.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set.
|
||||
* @note Overwrites the existing id without destroying the old set, so every
|
||||
* property set before a second call is lost.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_properties();
|
||||
/**
|
||||
* @brief Registry load properties.
|
||||
* @param fname Path to the input or output file.
|
||||
* @brief Load a JSON configuration file into the property registry.
|
||||
*
|
||||
* Expects a document with a top-level `properties` object whose members are all
|
||||
* strings; each becomes one entry. Numbers are configured as strings here and
|
||||
* parsed by whoever reads them -- `game.screenwidth` is `"800"`, not `800`.
|
||||
*
|
||||
* @param fname Path to the JSON document. Required. Used verbatim.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p fname is `NULL`, or if the file cannot be
|
||||
* opened or does not parse. The message carries jansson's line number
|
||||
* and text.
|
||||
* @throws AKERR_KEY If the document has no top-level `properties` member.
|
||||
* @throws AKERR_TYPE If `properties` is not an object, or if one of its members
|
||||
* is not a string.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @note Writes into #AKGL_REGISTRY_PROPERTIES, so akgl_registry_init_properties
|
||||
* has to have run -- otherwise every entry is silently dropped and this
|
||||
* still returns success.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname);
|
||||
/**
|
||||
* @brief Registry init actor state strings.
|
||||
* @brief Create the actor-state-name registry and fill it from the name table.
|
||||
*
|
||||
* Walks `AKGL_ACTOR_STATE_STRING_NAMES` and maps entry `i` to the value `1 << i`,
|
||||
* which is what lets a character JSON write `"AKGL_ACTOR_STATE_FACE_LEFT"` and
|
||||
* have akgl_character_load_json turn it into a bit.
|
||||
*
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If SDL cannot allocate the property set. The
|
||||
* individual name registrations are not checked.
|
||||
*
|
||||
* @note The name table it reads disagrees with actor.h in two places: bits 11
|
||||
* and 12 are named `UNDEFINED_11`/`UNDEFINED_12` rather than `MOVING_IN`
|
||||
* and `MOVING_OUT`, so those two states cannot be named from JSON at all.
|
||||
* TODO.md items 24-26.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_init_actor_state_strings();
|
||||
/**
|
||||
* @brief Set property.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @param value Property value to store.
|
||||
* @brief Set one configuration property.
|
||||
*
|
||||
* @param name Property key. Required.
|
||||
* @param value Property value. Required. SDL copies it, so the caller's buffer
|
||||
* can go away afterwards.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p name or @p value is `NULL`.
|
||||
*
|
||||
* @note The write itself is unchecked. If #AKGL_REGISTRY_PROPERTIES is still 0 --
|
||||
* akgl_registry_init_properties never having run -- this discards the
|
||||
* value and returns success.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *value);
|
||||
/**
|
||||
* @brief Get property.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param def Fallback string returned when the property is absent.
|
||||
* @brief Read one configuration property into a pooled string, with a fallback.
|
||||
*
|
||||
* Absence is not an error: an unset property yields @p def, which is how every
|
||||
* caller in the library gets a working default without checking first.
|
||||
*
|
||||
* @param name Property key. Required.
|
||||
* @param dest Receives the value. Required. If `*dest` is `NULL` a string is
|
||||
* claimed from the pool for you, so initialize it to `NULL` on the
|
||||
* first call; either way the caller releases it with
|
||||
* akgl_heap_release_string. If `*dest` is non-`NULL` it is written
|
||||
* in place.
|
||||
* @param def Value to use when @p name is not set. Effectively required --
|
||||
* a `NULL` here on an unset property is an AKERR_NULLPOINTER rather
|
||||
* than an empty result.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p name or @p dest is `NULL`, or if the property
|
||||
* is unset and @p def is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If the stored value is #AKGL_MAX_STRING_LENGTH bytes
|
||||
* or longer, which will not fit an akgl_String with its terminator.
|
||||
* @throws AKERR_VALUE If the value and the destination overlap in memory.
|
||||
* @throws AKGL_ERR_HEAP If `*dest` was `NULL` and the string pool is exhausted.
|
||||
*
|
||||
* @note Only the value and its terminator are copied. The rest of @p dest keeps
|
||||
* whatever the previous holder left there, so read the result as a C
|
||||
* string rather than as #AKGL_MAX_STRING_LENGTH bytes.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def);
|
||||
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
/**
|
||||
* @file renderer.h
|
||||
* @brief Declares the public renderer API.
|
||||
* @brief The pluggable rendering backend: a record of function pointers plus an initializer.
|
||||
*
|
||||
* There is one backend shipped, the 2D SDL one, and `akgl_render_init2d` is its
|
||||
* initializer -- it creates the window and the `SDL_Renderer` and then fills in
|
||||
* the six `akgl_render_2d_*` entry points. A different renderer is a different
|
||||
* initializer populating the same struct, not a branch inside these functions.
|
||||
*
|
||||
* Those are two separable jobs, and a host that already owns an `SDL_Renderer`
|
||||
* -- an embedded interpreter, which must not create the window -- wants only the
|
||||
* second. akgl_render_bind2d() is that half on its own; akgl_render_init2d()
|
||||
* makes the window and then calls it.
|
||||
*
|
||||
* Callers do not normally name the `akgl_render_2d_*` functions directly; they
|
||||
* go through the pointers on the backend (`renderer->frame_start(renderer)`),
|
||||
* which is what makes the swap possible. The global `renderer` in game.h is the
|
||||
* instance the rest of the library draws through.
|
||||
*
|
||||
* A frame is `frame_start` (clear), any number of `draw_*` calls, then
|
||||
* `frame_end` (present).
|
||||
*/
|
||||
|
||||
#ifndef _RENDERER_H_
|
||||
@@ -14,71 +32,168 @@
|
||||
|
||||
/** @brief Defines a pluggable renderer backend and drawing callbacks. */
|
||||
typedef struct akgl_RenderBackend {
|
||||
SDL_Renderer *sdl_renderer;
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_texture)(struct akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags);
|
||||
SDL_Renderer *sdl_renderer; /**< The SDL renderer, created by the initializer. Owned by SDL, not freed here. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*shutdown)(struct akgl_RenderBackend *self); /**< Tear the backend down. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*frame_start)(struct akgl_RenderBackend *self); /**< Begin a frame: clear the target. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*frame_end)(struct akgl_RenderBackend *self); /**< End a frame: present it. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_texture)(struct akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip); /**< Blit one texture. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_mesh)(struct akgl_RenderBackend *self); /**< Reserved for a 3D backend; the 2D one refuses. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *(*draw_world)(struct akgl_RenderBackend *self, akgl_Iterator *opflags); /**< Draw the whole scene, layer by layer. */
|
||||
} akgl_RenderBackend;
|
||||
|
||||
/**
|
||||
* @brief Render 2d shutdown.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @brief Tear the 2D backend down.
|
||||
*
|
||||
* A no-op placeholder that always succeeds. The window and `SDL_Renderer` this
|
||||
* backend created are still owned by SDL and are reclaimed by `SDL_Quit`, so
|
||||
* there is nothing here to release yet; the hook exists so a backend that *does*
|
||||
* own resources has somewhere to free them.
|
||||
*
|
||||
* @param self The backend to shut down. Ignored -- not even dereferenced, so
|
||||
* `NULL` is currently harmless.
|
||||
* @return `NULL` always. It has no failure path today; check it anyway, because
|
||||
* a backend that acquires anything will grow one.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_shutdown(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Render 2d frame start.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @brief Begin a frame: clear the render target to opaque black.
|
||||
* @param self The backend to draw through. Required, and dereferenced *before*
|
||||
* it is checked -- a `NULL` @p self is a crash, not an error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
|
||||
* backend was never run through akgl_render_init2d.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_start(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Render 2d frame end.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @brief End a frame: present everything drawn since akgl_render_2d_frame_start.
|
||||
* @param self The backend to draw through. Required, and dereferenced *before*
|
||||
* it is checked -- a `NULL` @p self is a crash, not an error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If `self->sdl_renderer` is `NULL`, which means the
|
||||
* backend was never run through akgl_render_init2d.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_frame_end(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Render 2d draw texture.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param texture SDL texture to draw.
|
||||
* @param src Source value to copy or inspect.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param angle Clockwise rotation angle in degrees.
|
||||
* @param center Optional rotation center.
|
||||
* @param flip SDL texture flip mode.
|
||||
* @brief Blit a texture, rotating it only when asked to.
|
||||
*
|
||||
* A rotation of exactly 0 takes the plain `SDL_RenderTexture` path, which is why
|
||||
* @p center and @p flip are only consulted when @p angle is non-zero.
|
||||
*
|
||||
* @param self The backend to draw through. Required.
|
||||
* @param texture The texture to blit. Required.
|
||||
* @param src The rectangle of @p texture to take, in pixels. Optional --
|
||||
* `NULL` means the whole texture.
|
||||
* @param dest Where to put it on the render target, in pixels. Optional --
|
||||
* `NULL` means stretch to fill the whole target. Not an output
|
||||
* parameter despite the name.
|
||||
* @param angle Clockwise rotation in degrees. Exactly 0 skips rotation
|
||||
* entirely, along with @p center and @p flip.
|
||||
* @param center Pivot for the rotation, relative to @p dest. Required *when*
|
||||
* @p angle is non-zero, ignored otherwise. SDL's own "NULL means
|
||||
* the centre of dest" convention is not available here.
|
||||
* @param flip Horizontal/vertical mirroring. Applied only on the rotated
|
||||
* path; ignored when @p angle is 0.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self or @p texture is `NULL`; if @p angle is
|
||||
* non-zero and @p center is `NULL`; or if the SDL draw call itself
|
||||
* fails, in which case the message carries `SDL_GetError()`. That last
|
||||
* case is a reused status rather than a pointer problem.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_texture(akgl_RenderBackend *self, SDL_Texture *texture, SDL_FRect *src, SDL_FRect *dest, double angle, SDL_FPoint *center, SDL_FlipMode flip);
|
||||
/**
|
||||
* @brief Render 2d draw mesh.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||
* @brief Draw geometry. Not implemented by the 2D backend.
|
||||
*
|
||||
* The hook exists so a 3D backend has a slot to fill; this one refuses every
|
||||
* call. It is not a stub that quietly does nothing -- it fails loudly, so a
|
||||
* caller that reaches it finds out at once.
|
||||
*
|
||||
* @param self The backend. Ignored.
|
||||
* @return Never `NULL`.
|
||||
* @throws AKERR_API Always, with the message "Not implemented".
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_mesh(akgl_RenderBackend *self);
|
||||
/**
|
||||
* @brief Render 2d draw world.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param opflags Optional iterator operation flags; `NULL` selects defaults.
|
||||
* @brief Draw the whole scene: every tilemap layer, and the actors standing on it.
|
||||
*
|
||||
* Walks layers from 0 to #AKGL_TILEMAP_MAX_LAYERS. For each, it draws that layer
|
||||
* of the global `gamemap` through `camera` (if the map has that many layers),
|
||||
* then sweeps the actor pool and calls `renderfunc` on every live actor whose
|
||||
* `layer` matches. Sweeping in layer order is what puts actors in front of the
|
||||
* scenery they stand on and behind the scenery they walk under.
|
||||
*
|
||||
* It reads the globals `gamemap`, `camera`, and `HEAP_ACTOR` directly rather
|
||||
* than taking them as arguments, so there is exactly one world to draw.
|
||||
*
|
||||
* @param self The backend to draw through. Required.
|
||||
* @param opflags Iterator flags. Optional -- `NULL` substitutes a zeroed set.
|
||||
* Currently ignored either way: this function sweeps the actor
|
||||
* pool itself instead of going through
|
||||
* akgl_registry_iterate_actor, so no `AKGL_ITERATOR_OP_*` bit
|
||||
* reaches anything. The parameter is here for the backend
|
||||
* signature and for the layer-mask support that has yet to be
|
||||
* wired up.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
* @throws AKERR_* Whatever akgl_tilemap_draw or an actor's own `renderfunc`
|
||||
* raises; the first failure aborts the frame and propagates unchanged.
|
||||
*
|
||||
* @warning Neither the global `gamemap` nor a live actor's `renderfunc` is
|
||||
* checked before use, so drawing before akgl_tilemap_load, or with a
|
||||
* hand-built actor that was never run through akgl_actor_initialize,
|
||||
* is a crash rather than an error context.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags);
|
||||
|
||||
/**
|
||||
* @brief Render init2d.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @brief Install the 2D backend's methods on a backend the caller owns.
|
||||
*
|
||||
* The vtable half of akgl_render_init2d(), with no window, no renderer, and no
|
||||
* property registry involved: it points @p self's six function pointers at the
|
||||
* `akgl_render_2d_*` entry points and returns. `sdl_renderer` is not touched,
|
||||
* so a caller who has already put its own `SDL_Renderer` there keeps it, and a
|
||||
* caller who has not gets a backend whose entry points all report
|
||||
* `AKERR_NULLPOINTER` rather than crash.
|
||||
*
|
||||
* This is the entry point for a host that owns its own window -- it can drive
|
||||
* akgl_actor_render(), akgl_tilemap_draw() and the rest through a backend
|
||||
* libakgl never created. akgl_render_init2d() is the same thing with a window
|
||||
* in front of it.
|
||||
*
|
||||
* @param self The backend to bind. Required. Only the method pointers are
|
||||
* written.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self);
|
||||
|
||||
/**
|
||||
* @brief Create the window and SDL renderer, and bind the 2D backend's methods.
|
||||
*
|
||||
* Reads `game.screenwidth` and `game.screenheight` from the property registry
|
||||
* (both defaulting to the string "0", which asks SDL for a zero-sized window),
|
||||
* creates the window and renderer with `game.uri` as the title, points `camera`
|
||||
* at the full screen rectangle, and then calls akgl_render_bind2d() to install
|
||||
* the six `akgl_render_2d_*` function pointers on @p self.
|
||||
*
|
||||
* Because the dimensions come from the registry, akgl_registry_init_properties
|
||||
* and the property writes have to happen first -- see the note on
|
||||
* akgl_registry_init about the properties registry not being initialized for
|
||||
* callers that skip akgl_game_init.
|
||||
*
|
||||
* @param self The backend to initialize. Required. Its `sdl_renderer` and method
|
||||
* pointers are overwritten; anything it held before is not released.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
||||
* @throws AKERR_VALUE If `game.screenwidth` or `game.screenheight` is set to
|
||||
* something that is not a base-10 integer.
|
||||
* @throws ERANGE If either dimension does not fit in an `int`.
|
||||
* @throws AKGL_ERR_SDL If the window and renderer cannot be created. The message
|
||||
* carries `SDL_GetError()`.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted while reading the two
|
||||
* properties.
|
||||
*
|
||||
* @note The two pooled strings holding the dimensions are only released on the
|
||||
* success path, so each failed initialization leaks two string slots.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self);
|
||||
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
/**
|
||||
* @file sprite.h
|
||||
* @brief Declares the public sprite API.
|
||||
* @brief Spritesheets (one texture, many frames) and the animations cut out of them.
|
||||
*
|
||||
* The split is deliberate: an akgl_SpriteSheet owns the `SDL_Texture` and the
|
||||
* frame grid, and any number of akgl_Sprite animations point at the same sheet
|
||||
* and name the frame indices they use. Loading two sprites from the same image
|
||||
* loads the image once -- akgl_sprite_load_json looks the sheet up in
|
||||
* #AKGL_REGISTRY_SPRITESHEET by resolved path before creating one.
|
||||
*
|
||||
* Both are pool objects (akgl_heap_next_sprite, akgl_heap_next_spritesheet) and
|
||||
* both publish themselves in a registry under their name, which is how
|
||||
* akgl_character_sprite_add and the tilemap loader find them. Because loading a
|
||||
* sheet uploads a texture, the renderer has to exist first -- so
|
||||
* akgl_render_init2d, or akgl_game_init, before any of this.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_SPRITE_H_
|
||||
@@ -21,67 +33,138 @@
|
||||
|
||||
/** @brief Stores a loaded spritesheet texture and frame geometry. */
|
||||
typedef struct {
|
||||
uint8_t refcount;
|
||||
SDL_Texture *texture;
|
||||
char name[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH];
|
||||
uint16_t sprite_w;
|
||||
uint16_t sprite_h;
|
||||
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. Destroys the texture when it reaches 0. */
|
||||
SDL_Texture *texture; /**< The whole sheet as one GPU texture. Owned by this struct. */
|
||||
char name[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH]; /**< Registry key: the resolved path the image was loaded from. */
|
||||
uint16_t sprite_w; /**< Frame width. Vestigial: nothing in the library writes or reads it; the grid comes from akgl_Sprite::width. */
|
||||
uint16_t sprite_h; /**< Frame height. Vestigial, same as sprite_w. */
|
||||
} akgl_SpriteSheet;
|
||||
|
||||
/** @brief Describes an animated sprite and its spritesheet reference. */
|
||||
typedef struct {
|
||||
uint8_t refcount;
|
||||
uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; // which IDs on the spritesheet belong to our frames
|
||||
uint32_t frames; // how many frames are in this animation
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t speed; // how many milliseconds a given sprite frame should be visible before cycling
|
||||
bool loop; // when this sprite is done playing, it should immediately start again
|
||||
bool loopReverse; // when this sprite is done playing, it should go in reverse order through its frames
|
||||
char name[AKGL_SPRITE_MAX_NAME_LENGTH];
|
||||
akgl_SpriteSheet *sheet;
|
||||
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. One reference per character that maps it. */
|
||||
uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; /**< Frame numbers on the sheet, in playback order. Counted left to right, then wrapping to the next row. */
|
||||
uint32_t frames; /**< How many entries of frameids are in use. */
|
||||
uint32_t width; /**< Frame width in pixels; also the horizontal stride used to find a frame on the sheet. */
|
||||
uint32_t height; /**< Frame height in pixels; the vertical stride once frames wrap to the next row. */
|
||||
uint32_t speed; /**< Nanoseconds one frame is held. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONESEC_MS, which despite its name is nanoseconds-per-millisecond. TODO.md item 6. */
|
||||
bool loop; /**< Restart from frame 0 when the last frame is reached, instead of holding it. */
|
||||
bool loopReverse; /**< Play back down to frame 0 instead of jumping to it -- a ping-pong loop. */
|
||||
char name[AKGL_SPRITE_MAX_NAME_LENGTH]; /**< Registry key, from the JSON `name` field. */
|
||||
akgl_SpriteSheet *sheet; /**< The sheet these frames are cut from. Borrowed, not owned. */
|
||||
} akgl_Sprite;
|
||||
|
||||
// initializes a new sprite to use the given sheet and otherwise sets to zero
|
||||
/**
|
||||
* @brief Sprite initialize.
|
||||
* @param spr Sprite object to initialize.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @param sheet Spritesheet used by the sprite.
|
||||
* @brief Zero a pooled sprite, bind it to a sheet, and publish it in the sprite registry.
|
||||
*
|
||||
* Sets the name and the sheet and takes the first reference; everything else --
|
||||
* frame list, geometry, speed, loop flags -- is left at zero for the caller, or
|
||||
* akgl_sprite_load_json, to fill in. The sheet is borrowed, so this does *not*
|
||||
* take a reference on it: releasing the sheet out from under a live sprite
|
||||
* leaves a dangling pointer.
|
||||
*
|
||||
* @param spr Pooled sprite to initialize, normally from akgl_heap_next_sprite.
|
||||
* Required. Any previous contents are discarded.
|
||||
* @param name Registry key, NUL-terminated. Required. Copied at a fixed
|
||||
* #AKGL_SPRITE_MAX_NAME_LENGTH bytes, so a shorter string reads
|
||||
* past its end -- pass a name from an akgl_String or another
|
||||
* buffer of at least that size. An existing entry with the same
|
||||
* name is silently replaced.
|
||||
* @param sheet The spritesheet this sprite's frames come from. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p spr, @p name, or @p sheet is `NULL`.
|
||||
* @throws AKERR_KEY If the sprite cannot be written into #AKGL_REGISTRY_SPRITE
|
||||
* -- in practice, because akgl_registry_init has not run.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet);
|
||||
// loads a given image file into a new spritesheet
|
||||
/**
|
||||
* @brief Spritesheet initialize.
|
||||
* @param sheet Spritesheet used by the sprite.
|
||||
* @param sprite_w Width of one sprite frame in pixels.
|
||||
* @param sprite_h Height of one sprite frame in pixels.
|
||||
* @param filename Path to the source asset or JSON document.
|
||||
* @brief Load an image file as a texture and publish it as a spritesheet.
|
||||
*
|
||||
* Uploads @p filename through SDL_image using the global `renderer`, so the
|
||||
* renderer must already be initialized. The sheet is registered in
|
||||
* #AKGL_REGISTRY_SPRITESHEET under @p filename, which is the key
|
||||
* akgl_sprite_load_json looks it up by to avoid loading the same image twice.
|
||||
*
|
||||
* @param sheet Pooled spritesheet to initialize, normally from
|
||||
* akgl_heap_next_spritesheet. Required. Zeroed first, so a
|
||||
* texture it already held is leaked rather than destroyed.
|
||||
* @param sprite_w Frame width in pixels. Accepted and currently discarded --
|
||||
* the frame grid actually used at draw time comes from the
|
||||
* akgl_Sprite's `width`/`height`, not from here.
|
||||
* @param sprite_h Frame height in pixels. Same caveat as @p sprite_w.
|
||||
* @param filename Path to the image, in any format SDL_image can decode.
|
||||
* Required. Doubles as the registry key, so callers should pass
|
||||
* an already-resolved path; two spellings of the same file are
|
||||
* two sheets. Truncated at
|
||||
* #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p sheet or @p filename is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the image cannot be loaded -- missing, unreadable, or
|
||||
* a format SDL_image was not built with. The message carries
|
||||
* `SDL_GetError()`.
|
||||
* @throws AKERR_KEY If the sheet cannot be written into
|
||||
* #AKGL_REGISTRY_SPRITESHEET.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename);
|
||||
/**
|
||||
* @brief Sprite load json.
|
||||
* @param filename Path to the source asset or JSON document.
|
||||
* @brief Build a sprite -- and, if needed, its spritesheet -- from a JSON definition file.
|
||||
*
|
||||
* Reads `name`, `width`, `height`, `speed` (seconds, scaled to milliseconds),
|
||||
* `loop`, `loopReverse`, a `frames` array of frame indices, and a `spritesheet`
|
||||
* object holding `filename`, `frame_width`, and `frame_height`. The sheet's
|
||||
* `filename` is resolved *relative to the directory of @p filename*, so a sprite
|
||||
* definition can sit next to its image and move with it.
|
||||
*
|
||||
* If a spritesheet with that resolved path is already registered it is reused;
|
||||
* otherwise one is claimed from the pool and loaded. On failure both the sprite
|
||||
* and any sheet loaded for it are released again.
|
||||
*
|
||||
* @param filename Path to the JSON document. Required. Must be shorter than
|
||||
* #AKGL_MAX_STRING_LENGTH -- it is copied into a pooled string
|
||||
* so `dirname` can be taken without modifying the caller's
|
||||
* buffer.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p filename is `NULL`, or if the file cannot be
|
||||
* opened or does not parse. The message carries jansson's line number
|
||||
* and text.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p filename is at least #AKGL_MAX_STRING_LENGTH
|
||||
* bytes long, or if the `frames` array is indexed past its end.
|
||||
* @throws AKERR_KEY If a required key is absent, or if the sprite or sheet
|
||||
* cannot be added to its registry. The message names the key.
|
||||
* @throws AKERR_TYPE If a key is present with the wrong JSON type -- `speed` as
|
||||
* a string, `frames` as an object.
|
||||
* @throws AKGL_ERR_SDL If the spritesheet image fails to load.
|
||||
* @throws AKGL_ERR_HEAP If the sprite, spritesheet, or string pool is exhausted.
|
||||
*
|
||||
* @note The `frames` array is not bounded against #AKGL_SPRITE_MAX_FRAMES. A
|
||||
* definition with more than 16 frames writes past `frameids` into the
|
||||
* rest of the struct.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_load_json(char *filename);
|
||||
|
||||
/**
|
||||
* @brief Sprite sheet coords for frame.
|
||||
* @param self Backend or object instance to operate on.
|
||||
* @param srccoords Output source rectangle for the selected frame.
|
||||
* @param frameid Sprite frame index to resolve.
|
||||
* @brief Work out where one animation frame sits on its spritesheet.
|
||||
*
|
||||
* Frames are numbered in reading order across the sheet: multiply the frame
|
||||
* number by the sprite width, then wrap whole rows off the right-hand edge of
|
||||
* the texture and step down by the sprite height for each. Callers use the
|
||||
* result as the source rectangle for `SDL_RenderTexture`.
|
||||
*
|
||||
* @param self The sprite whose sheet and frame geometry to use. Required,
|
||||
* along with its `sheet`.
|
||||
* @param srccoords Receives the frame's rectangle on the sheet, in pixels.
|
||||
* Required -- the return value is the error context.
|
||||
* @param frameid Index into the sprite's `frameids` array, not a frame number
|
||||
* on the sheet. Not bounds-checked against `frames` or
|
||||
* #AKGL_SPRITE_MAX_FRAMES; an out-of-range index reads a
|
||||
* neighbouring struct member and yields a nonsense rectangle
|
||||
* rather than an error.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p self, @p srccoords, or `self->sheet` is
|
||||
* `NULL`. Note that `sheet->texture` is dereferenced without a check,
|
||||
* so a registered-but-unloaded sheet is a crash rather than an error.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid);
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/**
|
||||
* @file staticstring.h
|
||||
* @brief Declares the public staticstring API.
|
||||
* @brief A fixed-capacity string object handed out by the akgl string heap layer.
|
||||
*
|
||||
* The library allocates nothing at runtime, so a "string" here is a
|
||||
* PATH_MAX-sized buffer claimed from the pool with akgl_heap_next_string and
|
||||
* given back with akgl_heap_release_string. Capacity is fixed at compile time:
|
||||
* these functions truncate rather than grow, and truncation is silent.
|
||||
*/
|
||||
|
||||
#ifndef _STRING_H_
|
||||
@@ -15,25 +20,52 @@
|
||||
/** @brief Provides a fixed-capacity, heap-managed string buffer. */
|
||||
typedef struct
|
||||
{
|
||||
int refcount;
|
||||
char data[AKGL_MAX_STRING_LENGTH];
|
||||
int refcount; /**< Pool bookkeeping; 0 means the slot is free. Owned by the heap layer. */
|
||||
char data[AKGL_MAX_STRING_LENGTH]; /**< The characters. Not guaranteed NUL-terminated when filled to capacity. */
|
||||
} akgl_String;
|
||||
|
||||
/**
|
||||
* @brief String initialize.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param init Optional initial string contents; `NULL` clears the buffer.
|
||||
* @brief Set a pooled string's contents and mark the slot in use.
|
||||
*
|
||||
* Copies at most #AKGL_MAX_STRING_LENGTH bytes out of @p init, or zeroes the
|
||||
* buffer when @p init is `NULL`, then sets `refcount` to 1. Callers normally
|
||||
* reach this through akgl_heap_next_string rather than calling it directly.
|
||||
*
|
||||
* @param obj The pooled string to (re)initialize. Required. Its previous
|
||||
* contents are discarded without inspection.
|
||||
* @param init Initial contents, NUL-terminated. Optional -- `NULL` zero-fills
|
||||
* the buffer instead. An @p init longer than
|
||||
* #AKGL_MAX_STRING_LENGTH is truncated *and left unterminated*,
|
||||
* because this is `strncpy` semantics, not `strlcpy`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
|
||||
*
|
||||
* @note Known defect: the `NULL` @p init path zeroes `sizeof(akgl_String)`
|
||||
* bytes starting at `data`, which is four bytes past the end of the
|
||||
* buffer -- `refcount` sits in front of it. TODO.md, "Known and still
|
||||
* open" item 6.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_initialize(akgl_String *obj, char *init);
|
||||
/**
|
||||
* @brief String copy.
|
||||
* @param src Source value to copy or inspect.
|
||||
* @param dst Output destination populated by the function.
|
||||
* @param count Maximum number of elements or bytes to process; zero selects the default.
|
||||
* @brief Copy the contents of one pooled string into another.
|
||||
*
|
||||
* A bounded `strncpy` between two already-claimed pool slots. It copies bytes
|
||||
* only: `refcount` is left alone, so @p dst keeps whatever pool state it had.
|
||||
*
|
||||
* @param src Source string. Required. Read up to @p count bytes.
|
||||
* @param dst Destination string. Required. Overwritten in place; the pool
|
||||
* slot must already have been claimed.
|
||||
* @param count Maximum bytes to copy. 0 selects #AKGL_MAX_STRING_LENGTH, the
|
||||
* whole buffer. A @p count shorter than the source truncates
|
||||
* without writing a terminator; a @p count longer than the source
|
||||
* zero-pads the remainder, per `strncpy`. Values above
|
||||
* #AKGL_MAX_STRING_LENGTH overrun both buffers and are not
|
||||
* rejected.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p src or @p dst is `NULL`.
|
||||
* @throws errno Whatever `errno` holds if `strncpy` returns something other
|
||||
* than @p dst. In practice `strncpy` always returns its destination, so
|
||||
* this path is unreachable rather than merely rare.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_string_copy(akgl_String *src, akgl_String *dst, int count);
|
||||
#endif //_STRING_H_
|
||||
|
||||
@@ -1,35 +1,158 @@
|
||||
/**
|
||||
* @file text.h
|
||||
* @brief Declares the public text API.
|
||||
* @brief Loading fonts and drawing or measuring strings with them.
|
||||
*
|
||||
* Fonts are `TTF_Font *` handles kept in the #AKGL_REGISTRY_FONT property
|
||||
* registry under a caller-chosen name; there is no akgl font type wrapping them.
|
||||
* SDL_ttf must be initialized (akgl_game_init does it) before any of this.
|
||||
*
|
||||
* The two measure functions do not touch the renderer, so they are usable
|
||||
* before -- or entirely without -- a window. Drawing is immediate mode: each
|
||||
* akgl_text_rendertextat() call rasterizes, uploads, blits, and throws the
|
||||
* texture away, which is fine for a HUD line and wrong for a large body of
|
||||
* static text redrawn every frame.
|
||||
*/
|
||||
|
||||
#ifndef _TEXT_H_
|
||||
#define _TEXT_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <akerror.h>
|
||||
|
||||
/**
|
||||
* @brief Text loadfont.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @param filepath Path to the requested file.
|
||||
* @param size Requested font size.
|
||||
* @brief Open a TrueType font at one size and publish it in the font registry.
|
||||
*
|
||||
* A size is baked into the handle, so the same file at two sizes is two calls
|
||||
* under two names. Nothing releases these: the handles live until the process
|
||||
* ends.
|
||||
*
|
||||
* @param name Registry key to publish the font under. Required. An existing
|
||||
* entry with the same name is replaced, and the font it
|
||||
* displaced is closed through akgl_text_unloadfont -- but only
|
||||
* after the new one has opened, so a failed load leaves the
|
||||
* caller with the font they already had.
|
||||
* @param filepath Path to a `.ttf`/`.otf` file. Required. Used verbatim -- not
|
||||
* resolved against `SDL_GetBasePath()`.
|
||||
* @param size Point size to rasterize at. Passed straight to SDL_ttf, which
|
||||
* rejects anything that is not positive.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p name or @p filepath is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the font cannot be opened -- missing, unreadable, not
|
||||
* a font, or a @p size SDL_ttf refuses. The message carries
|
||||
* `SDL_GetError()`.
|
||||
* @throws AKERR_KEY If the font cannot be written into #AKGL_REGISTRY_FONT --
|
||||
* in practice, because akgl_registry_init has not run.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size);
|
||||
/**
|
||||
* @brief Text rendertextat.
|
||||
* @param font Font used to render the text.
|
||||
* @param text UTF-8 text to render.
|
||||
* @param color Text color.
|
||||
* @param wraplength Maximum rendered line width; zero disables wrapping.
|
||||
* @param x Horizontal destination coordinate.
|
||||
* @param y Vertical destination coordinate.
|
||||
* @brief Close a loaded font and take it out of the font registry.
|
||||
*
|
||||
* The other half of akgl_text_loadfont, and for a while the half that did not
|
||||
* exist: a font could be opened and published but never handed back, so a game
|
||||
* that changed fonts between scenes had no way to reclaim the one it had
|
||||
* finished with. A `TTF_Font` is about ten kilobytes once FreeType's own
|
||||
* structures are counted.
|
||||
*
|
||||
* The registry entry is cleared before the font is closed, so a font is never
|
||||
* reachable through #AKGL_REGISTRY_FONT after it has gone back to SDL_ttf.
|
||||
*
|
||||
* @param name Registry key the font was published under. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p name is `NULL`.
|
||||
* @throws AKERR_KEY If no font is registered under @p name -- including the
|
||||
* case where it was already unloaded, which makes a double unload an
|
||||
* error rather than a double close.
|
||||
*
|
||||
* @warning Anything still holding the `TTF_Font *` -- a caller that fetched it
|
||||
* from the registry earlier, a pending akgl_text_rendertextat -- is
|
||||
* left with a dangling pointer. Fonts are not reference counted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name);
|
||||
/**
|
||||
* @brief Rasterize a string and blit it at a screen position, in one call.
|
||||
*
|
||||
* Renders blended (anti-aliased, alpha-blended) through SDL_ttf, uploads the
|
||||
* result to a texture, draws it through the global `renderer`, and destroys both
|
||||
* the texture and the surface before returning. The text is drawn at its natural
|
||||
* size -- @p x and @p y are the top-left corner, not a centre.
|
||||
*
|
||||
* Coordinates are screen coordinates, not world ones: this does not go through
|
||||
* the camera, so a HUD stays put while the world scrolls under it.
|
||||
*
|
||||
* @param font Font to render with, from akgl_text_loadfont. Required.
|
||||
* @param text UTF-8 text. Required. May contain newlines, which break
|
||||
* lines on either path.
|
||||
* @param color Text colour, including alpha.
|
||||
* @param wraplength Wrap width in pixels. Greater than 0 wraps on word
|
||||
* boundaries at that width; 0 or less draws a single line and
|
||||
* breaks only on newlines in @p text.
|
||||
* @param x Left edge of the text, in screen pixels.
|
||||
* @param y Top edge of the text, in screen pixels.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p font or @p text is `NULL`; if the global
|
||||
* `renderer`, its `sdl_renderer`, or its `draw_texture` is `NULL` --
|
||||
* that last one is the state a backend is in between being allocated
|
||||
* and being run through akgl_render_bind2d(); if SDL_ttf cannot
|
||||
* rasterize the string; or if the surface cannot be uploaded as a
|
||||
* texture. The last two carry `SDL_GetError()` and are a reused status
|
||||
* rather than a pointer problem.
|
||||
* @throws AKERR_* Whatever the backend's `draw_texture` raises.
|
||||
*
|
||||
* @note On a failure after rasterizing -- the texture upload, or the draw -- the
|
||||
* surface and texture are not destroyed, because the error returns before
|
||||
* the cleanup. Repeated failures leak.
|
||||
* @note The empty string is **refused**, not drawn as nothing: SDL_ttf reports
|
||||
* "Text has zero width" and this passes that on as `AKERR_NULLPOINTER`.
|
||||
* akgl_text_measure() accepts it, so the two disagree. A caller drawing a
|
||||
* line of text that may be empty has to check for it. TODO.md, "Known and
|
||||
* still open".
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y);
|
||||
/**
|
||||
* @brief Report the size, in pixels, that @p text would occupy on one line.
|
||||
*
|
||||
* Nothing is drawn and no renderer is required. A caller building a character
|
||||
* grid measures one cell with this -- the advance width of a single glyph in a
|
||||
* monospaced font -- and derives the rest of the grid from it.
|
||||
*
|
||||
* @param font Font to measure with, from akgl_text_loadfont. Required.
|
||||
* @param text UTF-8 text to measure. Required. The empty string is legal and
|
||||
* measures 0 wide by one line high.
|
||||
* @param w Receives the width in pixels. Required.
|
||||
* @param h Receives the height in pixels -- one line, whatever @p text
|
||||
* contains, since this form does not wrap. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
|
||||
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h);
|
||||
/**
|
||||
* @brief Report the size, in pixels, that @p text would occupy when wrapped.
|
||||
*
|
||||
* The companion to akgl_text_measure() for the wrapping case, matching the
|
||||
* @p wraplength argument akgl_text_rendertextat() already takes: a string
|
||||
* longer than @p wraplength reports the height of every line it breaks onto.
|
||||
* A @p wraplength of zero wraps only on newlines in @p text.
|
||||
*
|
||||
* @param font Font to measure with, from akgl_text_loadfont. Required.
|
||||
* @param text UTF-8 text to measure. Required.
|
||||
* @param wraplength Wrap width in pixels. 0 wraps on newlines only. Negative is
|
||||
* refused rather than passed through: SDL_ttf reads a negative
|
||||
* width as a very large unsigned one and silently stops
|
||||
* wrapping, which would return a measurement that is wrong
|
||||
* rather than an error.
|
||||
* @param w Receives the width in pixels: the longest line, not
|
||||
* @p wraplength. Required.
|
||||
* @param h Receives the height in pixels, covering every line the text
|
||||
* wraps onto. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p font, @p text, @p w, or @p h is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p wraplength is negative.
|
||||
* @throws AKGL_ERR_SDL If SDL_ttf cannot measure the string -- a corrupt font,
|
||||
* or text that is not valid UTF-8. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h);
|
||||
|
||||
#endif // _TEXT_H_
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
/**
|
||||
* @file tilemap.h
|
||||
* @brief Declares the public tilemap API.
|
||||
* @brief Loading and drawing Tiled maps: tilesets, layers, embedded actors, per-map physics.
|
||||
*
|
||||
* The map format is the JSON export from the Tiled editor, so the level design
|
||||
* tool is somebody else's and the loader is the only thing that has to know
|
||||
* about it. A map is a stack of layers -- tile grids, images, and object groups
|
||||
* -- drawn over a set of tilesets, each tileset being one image cut into a grid.
|
||||
*
|
||||
* Three things beyond ordinary Tiled semantics are worth knowing:
|
||||
*
|
||||
* - **Object layers can spawn actors.** An object of type `actor` names a
|
||||
* character in its custom properties, and loading the map creates and
|
||||
* registers the actor rather than just recording a rectangle.
|
||||
* - **A map can carry its own physics.** A `physics.model` custom property on
|
||||
* the map selects a backend, and `physics.gravity.*` / `physics.drag.*` set
|
||||
* its constants, so a swimming level and a walking level differ by data
|
||||
* rather than by code.
|
||||
* - **A map can carry a pseudo-3D perspective.** Two objects named
|
||||
* `p_foreground` and `p_vanishing` in an object layer, each with a `scale`
|
||||
* property, define a band down the screen over which actors are scaled --
|
||||
* which is what makes a character look further away as they walk up the
|
||||
* screen. akgl_tilemap_scale_actor applies it.
|
||||
*
|
||||
* All paths inside a map -- tileset images, layer images -- are resolved
|
||||
* relative to the map file, so a map and its art move together.
|
||||
*
|
||||
* Note the size of akgl_Tilemap: a layer's tile grid alone is 512x512 ints, and
|
||||
* a tileset's offset table is 65536 pairs. It is a megabytes-large object and
|
||||
* belongs in static storage, which is where `_akgl_gamemap` puts it. Do not put
|
||||
* one on the stack.
|
||||
*/
|
||||
|
||||
#ifndef _TILEMAP_H_
|
||||
@@ -12,61 +40,74 @@
|
||||
#include <akgl/physics.h>
|
||||
#include <jansson.h>
|
||||
|
||||
/** @brief Widest map, in tiles. Width times height is what is actually bounded. */
|
||||
#define AKGL_TILEMAP_MAX_WIDTH 512
|
||||
/** @brief Tallest map, in tiles. */
|
||||
#define AKGL_TILEMAP_MAX_HEIGHT 512
|
||||
/** @brief Layers per map. Also the number of draw passes akgl_render_2d_draw_world makes. */
|
||||
#define AKGL_TILEMAP_MAX_LAYERS 16
|
||||
/** @brief Tilesets per map. */
|
||||
#define AKGL_TILEMAP_MAX_TILESETS 16
|
||||
/** @brief Entries in a tileset's offset table. Indexed by *local* tile id, so a tileset with a high `firstgid` still starts at 0. */
|
||||
#define AKGL_TILEMAP_MAX_TILES_PER_IMAGE 65536
|
||||
/** @brief Longest tileset name. */
|
||||
#define AKGL_TILEMAP_MAX_TILESET_NAME_SIZE 512
|
||||
/** @brief Longest resolved tileset image path. */
|
||||
#define AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE PATH_MAX
|
||||
/** @brief Longest object name. Note that an object naming an actor is truncated at #AKGL_ACTOR_MAX_NAME_LENGTH (128) instead. */
|
||||
#define AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE 512
|
||||
/** @brief Objects in one object layer. Not enforced by the loader -- a longer group writes past the array. */
|
||||
#define AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER 128
|
||||
|
||||
/** @brief Object `type` for a Tiled object whose `type` field is the string "actor". */
|
||||
#define AKGL_TILEMAP_OBJECT_TYPE_ACTOR 1
|
||||
|
||||
/** @brief Layer `type` for a Tiled `tilelayer`: a grid of tile ids. */
|
||||
#define AKGL_TILEMAP_LAYER_TYPE_TILES 1
|
||||
/** @brief Layer `type` for a Tiled `objectgroup`: actors and perspective markers. */
|
||||
#define AKGL_TILEMAP_LAYER_TYPE_OBJECTS 2
|
||||
/** @brief Layer `type` for a Tiled `imagelayer`: one image drawn at the origin. */
|
||||
#define AKGL_TILEMAP_LAYER_TYPE_IMAGE 3
|
||||
|
||||
/** @brief Stores tileset metadata, texture, and frame offsets. */
|
||||
/** @brief One object placed in a Tiled object layer. */
|
||||
typedef struct {
|
||||
float x;
|
||||
float y;
|
||||
int gid;
|
||||
int id;
|
||||
int height;
|
||||
int width;
|
||||
int rotation;
|
||||
int type;
|
||||
bool visible;
|
||||
akgl_Actor *actorptr;
|
||||
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE];
|
||||
float x; /**< Position in map pixels, from the Tiled object. Copied onto the actor it spawns. */
|
||||
float y; /**< Position in map pixels. */
|
||||
int gid; /**< Global tile id, for a tile object. Not read by the loader. */
|
||||
int id; /**< Tiled's object id. Not read by the loader. */
|
||||
int height; /**< Height in map pixels. Read into the map's perspective bands, not into this field. */
|
||||
int width; /**< Width in map pixels. Not read by the loader. */
|
||||
int rotation; /**< Rotation in degrees. Not read by the loader. */
|
||||
int type; /**< #AKGL_TILEMAP_OBJECT_TYPE_ACTOR, or 0 for anything else. */
|
||||
bool visible; /**< Copied onto the actor. Forced to `false` for perspective markers, which are geometry rather than scenery. */
|
||||
akgl_Actor *actorptr; /**< The actor this object spawned or attached to, for an actor object. `NULL` otherwise. Borrowed. */
|
||||
char name[AKGL_TILEMAP_MAX_OBJECT_NAME_SIZE]; /**< Object name. For an actor object this is the registry key; for a perspective marker it is `p_foreground` or `p_vanishing`. */
|
||||
} akgl_TilemapObject;
|
||||
|
||||
/** @brief Represents an object embedded in a tilemap layer. */
|
||||
/** @brief One layer of a tilemap: a tile grid, an image, or a group of objects. */
|
||||
typedef struct {
|
||||
short type;
|
||||
float opacity;
|
||||
bool visible;
|
||||
int height;
|
||||
int width;
|
||||
int x;
|
||||
int y;
|
||||
int id;
|
||||
SDL_Texture *texture;
|
||||
int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT];
|
||||
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER];
|
||||
short type; /**< Which of the `AKGL_TILEMAP_LAYER_TYPE_*` kinds this is; decides which of the members below mean anything. */
|
||||
float opacity; /**< 0.0 to 1.0, from Tiled. Recorded but not yet applied at draw time. */
|
||||
bool visible; /**< From Tiled. Recorded but not yet consulted at draw time. */
|
||||
int height; /**< Tile layer: height in tiles. Image layer: the texture's height in pixels. */
|
||||
int width; /**< Tile layer: width in tiles. Image layer: the texture's width in pixels. */
|
||||
int x; /**< Layer offset from Tiled. Recorded but not applied at draw time. */
|
||||
int y; /**< Layer offset from Tiled. */
|
||||
int id; /**< Tiled's layer id. Not the same as the index into akgl_Tilemap::layers. */
|
||||
SDL_Texture *texture; /**< Image layers only: the whole layer as one texture. `NULL` otherwise. Owned by this struct. */
|
||||
int data[AKGL_TILEMAP_MAX_WIDTH * AKGL_TILEMAP_MAX_HEIGHT]; /**< Tile layers only: global tile ids in row-major order. 0 means an empty cell. */
|
||||
akgl_TilemapObject objects[AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER]; /**< Object layers only. */
|
||||
} akgl_TilemapLayer;
|
||||
|
||||
/** @brief Stores tile, image, or object data for one map layer. */
|
||||
/** @brief One tileset: an image cut into a grid, plus the lookup table that finds a tile in it. */
|
||||
typedef struct {
|
||||
int columns;
|
||||
int firstgid;
|
||||
char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE];
|
||||
int imageheight;
|
||||
int imagewidth;
|
||||
char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE];
|
||||
SDL_Texture *texture;
|
||||
int columns; /**< Tiles per row in the image. What turns a linear tile id into a row and column. */
|
||||
int firstgid; /**< Global id of this tileset's first tile. Subtracting it from a map cell gives the local tile id. */
|
||||
char imagefilename[AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE]; /**< Resolved absolute path to the image, from the map file's directory. */
|
||||
int imageheight; /**< Image height in pixels, as declared by Tiled. */
|
||||
int imagewidth; /**< Image width in pixels, as declared. */
|
||||
char name[AKGL_TILEMAP_MAX_TILESET_NAME_SIZE]; /**< Tileset name from Tiled. Diagnostic only; lookups go by `firstgid`. */
|
||||
SDL_Texture *texture; /**< The image as one texture. Owned by this struct. */
|
||||
// Use this as a lookup table instead of storing tiles
|
||||
// in individual textures to blit them from a single
|
||||
// texture at runtime
|
||||
@@ -81,64 +122,123 @@ typedef struct {
|
||||
// lead to premature exhaustion of AKGL_TILEMAP_MAX_TILES_PER_IMAGE
|
||||
// because set D or E may only have 64 tiles but they may be
|
||||
// at the upper end of the array bound already because of this.
|
||||
int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2];
|
||||
int tilecount;
|
||||
int tileheight;
|
||||
int tilewidth;
|
||||
int spacing;
|
||||
int margin;
|
||||
int tile_offsets[AKGL_TILEMAP_MAX_TILES_PER_IMAGE][2]; /**< Local tile id -> {x, y} pixel offset into the image. Computed once at load by akgl_tilemap_compute_tileset_offsets. */
|
||||
int tilecount; /**< How many tiles the image holds. */
|
||||
int tileheight; /**< Height of one tile in pixels. */
|
||||
int tilewidth; /**< Width of one tile in pixels. */
|
||||
int spacing; /**< Pixels between adjacent tiles in the image. */
|
||||
int margin; /**< Pixels of border around the whole grid. Recorded but **not** accounted for by the offset computation. */
|
||||
} akgl_Tileset;
|
||||
|
||||
/** @brief Represents a complete tilemap and its optional physics backend. */
|
||||
typedef struct {
|
||||
int tilewidth;
|
||||
int tileheight;
|
||||
int width;
|
||||
int height;
|
||||
int numlayers;
|
||||
int orientation; // 0 = orthogonal, 1 = isometric
|
||||
int numtilesets;
|
||||
int p_foreground_y;
|
||||
int p_vanishing_y;
|
||||
int p_foreground_h;
|
||||
int p_vanishing_h;
|
||||
float p_foreground_scale;
|
||||
float p_vanishing_scale;
|
||||
float p_scale;
|
||||
float p_rate;
|
||||
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS];
|
||||
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS];
|
||||
int tilewidth; /**< Width of one map cell in pixels. Tiles from a tileset with a different tile size are not rescaled. */
|
||||
int tileheight; /**< Height of one map cell in pixels. */
|
||||
int width; /**< Map width in tiles. */
|
||||
int height; /**< Map height in tiles. */
|
||||
int numlayers; /**< Layers actually loaded, at most #AKGL_TILEMAP_MAX_LAYERS. */
|
||||
int orientation; /**< 0 = orthogonal, 1 = isometric. Always set to 0 by the loader; isometric is not implemented. */
|
||||
int numtilesets; /**< Tilesets actually loaded. */
|
||||
int p_foreground_y; /**< Y of the `p_foreground` marker: the row at which actors are at full size. 0 disables perspective. */
|
||||
int p_vanishing_y; /**< Y of the `p_vanishing` marker: the row at which actors are smallest. 0 disables perspective. */
|
||||
int p_foreground_h; /**< Height of the `p_foreground` marker object. Recorded; not used in the current rate calculation. */
|
||||
int p_vanishing_h; /**< Height of the `p_vanishing` marker object. Recorded, likewise. */
|
||||
float p_foreground_scale; /**< Actor scale at `p_foreground_y`. Defaults to 1.0. */
|
||||
float p_vanishing_scale; /**< Actor scale at `p_vanishing_y`. Defaults to 1.0. */
|
||||
float p_scale; /**< Unused. Left from an earlier formulation of the perspective maths. */
|
||||
float p_rate; /**< Scale change per pixel of y between the two markers. Derived at load; what akgl_tilemap_scale_actor interpolates with. */
|
||||
akgl_Tileset tilesets[AKGL_TILEMAP_MAX_TILESETS]; /**< The tilesets, in the order Tiled listed them. */
|
||||
akgl_TilemapLayer layers[AKGL_TILEMAP_MAX_LAYERS]; /**< The layers, in draw order: index 0 is furthest back. */
|
||||
|
||||
// Different levels may have different physics.
|
||||
bool use_own_physics;
|
||||
akgl_PhysicsBackend physics;
|
||||
bool use_own_physics; /**< Set when the map declared a `physics.model` property. A caller that honours it simulates through `physics` instead of the global backend. */
|
||||
akgl_PhysicsBackend physics; /**< This map's own backend, valid only when `use_own_physics` is set. */
|
||||
} akgl_Tilemap;
|
||||
|
||||
/**
|
||||
* @brief Tilemap load.
|
||||
* @param fname Path to the input or output file.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Load a Tiled JSON map: physics, geometry, layers, tilesets, and any actors it spawns.
|
||||
*
|
||||
* Zeroes @p dest, then reads the map's own physics properties, its tile
|
||||
* dimensions, its size in tiles, its layers, and its tilesets -- in that order,
|
||||
* because layers reference tilesets by global id and objects reference
|
||||
* characters by name. Finally, if the map carried both perspective markers, it
|
||||
* works out the per-pixel scaling rate between them.
|
||||
*
|
||||
* Loading a map has side effects beyond @p dest: tileset and layer images are
|
||||
* uploaded as textures, and every `actor` object in an object layer is created
|
||||
* in the actor pool and published in #AKGL_REGISTRY_ACTOR. So the renderer, the
|
||||
* pools, the registries, and the characters the map names all have to be in
|
||||
* place first.
|
||||
*
|
||||
* @param fname Path to the map JSON. Required. Its directory becomes the root
|
||||
* that every tileset and layer image inside is resolved against.
|
||||
* @param dest The tilemap to fill in. Required. Zeroed first, so a map already
|
||||
* loaded into it has its textures leaked rather than destroyed --
|
||||
* call akgl_tilemap_release first if you are reusing one. Do not
|
||||
* put one of these on the stack; see the note on this file.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p fname or @p dest is `NULL`; if the map file
|
||||
* cannot be opened or does not parse, with jansson's line and text; or
|
||||
* if a tileset image cannot be loaded.
|
||||
* @throws AKERR_OUTOFBOUNDS If the map is larger than
|
||||
* #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT tiles, if it has
|
||||
* more than #AKGL_TILEMAP_MAX_LAYERS layers, or if a layer's own
|
||||
* declared size exceeds the same bound.
|
||||
* @throws AKERR_KEY If a required key is absent, if an `actor` object has an
|
||||
* empty name, or if the map names a physics model that does not exist.
|
||||
* @throws AKERR_TYPE If a key is present with the wrong JSON type.
|
||||
* @throws ENOENT If @p fname or a path referenced inside it does not exist.
|
||||
* @throws AKGL_ERR_SDL If a layer image cannot be loaded.
|
||||
* @throws AKGL_ERR_HEAP If the string or actor pool is exhausted.
|
||||
*
|
||||
* @note Nothing is released on a failure part-way through: textures already
|
||||
* uploaded and actors already created stay. Treat a failed load as
|
||||
* needing akgl_tilemap_release and a fresh actor registry.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load(char *fname, akgl_Tilemap *dest);
|
||||
/**
|
||||
* @brief Tilemap draw.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param viewport World-space rectangle visible to the renderer.
|
||||
* @param layeridx Zero-based tilemap layer index.
|
||||
* @brief Draw one layer of a map, clipped to a viewport.
|
||||
*
|
||||
* Only the tiles that intersect @p viewport are drawn, and the tiles at its
|
||||
* edges are drawn partially, so scrolling is smooth rather than snapping to the
|
||||
* tile grid. An image layer ignores the viewport entirely and is drawn once at
|
||||
* the origin.
|
||||
*
|
||||
* Drawing goes through the global `renderer`, not through a backend passed in.
|
||||
*
|
||||
* @param dest The map to draw from. Required. Not an output parameter
|
||||
* despite the name.
|
||||
* @param viewport The rectangle of the map, in map pixels, that is on screen.
|
||||
* Required. Usually the global `camera`.
|
||||
* @param layeridx Which layer to draw, 0 to `numlayers - 1`. **Not**
|
||||
* bounds-checked: an index past the end reads a neighbouring
|
||||
* layer, or past the array.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p viewport is `NULL`.
|
||||
* @throws AKERR_* Whatever the renderer's `draw_texture` raises. The first
|
||||
* failure abandons the rest of the layer.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw(akgl_Tilemap *dest, SDL_FRect *viewport, int layeridx);
|
||||
/**
|
||||
* @brief Tilemap draw tileset.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param tilesetidx Zero-based tileset index.
|
||||
* @brief Draw a whole tileset to the screen, tile by tile, from its offset table.
|
||||
*
|
||||
* A debugging tool, not a rendering path. It reconstructs the original tileset
|
||||
* image out of the offsets computed at load time, so if the picture that appears
|
||||
* matches the source image the offset table is right -- and if tiles are shifted
|
||||
* or duplicated, it is not. The `charviewer` utility uses it.
|
||||
*
|
||||
* @param dest The map holding the tileset. Required. Not an output
|
||||
* parameter despite the name.
|
||||
* @param tilesetidx Which tileset to draw, 0 to `numtilesets - 1`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p tilesetidx is at or above `numtilesets`. A
|
||||
* negative index is not rejected.
|
||||
* @throws AKERR_* Whatever the renderer's `draw_texture` raises.
|
||||
*
|
||||
* @note Tiles are laid out using the *map's* tile size rather than the
|
||||
* tileset's, so a tileset whose tiles are a different size from the map's
|
||||
* is reconstructed at the wrong pitch.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest, int tilesetidx);
|
||||
|
||||
@@ -148,106 +248,245 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_draw_tileset(akgl_Tilemap *dest,
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Get json tilemap property.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param type Expected JSON property type or backend type name.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Find one entry in a Tiled `properties` array by name, checking its declared type.
|
||||
*
|
||||
* Tiled does not store custom properties as JSON object members. It stores them
|
||||
* as an array of `{"name": ..., "type": ..., "value": ...}` objects, so getting
|
||||
* one means a linear scan and a type-string comparison rather than a lookup.
|
||||
* That is what this does; the `akgl_get_json_properties_*` wrappers put a typed
|
||||
* face on it.
|
||||
*
|
||||
* @param obj The Tiled object -- map, layer, or object -- whose `properties`
|
||||
* array to search. Required.
|
||||
* @param key The property name to find. Required.
|
||||
* @param type The type string Tiled wrote, e.g. `"string"`, `"int"`, `"float"`.
|
||||
* A property found under the right name but the wrong type is an
|
||||
* error, not a miss. Not `NULL`-checked.
|
||||
* @param dest Receives a borrowed pointer to the whole property object -- the
|
||||
* one with `name`, `type`, and `value` -- not to its value. Not
|
||||
* `NULL`-checked.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_TYPE When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
|
||||
* @throws AKERR_KEY If @p obj has no `properties` array, or if no entry in it
|
||||
* has that name.
|
||||
* @throws AKERR_TYPE If `properties` is not an array, an entry is malformed, or
|
||||
* the named property's declared type is not @p type. That last message
|
||||
* reports both the expected and the actual type.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_tilemap_property(json_t *obj, char *key, char *type, json_t **dest);
|
||||
/**
|
||||
* @brief Get json properties string.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a Tiled custom property declared as `string`.
|
||||
* @param obj The Tiled object whose `properties` array to search. Required.
|
||||
* @param key The property name. Required.
|
||||
* @param dest Receives the value in a *newly claimed* pool string. Required. Note
|
||||
* that this always claims -- unlike akgl_get_json_string_value it
|
||||
* does not reuse a string already in `*dest`, so passing one leaks
|
||||
* it. The caller releases the result with akgl_heap_release_string.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
|
||||
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
|
||||
* @throws AKERR_TYPE If the property is not declared `string`.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_string(json_t *obj, char *key, akgl_String **dest);
|
||||
/**
|
||||
* @brief Get json properties integer.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a Tiled custom property declared as `int`.
|
||||
* @param obj The Tiled object whose `properties` array to search. Required.
|
||||
* @param key The property name. Required.
|
||||
* @param dest Receives the value. Not written on any failure path.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
|
||||
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
|
||||
* @throws AKERR_TYPE If the property is not declared `int`, or its `value` is
|
||||
* not an integer.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_properties_integer(json_t *obj, char *key, int *dest);
|
||||
/**
|
||||
* @brief Tilemap compute tileset offsets.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param tilesetidx Zero-based tileset index.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @brief Build a tileset's local-id-to-pixel-offset table.
|
||||
*
|
||||
* Run once per tileset at load time so drawing a tile is an array lookup rather
|
||||
* than a division. Walks the grid left to right and top to bottom, stepping by
|
||||
* tile size plus `spacing`.
|
||||
*
|
||||
* @param dest The map holding the tileset. Required in practice, though
|
||||
* not checked -- it is dereferenced immediately.
|
||||
* @param tilesetidx Which tileset, 0 to `numtilesets - 1`. Not bounds-checked.
|
||||
* @return `NULL`. There is no failure path: it is arithmetic into an already
|
||||
* allocated table.
|
||||
*
|
||||
* @note Two known limits. `margin` is not accounted for, so a tileset image with
|
||||
* a border produces offsets shifted by it. And the table is indexed by
|
||||
* local id from 0, while akgl_tilemap_draw indexes it by
|
||||
* `tilenum - firstgid`, so the entries a second tileset needs sit at the
|
||||
* front of its own table -- see the FIXME on akgl_Tileset::tile_offsets
|
||||
* for why that wastes space.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int tilesetidx);
|
||||
/**
|
||||
* @brief Tilemap load layer objects.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param layerid Zero-based tilemap layer index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @brief Load one object layer: spawn its actors and record its perspective markers.
|
||||
*
|
||||
* Two kinds of object are understood. `actor` objects name a registered actor,
|
||||
* or create one -- claiming it from the pool, initializing it, and binding the
|
||||
* character named in its `character` property -- and place it at the object's
|
||||
* coordinates on this layer. `perspective` objects named `p_foreground` or
|
||||
* `p_vanishing` set the map's scaling band and are themselves invisible.
|
||||
* Anything else is recorded and otherwise ignored.
|
||||
*
|
||||
* An actor named by two objects is not duplicated: the second takes another
|
||||
* reference on the first.
|
||||
*
|
||||
* @param dest The map to load into. Required.
|
||||
* @param root The layer's JSON object -- not the map root. Required.
|
||||
* @param layerid Index of the layer being loaded, which becomes each spawned
|
||||
* actor's `layer`. Not bounds-checked.
|
||||
* @param dirname Directory to resolve paths against. Accepted for signature
|
||||
* consistency with the other layer loaders; nothing here uses it.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
|
||||
* @throws AKERR_KEY If the layer has no `objects` array, if a required key is
|
||||
* absent, or if an `actor` object has an empty name.
|
||||
* @throws AKERR_TYPE If a key is present with the wrong JSON type.
|
||||
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
|
||||
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
|
||||
* @throws AKERR_* Whatever akgl_actor_initialize or akgl_actor_set_character
|
||||
* raises -- notably AKERR_KEY if the named character is not registered.
|
||||
*
|
||||
* @warning The object count is not checked against
|
||||
* #AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER, so a layer with more than 128
|
||||
* objects writes past the array.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap load layer tile.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param layerid Zero-based tilemap layer index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @brief Load one tile layer's grid of global tile ids.
|
||||
*
|
||||
* @param dest The map to load into. Required.
|
||||
* @param root The layer's JSON object -- not the map root. Required.
|
||||
* @param layerid Index of the layer being loaded. Not bounds-checked.
|
||||
* @param dirname Directory to resolve paths against. Required, though a tile
|
||||
* layer references no files and nothing here uses it.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If the layer's declared `width` times `height`
|
||||
* reaches #AKGL_TILEMAP_MAX_WIDTH x #AKGL_TILEMAP_MAX_HEIGHT, or if the
|
||||
* `data` array is shorter than that product.
|
||||
* @throws AKERR_KEY If `height`, `width`, or `data` is absent.
|
||||
* @throws AKERR_TYPE If one of them has the wrong JSON type, or a cell is not an
|
||||
* integer.
|
||||
*
|
||||
* @note The declared `width` and `height` are trusted over the actual length of
|
||||
* `data`: a layer declaring more cells than it lists fails on the read
|
||||
* past the end rather than on a length check.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap load layers.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @brief Load every layer of a map, dispatching on each layer's declared type.
|
||||
*
|
||||
* Reads the common fields -- id, opacity, visibility, offset -- then hands the
|
||||
* layer to the loader for its kind: `objectgroup`, `tilelayer`, or `imagelayer`.
|
||||
* A layer of any other type keeps its common fields and is otherwise skipped,
|
||||
* with `type` left at 0.
|
||||
*
|
||||
* @param dest The map to load into. Required. `numlayers` is set from the
|
||||
* array's length before any bound is checked, so it may briefly
|
||||
* exceed #AKGL_TILEMAP_MAX_LAYERS on the failure path.
|
||||
* @param root The map's root JSON object. Required.
|
||||
* @param dirname Directory to resolve layer image paths against. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If the map has more than #AKGL_TILEMAP_MAX_LAYERS
|
||||
* layers.
|
||||
* @throws AKERR_KEY If the map has no `layers` array, or a layer is missing a
|
||||
* common field.
|
||||
* @throws AKERR_TYPE If a field has the wrong JSON type.
|
||||
* @throws AKERR_* Whatever the per-kind loader raises.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap load tilesets each.
|
||||
* @param tileset JSON tileset object to load.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param tsidx Zero-based destination tileset index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @brief Load one tileset's metadata and upload its image.
|
||||
*
|
||||
* @param tileset The tileset's JSON object. Required in practice, though not
|
||||
* checked -- it is passed straight to the accessors, which
|
||||
* report it.
|
||||
* @param dest The map to load into. Required, unchecked, dereferenced at once.
|
||||
* @param tsidx Which slot to fill, 0 to #AKGL_TILEMAP_MAX_TILESETS - 1. Not
|
||||
* bounds-checked.
|
||||
* @param dirname Directory to resolve the tileset's `image` path against --
|
||||
* the map file's own directory. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If a required key is missing its object, or if the
|
||||
* tileset image fails to load. The image message carries
|
||||
* `SDL_GetError()`.
|
||||
* @throws AKERR_KEY If one of `columns`, `firstgid`, `imageheight`,
|
||||
* `imagewidth`, `margin`, `spacing`, `tilecount`, `tileheight`,
|
||||
* `tilewidth`, `name`, or `image` is absent.
|
||||
* @throws AKERR_TYPE If one of them has the wrong JSON type.
|
||||
* @throws AKERR_OUTOFBOUNDS If the resolved image path is too long for a pooled
|
||||
* string.
|
||||
* @throws ENOENT If the image path does not exist.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @note This does not go through the spritesheet registry, so a tileset image
|
||||
* shared between two maps is loaded twice.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilemap *dest, int tsidx, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap load tilesets.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @brief Load every tileset of a map and compute each one's offset table.
|
||||
*
|
||||
* @param dest The map to load into. Required. `numtilesets` is reset to 0 and
|
||||
* incremented as each tileset succeeds, so it always reflects
|
||||
* what actually loaded.
|
||||
* @param root The map's root JSON object. Required.
|
||||
* @param dirname Directory to resolve tileset image paths against. Required in
|
||||
* practice; not checked here.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p root is `NULL`.
|
||||
* @throws AKERR_KEY If the map has no `tilesets` array.
|
||||
* @throws AKERR_TYPE If `tilesets` is not an array of objects.
|
||||
* @throws AKERR_* Whatever akgl_tilemap_load_tilesets_each raises.
|
||||
*
|
||||
* @warning The tileset count is not checked against
|
||||
* #AKGL_TILEMAP_MAX_TILESETS, so a map with more than 16 tilesets
|
||||
* writes past the array.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname);
|
||||
/**
|
||||
* @brief Tilemap release.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Destroy the textures a map owns.
|
||||
*
|
||||
* Call before reusing a tilemap struct for a different map; akgl_tilemap_load
|
||||
* zeroes rather than releases, so without this the previous map's textures are
|
||||
* leaked.
|
||||
*
|
||||
* @param dest The map to release. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
|
||||
*
|
||||
* @warning Known defect: the layer loop destroys `tilesets[i].texture` rather
|
||||
* than `layers[i].texture`, so tileset textures are destroyed twice and
|
||||
* image-layer textures never. Nothing is set to `NULL` either, so a
|
||||
* second call is a use-after-free. It also does not release the actors
|
||||
* the map's object layers created. TODO.md, "Known and still open"
|
||||
* item 2.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_release(akgl_Tilemap *dest);
|
||||
/**
|
||||
* @brief Tilemap scale actor.
|
||||
* @param map Tilemap to inspect, draw, or modify.
|
||||
* @param actor Actor to inspect or modify.
|
||||
* @brief Scale an actor for its distance up the screen, using the map's perspective band.
|
||||
*
|
||||
* The pseudo-3D trick: an actor at or below `p_foreground_y` is drawn at
|
||||
* `p_foreground_scale`, one at or above `p_vanishing_y` at `p_vanishing_scale`,
|
||||
* and one in between is interpolated linearly. A map with no perspective markers
|
||||
* has both scales at 1.0 and a rate of 0, so this is a no-op rather than a
|
||||
* special case.
|
||||
*
|
||||
* Only the actor's `scale` is written; nothing is drawn. akgl_game_update calls
|
||||
* it before each actor's update when #AKGL_ITERATOR_OP_TILEMAPSCALE is set.
|
||||
*
|
||||
* @param map The map supplying the perspective band. Required.
|
||||
* @param actor The actor to scale. Required. Its `y` is read and its `scale`
|
||||
* written.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p map or @p actor is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_tilemap_scale_actor(akgl_Tilemap *map, akgl_Actor *actor);
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
/**
|
||||
* @file types.h
|
||||
* @brief Declares the public types API.
|
||||
* @brief Width-named floating point aliases, to match the `<stdint.h>` spelling.
|
||||
*
|
||||
* C names its integers by width and its floats by rank, so a struct full of
|
||||
* `uint32_t` and `float` reads inconsistently. These are the float spellings the
|
||||
* rest of the library uses. They are plain aliases -- no guarantee beyond what
|
||||
* the platform's `float` and `double` already provide -- and are deliberately
|
||||
* *not* C23's `_Float32`/`_Float64`, which carry IEEE-754 requirements this
|
||||
* library does not need.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_TYPES_H_
|
||||
@@ -8,7 +15,9 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** @brief Single-precision float. Positions, velocities, and scale factors. */
|
||||
typedef float float32_t;
|
||||
/** @brief Double-precision float. Declared for symmetry; unused so far. */
|
||||
typedef double float64_t;
|
||||
|
||||
#endif // _AKGL_TYPES_H_
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
/**
|
||||
* @file util.h
|
||||
* @brief Declares the public util API.
|
||||
* @brief Axis-aligned collision tests, path resolution, and two test-only image helpers.
|
||||
*
|
||||
* The grab bag. Three unrelated groups live here: rectangle/point overlap for
|
||||
* the physics backend, path resolution for the asset loaders, and a pair of
|
||||
* pixel-comparison routines that exist only so tests can assert on what was
|
||||
* actually drawn.
|
||||
*
|
||||
* All the geometry here is axis-aligned and treats edges as touching: a point
|
||||
* exactly on a boundary is inside. There is no rotation support and no
|
||||
* separating-axis test.
|
||||
*/
|
||||
|
||||
#ifndef _UTIL_H_
|
||||
@@ -9,83 +18,173 @@
|
||||
#include <akerror.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
/** @brief Represents a two-dimensional point. */
|
||||
/** @brief An integer point. Carries a `z` the collision routines do not use. */
|
||||
typedef struct point {
|
||||
int x;
|
||||
int y;
|
||||
int z;
|
||||
int x; /**< Horizontal position, in whatever space the caller is working in. */
|
||||
int y; /**< Vertical position. */
|
||||
int z; /**< Depth. Never written by akgl_rectangle_points and never read by the collision tests. */
|
||||
} point;
|
||||
|
||||
/** @brief Stores the corners of an axis-aligned rectangle. */
|
||||
/**
|
||||
* @brief The four corners of an axis-aligned rectangle, precomputed.
|
||||
*
|
||||
* akgl_collide_rectangles works corner by corner rather than by comparing edge
|
||||
* spans, so it wants the corners as points. akgl_rectangle_points derives one of
|
||||
* these from an `SDL_FRect`.
|
||||
*/
|
||||
typedef struct RectanglePoints {
|
||||
point topleft;
|
||||
point topright;
|
||||
point bottomleft;
|
||||
point bottomright;
|
||||
point topleft; /**< (x, y). */
|
||||
point topright; /**< (x + w, y). */
|
||||
point bottomleft; /**< (x, y + h). */
|
||||
point bottomright; /**< (x + w, y + h). */
|
||||
} RectanglePoints;
|
||||
|
||||
/**
|
||||
* @brief Do not use. Three open parentheses, two closes -- any expansion is a
|
||||
* syntax error. It duplicates akgl_collide_rectangles(), has no callers,
|
||||
* and is slated for deletion. TODO.md item 20.
|
||||
*/
|
||||
#define AKGL_COLLIDE_RECTANGLES(r1x, r1y, r1w, r1h, r2x, r2y, r2w, r2h) ((r1x < (r2x + r2w)) || ((r1x + r1w) > r2x)
|
||||
|
||||
/**
|
||||
* @brief Rectangle points.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param rect Rectangle whose corner points are calculated.
|
||||
* @brief Expand a rectangle into its four corner points.
|
||||
*
|
||||
* Coordinates are truncated from `float` to `int` on the way in, so a rectangle
|
||||
* at x = 10.9 has its corners at 10. That is deliberate for tile-grid work and
|
||||
* wrong for sub-pixel work; callers needing the latter should not round-trip
|
||||
* through this.
|
||||
*
|
||||
* @param dest Receives the corners. Required.
|
||||
* @param rect The rectangle, in any coordinate space. Required. `w` and `h` are
|
||||
* taken as extents from `x`/`y`, so a negative one produces a
|
||||
* rectangle whose "bottom right" is above and left of its "top
|
||||
* left" -- which every test here then reports as empty.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p dest or @p rect is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect);
|
||||
/**
|
||||
* @brief Collide point rectangle.
|
||||
* @param p Point to test for containment.
|
||||
* @param r Rectangle corner data used for collision testing.
|
||||
* @param collide Output set to whether the tested geometry intersects.
|
||||
* @brief Test whether a point falls inside a rectangle, edges included.
|
||||
*
|
||||
* Compares against `topleft` and `bottomright` only, so it assumes @p r is
|
||||
* well-formed -- the two corners actually being the minimum and maximum. `z` is
|
||||
* ignored on both sides: this is a 2D test.
|
||||
*
|
||||
* @param p The point to test. Required.
|
||||
* @param r The rectangle, as corners from akgl_rectangle_points. Required.
|
||||
* @param collide Receives `true` when the point is inside or exactly on an edge,
|
||||
* `false` otherwise. Required -- the return value is the error
|
||||
* context. Not written on any failure path.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p p, @p r, or @p collide is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(point *p, RectanglePoints *r, bool *collide);
|
||||
/**
|
||||
* @brief Collide rectangles.
|
||||
* @param r1 First rectangle to compare.
|
||||
* @param r2 Second rectangle to compare.
|
||||
* @param collide Output set to whether the tested geometry intersects.
|
||||
* @brief Test whether two rectangles overlap, edges included.
|
||||
*
|
||||
* Tests all eight corners -- each rectangle's four against the other -- and
|
||||
* stops at the first hit. Checking both directions is what catches the case
|
||||
* where one rectangle is entirely inside the other and so has no corner within
|
||||
* its neighbour.
|
||||
*
|
||||
* @param r1 First rectangle. Required.
|
||||
* @param r2 Second rectangle. Required. Order does not matter.
|
||||
* @param collide Receives `true` on any overlap or shared edge, `false`
|
||||
* otherwise. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p r1, @p r2, or @p collide is `NULL`.
|
||||
*
|
||||
* @note A corner-containment test misses the one arrangement where two
|
||||
* rectangles overlap in a cross without either enclosing a corner of the
|
||||
* other -- a tall thin rectangle crossing a short wide one. Both are
|
||||
* reported as not colliding.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
|
||||
|
||||
/**
|
||||
* @brief Path relative.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param path Path to resolve or transform.
|
||||
* @param dst Output destination populated by the function.
|
||||
* @brief Resolve an asset path, trying the working directory before the given root.
|
||||
*
|
||||
* Asset files name their neighbours relatively -- a sprite definition names its
|
||||
* spritesheet, a tilemap names its tilesets -- and "relative" has to mean
|
||||
* relative to the file doing the naming, not to wherever the game was launched
|
||||
* from. So this tries @p path against the process working directory first, and
|
||||
* only if that does not exist joins it onto @p root and resolves that. Either
|
||||
* way the result is absolute, with symlinks and `..` folded out.
|
||||
*
|
||||
* @param root Directory to fall back to, normally `dirname` of the file that
|
||||
* contained @p path. Required, even when unused.
|
||||
* @param path The path to resolve, relative or absolute. Required.
|
||||
* @param dst Receives the resolved absolute path. Required, and must already be
|
||||
* a claimed pool string -- this writes into it, it does not claim
|
||||
* one for you.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dst is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If `root + "/" + path` would not fit in
|
||||
* #AKGL_MAX_STRING_LENGTH.
|
||||
* @throws ENOENT If neither spelling names an existing file. Any other `errno`
|
||||
* `realpath(3)` can raise -- EACCES on an unsearchable directory,
|
||||
* ELOOP, ENOTDIR -- propagates the same way.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @note The fallback path -- the common one, since most asset references are
|
||||
* relative to their own file rather than to the working directory --
|
||||
* returns straight out of the ENOENT handler and so never releases the
|
||||
* error context it was handling. Each such call consumes one slot of
|
||||
* libakerror's fixed 128-entry context array for the life of the process.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dst);
|
||||
|
||||
// These are REALLY slow routines that are only useful in testing harnesses
|
||||
/**
|
||||
* @brief Compare sdl surfaces.
|
||||
* @param s1 First SDL surface to compare.
|
||||
* @param s2 Second SDL surface to compare.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_VALUE When the corresponding validation or operation fails.
|
||||
* @brief Assert that two surfaces hold byte-identical pixels.
|
||||
*
|
||||
* A `memcmp` over the raw pixel buffer, so it is exact: one differing byte in
|
||||
* one pixel is a failure. Meant for test harnesses asserting on rendered output,
|
||||
* not for anything on a frame path.
|
||||
*
|
||||
* @param s1 First surface. Required. Its `pitch * h` is what determines how many
|
||||
* bytes are compared.
|
||||
* @param s2 Second surface. Required.
|
||||
* @return `NULL` when the pixels match, otherwise an error context owned by the
|
||||
* caller. "Not equal" is reported as an error, not as an out-param.
|
||||
* @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`.
|
||||
* @throws AKERR_VALUE If the pixels differ.
|
||||
*
|
||||
* @warning The surfaces' dimensions, pitch, and format are not compared, so a
|
||||
* smaller @p s2 is read past its end rather than reported as a
|
||||
* mismatch. TODO.md, "Known and still open" item 5.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
|
||||
/**
|
||||
* @brief Render and compare.
|
||||
* @param t1 First texture to render for comparison.
|
||||
* @param t2 Second texture to render for comparison.
|
||||
* @param x Horizontal destination coordinate.
|
||||
* @param y Vertical destination coordinate.
|
||||
* @param w Destination width.
|
||||
* @param h Destination height.
|
||||
* @param writeout Optional filename for the rendered diagnostic image.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_IO When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
* @brief Draw two textures in turn, read the framebuffer back after each, and compare.
|
||||
*
|
||||
* The test-harness counterpart to akgl_compare_sdl_surfaces: it answers "do
|
||||
* these two textures *render* the same", which is not the same question as "are
|
||||
* these two textures identical", because the renderer's scaling and blending sit
|
||||
* in between. Both are drawn into the same rectangle against a cleared target.
|
||||
*
|
||||
* @param t1 First texture. Required.
|
||||
* @param t2 Second texture. Required.
|
||||
* @param x Left edge of the region, in pixels. Used for the source
|
||||
* rectangle, the destination, and the readback alike.
|
||||
* @param y Top edge of the region.
|
||||
* @param w Width of the region.
|
||||
* @param h Height of the region.
|
||||
* @param writeout Optional filename for a PNG of the *first* render, written
|
||||
* under `SDL_GetBasePath()`. `NULL` skips it. This is a
|
||||
* debugging aid -- when an image assertion fails, this is how
|
||||
* you see what was actually drawn.
|
||||
* @return `NULL` when the two renders match, otherwise an error context owned by
|
||||
* the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p t1 or @p t2 is `NULL`.
|
||||
* @throws AKGL_ERR_SDL If the framebuffer cannot be read back.
|
||||
* @throws AKERR_IO If @p writeout is given and the PNG cannot be written.
|
||||
* @throws AKERR_VALUE If the two renders differ.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @warning Known defect: both passes draw @p t1 -- @p t2 is never rendered -- so
|
||||
* this currently always reports a match. TODO.md, "Known and still
|
||||
* open" item 1.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);
|
||||
|
||||
|
||||
56
include/akgl/version.h.in
Normal file
56
include/akgl/version.h.in
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @file version.h
|
||||
* @brief Declares the libakgl version, both as compiled against and as linked.
|
||||
*
|
||||
* GENERATED FILE -- edit include/akgl/version.h.in, never the copy in the build
|
||||
* tree. Every value here comes from the project() call in CMakeLists.txt, which
|
||||
* is also what sets the shared library's VERSION and SOVERSION and the Version
|
||||
* field in akgl.pc. One number, one place, so the header, the soname and
|
||||
* pkg-config cannot drift apart.
|
||||
*
|
||||
* AKGL_VERSION is the version you *compiled against*. akgl_version() reports the
|
||||
* version of the libakgl you actually *linked*. They disagree when a stale
|
||||
* shared library is ahead of the new one on the loader path -- the failure the
|
||||
* soname exists to prevent and this pair exists to diagnose.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_VERSION_H_
|
||||
#define _AKGL_VERSION_H_
|
||||
|
||||
#define AKGL_VERSION "@PROJECT_VERSION@"
|
||||
#define AKGL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
|
||||
#define AKGL_VERSION_MINOR @PROJECT_VERSION_MINOR@
|
||||
#define AKGL_VERSION_PATCH @PROJECT_VERSION_PATCH@
|
||||
|
||||
/**
|
||||
* @brief True when the headers on the include path are at least the given version.
|
||||
*
|
||||
* For consumers that must build against more than one libakgl release. This is
|
||||
* the test libakstdlib could not write against libakerror, which published no
|
||||
* version macro and had to feature-test on AKERR_FIRST_CONSUMER_STATUS instead.
|
||||
*
|
||||
* @code
|
||||
* #if AKGL_VERSION_AT_LEAST(0, 2, 0)
|
||||
* akgl_something_new();
|
||||
* #endif
|
||||
* @endcode
|
||||
*/
|
||||
#define AKGL_VERSION_AT_LEAST(major, minor, patch) \
|
||||
((AKGL_VERSION_MAJOR > (major)) || \
|
||||
(AKGL_VERSION_MAJOR == (major) && AKGL_VERSION_MINOR > (minor)) || \
|
||||
(AKGL_VERSION_MAJOR == (major) && AKGL_VERSION_MINOR == (minor) && \
|
||||
AKGL_VERSION_PATCH >= (patch)))
|
||||
|
||||
/**
|
||||
* @brief Report the version of the libakgl that is actually linked.
|
||||
*
|
||||
* Returns "major.minor.patch". Compare it against AKGL_VERSION to detect a
|
||||
* stale shared library. Never returns NULL, and the storage is static -- the
|
||||
* caller must not free or modify it.
|
||||
*
|
||||
* This returns a string rather than an akerr_ErrorContext * because it cannot
|
||||
* fail, the same reason akerr_name_for_status() returns a name directly.
|
||||
*/
|
||||
const char *akgl_version(void);
|
||||
|
||||
#endif // _AKGL_VERSION_H_
|
||||
98
scripts/hooks/pre-commit
Executable file
98
scripts/hooks/pre-commit
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Reindent staged C sources to the canonical style before the commit is made.
|
||||
# See AGENTS.md -> "Coding Style".
|
||||
#
|
||||
# Enable with:
|
||||
# git config core.hooksPath scripts/hooks
|
||||
# Bypass a single commit with `git commit --no-verify`.
|
||||
#
|
||||
# The hook checks the *staged* content, not the working tree, so what gets
|
||||
# committed is what was verified. When a file needs reindenting it is fixed in
|
||||
# the working tree and re-staged -- but only when the working tree and the index
|
||||
# agree for that file. If they disagree (a partial `git add -p`), re-staging
|
||||
# would sweep unstaged work into the commit, so the hook stops and asks you to
|
||||
# do it yourself.
|
||||
|
||||
set -eu
|
||||
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
reindent="$root/scripts/reindent.sh"
|
||||
|
||||
# Leave conflict resolution alone.
|
||||
if [ -e "$root/.git/MERGE_HEAD" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -x "$reindent" ]; then
|
||||
echo "pre-commit: $reindent missing or not executable; skipping style check" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Without Emacs the canonical style cannot be applied or even checked. Warn
|
||||
# rather than block: a hook that fails closed on a missing optional tool just
|
||||
# teaches everyone to pass --no-verify.
|
||||
if ! command -v emacs >/dev/null 2>&1; then
|
||||
echo "pre-commit: emacs not found; skipping reindent (see AGENTS.md)" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
staged=$(git diff --cached --name-only --diff-filter=ACMR \
|
||||
| grep -E '^(src|include|tests|util)/.*\.[ch]$' \
|
||||
| grep -v -x -F 'include/akgl/SDL_GameControllerDB.h' || true)
|
||||
|
||||
if [ -z "$staged" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||
|
||||
# Reindent a copy of each file's staged content and see whether it moves.
|
||||
needs=''
|
||||
for f in $staged; do
|
||||
copy="$tmp/$(printf '%s' "$f" | tr '/' '_')"
|
||||
git show ":$f" >"$copy"
|
||||
cp "$copy" "$copy.orig"
|
||||
if ! "$reindent" "$copy" >/dev/null 2>&1; then
|
||||
echo "pre-commit: reindent failed on $f; commit aborted" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! cmp -s "$copy" "$copy.orig"; then
|
||||
needs="$needs $f"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$needs" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Refuse to touch anything if even one file is partially staged.
|
||||
blocked=''
|
||||
for f in $needs; do
|
||||
if ! git diff --quiet -- "$f"; then
|
||||
blocked="$blocked $f"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$blocked" ]; then
|
||||
echo "pre-commit: these files need reindenting but have unstaged changes," >&2
|
||||
echo "so re-staging them would pull unstaged work into the commit:" >&2
|
||||
for f in $blocked; do echo " $f" >&2; done
|
||||
echo >&2
|
||||
echo "Reindent and stage them yourself, then commit again:" >&2
|
||||
echo " scripts/reindent.sh$(for f in $blocked; do printf ' %s' "$f"; done)" >&2
|
||||
echo " git add$(for f in $blocked; do printf ' %s' "$f"; done)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Safe: for every file needing work, the index and working tree agree.
|
||||
# shellcheck disable=SC2086
|
||||
"$reindent" $needs
|
||||
# shellcheck disable=SC2086
|
||||
git add $needs
|
||||
|
||||
echo "pre-commit: reindented and re-staged:" >&2
|
||||
for f in $needs; do echo " $f" >&2; done
|
||||
|
||||
exit 0
|
||||
108
scripts/memcheck.sh
Executable file
108
scripts/memcheck.sh
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Run the existing CTest suites under valgrind and report what leaked.
|
||||
#
|
||||
# scripts/memcheck.sh check every suite in ./build
|
||||
# scripts/memcheck.sh -R tilemap check only the suites matching a regex
|
||||
# scripts/memcheck.sh -LE perf skip the perf suites
|
||||
#
|
||||
# Anything after the build directory is handed straight to ctest, so every
|
||||
# selection flag ctest understands works here.
|
||||
#
|
||||
# Environment:
|
||||
# AKGL_BUILD_DIR build tree to check (default: build)
|
||||
# AKGL_MEMCHECK_LOG directory for the per-suite valgrind logs
|
||||
# (default: $AKGL_BUILD_DIR/Testing/Temporary)
|
||||
#
|
||||
# Exit status: 0 when valgrind found nothing, 1 when it found something, 2 on a
|
||||
# usage or environment error. This is the part `ctest -T memcheck` will not do
|
||||
# on its own -- it records defects and still exits 0, which is no use as a gate.
|
||||
|
||||
set -eu
|
||||
|
||||
AKGL_BUILD_DIR="${AKGL_BUILD_DIR:-build}"
|
||||
|
||||
function memcheck_die()
|
||||
{
|
||||
echo "memcheck: $*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# The suite runs headless on purpose. Loading the real GPU stack costs thousands
|
||||
# of unfixable findings inside the vendor driver, and none of them are libakgl's.
|
||||
# A suite that sets these hints itself is unaffected; the ones that do not --
|
||||
# tests/tilemap.c and tests/sprite.c -- pick them up from here.
|
||||
export SDL_VIDEO_DRIVER="${SDL_VIDEO_DRIVER:-dummy}"
|
||||
export SDL_RENDER_DRIVER="${SDL_RENDER_DRIVER:-software}"
|
||||
export SDL_AUDIO_DRIVER="${SDL_AUDIO_DRIVER:-dummy}"
|
||||
|
||||
command -v valgrind >/dev/null 2>&1 || memcheck_die "valgrind not found"
|
||||
command -v ctest >/dev/null 2>&1 || memcheck_die "ctest not found"
|
||||
[ -f "$AKGL_BUILD_DIR/CMakeCache.txt" ] || memcheck_die "$AKGL_BUILD_DIR is not a configured build tree"
|
||||
|
||||
AKGL_MEMCHECK_LOG="${AKGL_MEMCHECK_LOG:-$AKGL_BUILD_DIR/Testing/Temporary}"
|
||||
|
||||
# Old logs would be counted as this run's findings. CTest numbers them by test
|
||||
# id, so a run narrowed with -R leaves the others behind.
|
||||
rm -f "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log
|
||||
|
||||
# The run itself is allowed to fail: a suite that valgrind makes slow enough to
|
||||
# trip an assertion still produced a log worth reading, and the log is what
|
||||
# decides the exit status below.
|
||||
set +e
|
||||
ctest --test-dir "$AKGL_BUILD_DIR" -T memcheck --output-on-failure "$@"
|
||||
AKGL_CTEST_STATUS=$?
|
||||
set -e
|
||||
|
||||
# What counts as a finding. "Definitely lost" is a leak nobody can argue with;
|
||||
# the invalid-access lines are memcheck telling us the library read or wrote
|
||||
# outside an allocation, which is worse than a leak and easier to fix.
|
||||
#
|
||||
# "are definitely lost", not "definitely lost": the LEAK SUMMARY block ends every
|
||||
# clean log with "definitely lost: 0 bytes in 0 blocks", and matching that
|
||||
# reported a finding in all twenty-three suites, including the ones that never
|
||||
# call malloc.
|
||||
AKGL_MEMCHECK_PATTERN='are definitely lost|Invalid read|Invalid write|Invalid free|Mismatched free|Source and destination overlap|depends on uninitialised|Use of uninitialised'
|
||||
|
||||
total=0
|
||||
echo
|
||||
echo "== valgrind findings by suite =="
|
||||
for log in "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log; do
|
||||
[ -f "$log" ] || continue
|
||||
# CTest writes the test name into the log's first line as the command it ran.
|
||||
suite=$(grep -m1 -oE 'akgl_test_[a-z_]+' "$log" || basename "$log")
|
||||
count=$(grep -cE "$AKGL_MEMCHECK_PATTERN" "$log" || true)
|
||||
total=$((total + count))
|
||||
if [ "$count" -gt 0 ]; then
|
||||
printf '%-24s %3d finding(s) %s\n' "$suite" "$count" "$log"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$total" -eq 0 ]; then
|
||||
echo "none"
|
||||
echo
|
||||
# A clean valgrind run and a failing test are different things: a suite can
|
||||
# assert its way to a non-zero exit without leaking a byte, and saying
|
||||
# "clean" and returning 0 for that would hide it.
|
||||
if [ "$AKGL_CTEST_STATUS" -ne 0 ]; then
|
||||
echo "memcheck: valgrind found nothing, but ctest exited $AKGL_CTEST_STATUS -- a suite failed on its own terms" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "memcheck: clean ($AKGL_MEMCHECK_LOG)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== library frames in those stacks, most frequent first =="
|
||||
# Keyed on the function name rather than the file, because src/physics.c and
|
||||
# tests/physics.c are both "physics.c" in a valgrind frame and only one of them
|
||||
# is the library. Anything a caller can reach is an akgl_ symbol by convention,
|
||||
# which makes the prefix a usable filter; a static helper appears under whatever
|
||||
# akgl_ function called it, one frame further down.
|
||||
grep -hoE 'by 0x[0-9A-Fa-f]+: akgl_[A-Za-z0-9_]+ \([a-z_]+\.c:[0-9]+\)' \
|
||||
"$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log 2>/dev/null |
|
||||
sed -E 's/^by 0x[0-9A-Fa-f]+: //' | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
echo
|
||||
echo "memcheck: $total finding(s); full stacks in $AKGL_MEMCHECK_LOG" >&2
|
||||
exit 1
|
||||
87
scripts/reindent.el
Normal file
87
scripts/reindent.el
Normal file
@@ -0,0 +1,87 @@
|
||||
;;; reindent.el --- batch reindent to cc-mode "stroustrup" -*- lexical-binding: t -*-
|
||||
|
||||
;; Reindents each file named on the command line, in place, to the project's
|
||||
;; canonical style: cc-mode "stroustrup", 4 columns per level, tabs 8 columns
|
||||
;; wide. See AGENTS.md -> "Coding Style".
|
||||
;;
|
||||
;; The style is set explicitly here rather than read from .dir-locals.el so that
|
||||
;; the result does not depend on where the file lives -- the pre-commit hook
|
||||
;; runs this over temporary copies outside the project tree.
|
||||
;;
|
||||
;; Usage: emacs --batch -Q -l scripts/reindent.el -- FILE...
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cc-mode)
|
||||
|
||||
(setq make-backup-files nil
|
||||
create-lockfiles nil
|
||||
auto-save-default nil
|
||||
vc-handled-backends nil
|
||||
inhibit-message t)
|
||||
|
||||
(defun akgl-retab-leading-whitespace ()
|
||||
"Rewrite every line's leading whitespace as tabs-then-spaces at `tab-width'.
|
||||
|
||||
`indent-region' fixes the column a line starts at, but `indent-line-to' leaves
|
||||
a line alone when it is already at the right column even if the bytes are
|
||||
wrong -- eight spaces where the canonical form is one tab. This pass closes
|
||||
that gap.
|
||||
|
||||
Deliberately not `tabify': that function's `tabify-regexp' is \" [ \\t]+\",
|
||||
which is NOT anchored to the line start, so it rewrites runs of spaces
|
||||
anywhere on the line and destroys the hand-aligned columns in the bit-flag
|
||||
tables in actor.h and iterator.h. Only leading whitespace is touched here, and
|
||||
lines beginning inside a string literal are skipped outright."
|
||||
(goto-char (point-min))
|
||||
(while (not (eobp))
|
||||
(let ((bol (point)))
|
||||
(unless (nth 3 (syntax-ppss bol))
|
||||
(skip-chars-forward " \t" (line-end-position))
|
||||
(let ((col (current-column)))
|
||||
(unless (or (zerop col) (eolp))
|
||||
(let ((want (concat (make-string (/ col tab-width) ?\t)
|
||||
(make-string (% col tab-width) ?\s))))
|
||||
(unless (string= want (buffer-substring bol (point)))
|
||||
(delete-region bol (point))
|
||||
(insert want)))))))
|
||||
(forward-line 1)))
|
||||
|
||||
(defun akgl-reindent-file (path)
|
||||
"Reindent PATH in place. Returns t if the file changed on disk."
|
||||
(let ((before (with-temp-buffer
|
||||
(insert-file-contents path)
|
||||
(buffer-string))))
|
||||
(with-current-buffer (find-file-noselect path t)
|
||||
(c-mode)
|
||||
(c-set-style "stroustrup")
|
||||
(setq indent-tabs-mode t
|
||||
tab-width 8
|
||||
c-basic-offset 4
|
||||
require-final-newline t)
|
||||
;; 1. Put every line at its correct column.
|
||||
(indent-region (point-min) (point-max))
|
||||
;; 2. Convert leading whitespace to the canonical tab/space mix.
|
||||
(akgl-retab-leading-whitespace)
|
||||
;; 3. Trailing whitespace and a single final newline.
|
||||
(delete-trailing-whitespace)
|
||||
(goto-char (point-max))
|
||||
(unless (bolp) (insert "\n"))
|
||||
(let ((changed (not (string= before (buffer-string)))))
|
||||
(when changed (save-buffer))
|
||||
(kill-buffer)
|
||||
changed))))
|
||||
|
||||
;; Emacs leaves the "--" separator in `command-line-args-left'; drop it, along
|
||||
;; with any empty argument, so the remainder is exactly the file list.
|
||||
(dolist (path (seq-remove (lambda (a) (or (string= a "--") (string= a "")))
|
||||
command-line-args-left))
|
||||
(when (akgl-reindent-file path)
|
||||
(princ (format "reindented %s\n" path))))
|
||||
|
||||
;; Exit 0 on success whether or not anything changed, so that any non-zero
|
||||
;; status from this script means a real failure. Callers detect "something
|
||||
;; changed" from stdout, or by comparing files themselves.
|
||||
(kill-emacs 0)
|
||||
|
||||
;;; reindent.el ends here
|
||||
101
scripts/reindent.sh
Executable file
101
scripts/reindent.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Reindent C sources to the project's canonical style (cc-mode "stroustrup",
|
||||
# 4-column offset, 8-column tabs). See AGENTS.md -> "Coding Style".
|
||||
#
|
||||
# scripts/reindent.sh reindent every tracked C source in place
|
||||
# scripts/reindent.sh FILE... reindent only the named files
|
||||
# scripts/reindent.sh --check ... report non-conforming files, change nothing
|
||||
#
|
||||
# Exit status: 0 if everything already conforms (or was reindented), 1 if
|
||||
# --check found a file that needs reindenting, 2 on a usage or environment
|
||||
# error.
|
||||
|
||||
set -eu
|
||||
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
elisp="$root/scripts/reindent.el"
|
||||
|
||||
# Directories whose C sources are hand-maintained. Anything outside these is
|
||||
# vendored (deps/) or generated (include/akgl/SDL_GameControllerDB.h) and is
|
||||
# left alone.
|
||||
SCOPE='src include tests util'
|
||||
GENERATED='include/akgl/SDL_GameControllerDB.h'
|
||||
|
||||
check=0
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
check=1
|
||||
shift
|
||||
fi
|
||||
|
||||
if ! command -v emacs >/dev/null 2>&1; then
|
||||
echo "reindent: emacs not found; cannot verify or apply the canonical style" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ ! -f "$elisp" ]; then
|
||||
echo "reindent: missing $elisp" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Build the file list: explicit arguments, or every tracked C source in scope.
|
||||
if [ "$#" -gt 0 ]; then
|
||||
files=$(for f in "$@"; do printf '%s\n' "$f"; done)
|
||||
else
|
||||
files=$(cd "$root" && git ls-files $SCOPE | grep -E '\.[ch]$' || true)
|
||||
fi
|
||||
|
||||
# Drop generated files and anything that no longer exists on disk.
|
||||
files=$(printf '%s\n' "$files" | grep -v -x -F "$GENERATED" || true)
|
||||
files=$(cd "$root" && for f in $files; do [ -f "$f" ] && printf '%s\n' "$f"; done)
|
||||
|
||||
if [ -z "$files" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$check" -eq 0 ]; then
|
||||
# Reindent in place. A non-zero status here is a real failure -- never
|
||||
# swallow it, or a broken Emacs would look like "everything already conforms".
|
||||
# shellcheck disable=SC2086
|
||||
(cd "$root" && emacs --batch -Q -l "$elisp" -- $files) || {
|
||||
echo "reindent: emacs failed; no files were reindented" >&2
|
||||
exit 2
|
||||
}
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --check: reindent throwaway copies and report which originals differ. The
|
||||
# copies keep their extension so cc-mode still selects the right major mode.
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||
|
||||
# Accept both repo-relative and absolute paths: the default file list is
|
||||
# relative, but callers (including the pre-commit hook) pass absolute ones.
|
||||
abspath() {
|
||||
case "$1" in
|
||||
/*) printf '%s' "$1" ;;
|
||||
*) printf '%s/%s' "$root" "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
copies=''
|
||||
for f in $files; do
|
||||
dest="$tmp/$(printf '%s' "$f" | tr '/' '_')"
|
||||
cp "$(abspath "$f")" "$dest"
|
||||
copies="$copies $dest"
|
||||
done
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
emacs --batch -Q -l "$elisp" -- $copies >/dev/null || {
|
||||
echo "reindent: emacs failed; cannot determine whether sources conform" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
status=0
|
||||
for f in $files; do
|
||||
dest="$tmp/$(printf '%s' "$f" | tr '/' '_')"
|
||||
if ! cmp -s "$(abspath "$f")" "$dest"; then
|
||||
echo "$f"
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
exit $status
|
||||
85
scripts/valgrind.supp
Normal file
85
scripts/valgrind.supp
Normal file
@@ -0,0 +1,85 @@
|
||||
# Valgrind suppressions for the libakgl memory-check run.
|
||||
#
|
||||
# Used by `cmake --build build --target memcheck` and by any hand-run
|
||||
# `ctest -T memcheck`; see AGENTS.md -> "Memory checking".
|
||||
#
|
||||
# Every entry here is a deliberate decision that a finding belongs to somebody
|
||||
# else's code and cannot be fixed from this repository. Nothing that libakgl
|
||||
# allocates is suppressed, and nothing is suppressed merely because it is noisy.
|
||||
# If a suppression starts hiding a real finding, delete it -- a false negative in
|
||||
# a leak check is worth more than a quiet log.
|
||||
#
|
||||
# The memcheck target runs the suite headless (SDL_VIDEO_DRIVER=dummy,
|
||||
# SDL_RENDER_DRIVER=software, SDL_AUDIO_DRIVER=dummy) precisely so that the GPU
|
||||
# stack is never loaded. That removes thousands of unfixable findings inside
|
||||
# amdgpu_dri.so / Mesa / libGLX without suppressing anything, which is why there
|
||||
# are no driver entries below. Run the suite against a real driver and you will
|
||||
# need them; do not add them here on that account.
|
||||
|
||||
# SDL allocates its global state -- the hint table, the property registry, the
|
||||
# log category array, the clipboard -- on first use and reclaims it in SDL_Quit.
|
||||
# A test that fails before SDL_Quit, and every test that deliberately leaves SDL
|
||||
# up so a later assertion can look at it, leaves those blocks behind. They are
|
||||
# one-per-process, not per-call, so they cannot accumulate into a leak.
|
||||
{
|
||||
sdl3-global-state-still-reachable
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
...
|
||||
fun:SDL_InitSubSystem_REAL
|
||||
}
|
||||
|
||||
# dlopen keeps the link map and the loaded objects' TLS blocks for the process
|
||||
# lifetime; a dlclose does not return them. SDL loads its video, audio, and
|
||||
# render backends this way, and SDL_image and SDL_mixer load their codecs the
|
||||
# same way.
|
||||
{
|
||||
dl-open-keeps-its-link-map
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable,possible
|
||||
...
|
||||
fun:_dl_open
|
||||
}
|
||||
|
||||
# FreeType, reached through SDL_ttf, caches per-face and per-size structures
|
||||
# inside the library instance and frees them in FT_Done_FreeType, which SDL_ttf
|
||||
# calls from TTF_Quit. A suite that opens a font and exits without TTF_Quit
|
||||
# reports them; a font libakgl itself failed to close is a different finding and
|
||||
# is NOT covered here -- it appears under akgl_text_loadfont, not under
|
||||
# FT_Init_FreeType.
|
||||
{
|
||||
freetype-library-instance
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
...
|
||||
fun:FT_Init_FreeType
|
||||
}
|
||||
|
||||
# deps/semver's own unit test leaks the buffers it cuts strings into --
|
||||
# semver_unit.c:8 and :21, one calloc each per case. Vendored code, and its test
|
||||
# program at that: not ours to edit, and not worth carrying a local patch for.
|
||||
# Named by function rather than by file so that a rewrite of those cases stops
|
||||
# being suppressed and comes back as a finding.
|
||||
#
|
||||
# This is the only entry here that hides a real leak in a program this build
|
||||
# runs. It is bounded, it is in a test, and the alternative is either a fork of
|
||||
# a vendored dependency or a memcheck run that is always red -- see TODO.md,
|
||||
# "Memory checking" item 39.
|
||||
{
|
||||
vendored-semver-unit-test-strcut-first
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
fun:calloc
|
||||
fun:test_strcut_first
|
||||
fun:test_strcut
|
||||
fun:main
|
||||
}
|
||||
{
|
||||
vendored-semver-unit-test-strcut-second
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
fun:calloc
|
||||
fun:test_strcut_second
|
||||
fun:test_strcut
|
||||
fun:main
|
||||
}
|
||||
74
src/actor.c
74
src/actor.c
@@ -28,14 +28,14 @@ akerr_ErrorContext *akgl_actor_initialize(akgl_Actor *obj, char *name)
|
||||
obj->curSpriteReversing = false;
|
||||
obj->scale = 1.0;
|
||||
obj->movement_controls_face = true;
|
||||
|
||||
|
||||
obj->updatefunc = &akgl_actor_update;
|
||||
obj->renderfunc = &akgl_actor_render;
|
||||
obj->facefunc = &akgl_actor_automatic_face;
|
||||
obj->movementlogicfunc = &akgl_actor_logic_movement;
|
||||
obj->changeframefunc = &akgl_actor_logic_changeframe;
|
||||
obj->addchild = &akgl_actor_add_child;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_ACTOR, name, (void *)obj),
|
||||
@@ -111,7 +111,7 @@ akerr_ErrorContext *akgl_actor_logic_changeframe(akgl_Actor *obj, akgl_Sprite *c
|
||||
} else {
|
||||
// we are at the end of the animation and we either loop forward or do not loop
|
||||
obj->curSpriteFrameId = 0;
|
||||
}
|
||||
}
|
||||
// we are not looping in reverse and we are not at the end of the animation
|
||||
} else {
|
||||
obj->curSpriteFrameId += 1;
|
||||
@@ -149,7 +149,7 @@ akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Time curtime = 0;
|
||||
akgl_Sprite *curSprite = NULL;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor reference");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
|
||||
|
||||
@@ -169,27 +169,37 @@ akerr_ErrorContext *akgl_actor_update(akgl_Actor *obj)
|
||||
// or changeframefunc, both of which should never return AKERR_KEY...
|
||||
SUCCEED_RETURN(errctx);
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Actor visible.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param camera Camera rectangle used for visibility testing.
|
||||
* @param visible Output set to whether the actor intersects the camera.
|
||||
* @brief Decide whether an actor is worth drawing this frame.
|
||||
*
|
||||
* Two questions at once: is the actor on camera, and does it want to be drawn.
|
||||
* The camera test allows a sprite's own width and height on the near edges, so
|
||||
* an actor partly on screen still counts as visible rather than popping in once
|
||||
* its origin crosses the boundary.
|
||||
*
|
||||
* An actor with no sprite for its current state is reported as not visible
|
||||
* rather than as an error -- there is nothing to draw, which is an answer.
|
||||
*
|
||||
* @param obj The actor to test. Required, along with its `basechar`.
|
||||
* @param camera The visible rectangle in map coordinates. Required in practice;
|
||||
* not checked, and dereferenced once a sprite has been found.
|
||||
* @param visible Receives the verdict. Required; not checked. Written on the
|
||||
* no-sprite path as well as the ordinary ones.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or `obj->basechar` is `NULL`.
|
||||
*/
|
||||
static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, bool *visible)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Sprite *curSprite = NULL;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
|
||||
} CLEANUP {
|
||||
@@ -199,7 +209,7 @@ static akerr_ErrorContext *actor_visible(akgl_Actor *obj, SDL_FRect *camera, boo
|
||||
*visible = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
if ( (obj->x < (camera->x - curSprite->width)) ||
|
||||
(obj->x > (camera->x + camera->w)) ||
|
||||
(obj->y < (camera->y - curSprite->height)) ||
|
||||
@@ -218,10 +228,10 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
|
||||
bool visible = false;
|
||||
SDL_FRect src;
|
||||
SDL_FRect dest;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL actor");
|
||||
FAIL_ZERO_RETURN(errctx, obj->basechar, AKERR_NULLPOINTER, "Actor has NULL base character reference");
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_character_sprite_get(obj->basechar, obj->state, &curSprite));
|
||||
CATCH(errctx, actor_visible(obj, camera, &visible));
|
||||
@@ -232,7 +242,7 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
|
||||
// If an actor doesn't have a sprite for a state, just log it and move on
|
||||
LOG_ERROR(errctx);
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
if ( ! visible ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -254,7 +264,7 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
if ( obj->parent != NULL ) {
|
||||
dest.x = (obj->parent->x + obj->x - camera->x);
|
||||
dest.y = (obj->parent->y + obj->y - camera->y);
|
||||
@@ -271,21 +281,21 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
|
||||
|
||||
akerr_ErrorContext *akgl_actor_add_child(akgl_Actor *obj, akgl_Actor *child)
|
||||
{
|
||||
int i = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent pointer");
|
||||
FAIL_ZERO_RETURN(errctx, child, AKERR_NULLPOINTER, "NULL child pointer");
|
||||
|
||||
FAIL_NONZERO_RETURN(errctx, child->parent, AKERR_RELATIONSHIP, "Child object already has a parent");
|
||||
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) {
|
||||
if ( obj->children[i] == NULL ) {
|
||||
obj->children[i] = child;
|
||||
child->parent = obj;
|
||||
child->refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
int i = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL parent pointer");
|
||||
FAIL_ZERO_RETURN(errctx, child, AKERR_NULLPOINTER, "NULL child pointer");
|
||||
|
||||
FAIL_NONZERO_RETURN(errctx, child->parent, AKERR_RELATIONSHIP, "Child object already has a parent");
|
||||
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN ; i++ ) {
|
||||
if ( obj->children[i] == NULL ) {
|
||||
obj->children[i] = child;
|
||||
child->parent = obj;
|
||||
child->refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
|
||||
FAIL_RETURN(errctx, AKERR_OUTOFBOUNDS, "Parent object has no remaining child slots left");
|
||||
}
|
||||
|
||||
void akgl_registry_iterate_actor(void *userdata, SDL_PropertiesID registry, const char *name)
|
||||
|
||||
76
src/assets.c
76
src/assets.c
@@ -14,46 +14,46 @@
|
||||
|
||||
akerr_ErrorContext *akgl_load_start_bgm(char *fname)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
//akgl_String *tmpstr = NULL;
|
||||
MIX_Track *bgmtrack = NULL;
|
||||
SDL_PropertiesID bgmprops = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
//akgl_String *tmpstr = NULL;
|
||||
MIX_Track *bgmtrack = NULL;
|
||||
SDL_PropertiesID bgmprops = 0;
|
||||
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, fname, AKERR_NULLPOINTER, "akgl_load_start_bgm received NULL filename");
|
||||
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
|
||||
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
|
||||
SDL_Log("Loading music asset from %s", fname);
|
||||
bgm = MIX_LoadAudio(akgl_mixer, fname, true);
|
||||
FAIL_ZERO_BREAK(errctx, bgm, AKERR_NULLPOINTER, "Failed to load music asset %s : %s", fname, SDL_GetError());
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, fname, AKERR_NULLPOINTER, "akgl_load_start_bgm received NULL filename");
|
||||
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
|
||||
bgmtrack = MIX_CreateTrack(akgl_mixer);
|
||||
FAIL_ZERO_BREAK(errctx, bgmtrack, AKERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError());
|
||||
//SDL_snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
|
||||
SDL_Log("Loading music asset from %s", fname);
|
||||
bgm = MIX_LoadAudio(akgl_mixer, fname, true);
|
||||
FAIL_ZERO_BREAK(errctx, bgm, AKERR_NULLPOINTER, "Failed to load music asset %s : %s", fname, SDL_GetError());
|
||||
|
||||
akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM] = bgmtrack;
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
MIX_SetTrackAudio(bgmtrack, bgm),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
bgmtrack = MIX_CreateTrack(akgl_mixer);
|
||||
FAIL_ZERO_BREAK(errctx, bgmtrack, AKERR_NULLPOINTER, "Failed to create audio track for background music: %s", SDL_GetError());
|
||||
|
||||
SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
|
||||
|
||||
if (!MIX_PlayTrack(bgmtrack, bgmprops)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Failed to play music asset %s", fname);
|
||||
}
|
||||
} CLEANUP {
|
||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||
if ( errctx != NULL ) {
|
||||
if ( errctx->status != 0 && bgm != NULL) {
|
||||
MIX_DestroyAudio(bgm);
|
||||
}
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
akgl_tracks[AKGL_GAME_AUDIO_TRACK_BGM] = bgmtrack;
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
MIX_SetTrackAudio(bgmtrack, bgm),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
|
||||
SDL_SetNumberProperty(bgmprops, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
|
||||
|
||||
if (!MIX_PlayTrack(bgmtrack, bgmprops)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Failed to play music asset %s", fname);
|
||||
}
|
||||
} CLEANUP {
|
||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||
if ( errctx != NULL ) {
|
||||
if ( errctx->status != 0 && bgm != NULL) {
|
||||
MIX_DestroyAudio(bgm);
|
||||
}
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
518
src/audio.c
Normal file
518
src/audio.c
Normal file
@@ -0,0 +1,518 @@
|
||||
/**
|
||||
* @file audio.c
|
||||
* @brief Implements the audio subsystem.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/audio.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
akgl_AudioVoice akgl_audio_voices[AKGL_AUDIO_MAX_VOICES];
|
||||
|
||||
/*
|
||||
* The device akgl_audio_init() opened, or NULL when the voice table is not
|
||||
* connected to one. Everything that mutates a voice locks this stream when it
|
||||
* is open, because the device callback reads the same table on SDL's audio
|
||||
* thread. When it is NULL there is no other thread to race with.
|
||||
*/
|
||||
static SDL_AudioStream *audiostream = NULL;
|
||||
|
||||
/** @brief Level every voice is scaled by before the mix is clamped. */
|
||||
static float32_t mastervolume = 1.0f;
|
||||
|
||||
/** @brief Scratch the device callback mixes into before handing it to SDL. */
|
||||
static float32_t mixbuffer[AKGL_AUDIO_MIX_FRAMES];
|
||||
|
||||
/*
|
||||
* State for the noise waveform. A 32-bit xorshift rather than rand(): it needs
|
||||
* no allocation, no locking beyond what already guards the voice table, and it
|
||||
* produces the same sequence every run, so a test can assert on noise output.
|
||||
*/
|
||||
static uint32_t noisestate = 0x13579bdfu;
|
||||
|
||||
/** @brief Whether the voice table has been given its defaults yet. */
|
||||
static bool voicesready = false;
|
||||
|
||||
/**
|
||||
* @brief Put every voice back to a flat, audible default.
|
||||
*
|
||||
* A zeroed voice has a sustain of 0.0, which is silence -- so a caller who
|
||||
* sounded a note without first describing an envelope would get nothing and no
|
||||
* error saying why. The default is instead the simplest thing that makes noise:
|
||||
* a square wave with no attack, no decay and no release, held at full level for
|
||||
* as long as the gate is open.
|
||||
*/
|
||||
static void reset_voices(void)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
||||
akgl_audio_voices[i].active = false;
|
||||
akgl_audio_voices[i].waveform = AKGL_AUDIO_WAVE_SQUARE;
|
||||
akgl_audio_voices[i].hz = 0.0f;
|
||||
akgl_audio_voices[i].phase = 0.0f;
|
||||
akgl_audio_voices[i].duration_frames = 0;
|
||||
akgl_audio_voices[i].elapsed_frames = 0;
|
||||
akgl_audio_voices[i].attack_frames = 0;
|
||||
akgl_audio_voices[i].decay_frames = 0;
|
||||
akgl_audio_voices[i].release_frames = 0;
|
||||
akgl_audio_voices[i].sustain = 1.0f;
|
||||
akgl_audio_voices[i].sweep_from_hz = 0.0f;
|
||||
akgl_audio_voices[i].sweep_to_hz = 0.0f;
|
||||
akgl_audio_voices[i].sweep_step_hz = 0.0f;
|
||||
}
|
||||
mastervolume = 1.0f;
|
||||
voicesready = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Give the voice table its defaults the first time anything touches it.
|
||||
*
|
||||
* The table is process-wide static storage, so it is reachable before
|
||||
* akgl_audio_init() has run. Rather than make every entry point document an
|
||||
* ordering requirement, the first one to arrive sets the defaults.
|
||||
*/
|
||||
static void ensure_voices(void)
|
||||
{
|
||||
if ( voicesready == false ) {
|
||||
reset_voices();
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Lock the voice table against the device callback, if one is running. */
|
||||
static void lock_voices(void)
|
||||
{
|
||||
if ( audiostream != NULL ) {
|
||||
SDL_LockAudioStream(audiostream);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Release the lock taken by lock_voices(). */
|
||||
static void unlock_voices(void)
|
||||
{
|
||||
if ( audiostream != NULL ) {
|
||||
SDL_UnlockAudioStream(audiostream);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Convert a duration in milliseconds to a whole number of frames. */
|
||||
static uint32_t frames_for_ms(uint32_t ms)
|
||||
{
|
||||
return (uint32_t)(((uint64_t)ms * AKGL_AUDIO_SAMPLE_RATE) / 1000);
|
||||
}
|
||||
|
||||
/** @brief Next value of the noise oscillator, in the range -1.0 to 1.0. */
|
||||
static float32_t noise_sample(void)
|
||||
{
|
||||
noisestate ^= noisestate << 13;
|
||||
noisestate ^= noisestate >> 17;
|
||||
noisestate ^= noisestate << 5;
|
||||
// The top 24 bits are the well-mixed ones; scale them to -1..1.
|
||||
return (((float32_t)(noisestate >> 8) / 8388607.5f) - 1.0f);
|
||||
}
|
||||
|
||||
/** @brief One sample of @p voice's oscillator at its current phase. */
|
||||
static float32_t voice_oscillator(akgl_AudioVoice *voice)
|
||||
{
|
||||
float32_t value = 0.0f;
|
||||
|
||||
switch ( voice->waveform ) {
|
||||
case AKGL_AUDIO_WAVE_TRIANGLE:
|
||||
if ( voice->phase < 0.5f ) {
|
||||
value = (4.0f * voice->phase) - 1.0f;
|
||||
} else {
|
||||
value = 3.0f - (4.0f * voice->phase);
|
||||
}
|
||||
break;
|
||||
case AKGL_AUDIO_WAVE_SAWTOOTH:
|
||||
value = (2.0f * voice->phase) - 1.0f;
|
||||
break;
|
||||
case AKGL_AUDIO_WAVE_SQUARE:
|
||||
value = ( voice->phase < 0.5f ) ? 1.0f : -1.0f;
|
||||
break;
|
||||
case AKGL_AUDIO_WAVE_NOISE:
|
||||
value = noise_sample();
|
||||
break;
|
||||
case AKGL_AUDIO_WAVE_SINE:
|
||||
value = SDL_sinf(voice->phase * 2.0f * SDL_PI_F);
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Envelope level @p frame frames into the gate, before the release.
|
||||
*
|
||||
* Split out because the release has to start from wherever the gate left off,
|
||||
* which for a gate shorter than attack plus decay is partway up or down a
|
||||
* ramp rather than at the sustain level.
|
||||
*/
|
||||
static float32_t voice_gate_level(akgl_AudioVoice *voice, uint32_t frame)
|
||||
{
|
||||
uint32_t elapsed = frame;
|
||||
|
||||
if ( elapsed < voice->attack_frames ) {
|
||||
return (float32_t)elapsed / (float32_t)voice->attack_frames;
|
||||
}
|
||||
elapsed -= voice->attack_frames;
|
||||
if ( elapsed < voice->decay_frames ) {
|
||||
return 1.0f - ((1.0f - voice->sustain) * ((float32_t)elapsed / (float32_t)voice->decay_frames));
|
||||
}
|
||||
return voice->sustain;
|
||||
}
|
||||
|
||||
/** @brief Envelope level for @p voice where it currently stands. */
|
||||
static float32_t voice_envelope(akgl_AudioVoice *voice)
|
||||
{
|
||||
uint32_t released = 0;
|
||||
float32_t gatelevel = 0.0f;
|
||||
|
||||
if ( voice->elapsed_frames < voice->duration_frames ) {
|
||||
return voice_gate_level(voice, voice->elapsed_frames);
|
||||
}
|
||||
released = voice->elapsed_frames - voice->duration_frames;
|
||||
if ( released >= voice->release_frames ) {
|
||||
return 0.0f;
|
||||
}
|
||||
gatelevel = voice_gate_level(voice, voice->duration_frames);
|
||||
return gatelevel * (1.0f - ((float32_t)released / (float32_t)voice->release_frames));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pitch @p voice sounds at where its sweep currently stands.
|
||||
*
|
||||
* Recomputed from the frame counter each time rather than added to as it goes,
|
||||
* so a sweep lands on exactly the frequencies its step size describes however
|
||||
* the caller happens to have chopped up its calls to akgl_audio_mix().
|
||||
*
|
||||
* A voice that is not sweeping keeps the frequency it was given.
|
||||
*/
|
||||
static float32_t voice_sweep_hz(akgl_AudioVoice *voice)
|
||||
{
|
||||
uint32_t ticks = 0;
|
||||
float32_t moved = 0.0f;
|
||||
float32_t hz = 0.0f;
|
||||
|
||||
if ( voice->sweep_step_hz <= 0.0f ) {
|
||||
return voice->hz;
|
||||
}
|
||||
ticks = voice->elapsed_frames / AKGL_AUDIO_SWEEP_TICK_FRAMES;
|
||||
moved = voice->sweep_step_hz * (float32_t)ticks;
|
||||
if ( voice->sweep_to_hz < voice->sweep_from_hz ) {
|
||||
hz = voice->sweep_from_hz - moved;
|
||||
if ( hz < voice->sweep_to_hz ) {
|
||||
hz = voice->sweep_to_hz;
|
||||
}
|
||||
} else {
|
||||
hz = voice->sweep_from_hz + moved;
|
||||
if ( hz > voice->sweep_to_hz ) {
|
||||
hz = voice->sweep_to_hz;
|
||||
}
|
||||
}
|
||||
return hz;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start a note on @p voice, sweeping or held.
|
||||
*
|
||||
* The whole of what akgl_audio_tone() and akgl_audio_sweep() do once their
|
||||
* arguments have been checked. A held note is a sweep with a step of 0, which
|
||||
* is also what makes a voice reused for a plain tone stop sweeping.
|
||||
*/
|
||||
static void start_note(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
|
||||
{
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
akgl_audio_voices[voice].hz = from_hz;
|
||||
akgl_audio_voices[voice].phase = 0.0f;
|
||||
akgl_audio_voices[voice].sweep_from_hz = from_hz;
|
||||
akgl_audio_voices[voice].sweep_to_hz = to_hz;
|
||||
akgl_audio_voices[voice].sweep_step_hz = step_hz;
|
||||
akgl_audio_voices[voice].duration_frames = frames_for_ms(ms);
|
||||
akgl_audio_voices[voice].elapsed_frames = 0;
|
||||
akgl_audio_voices[voice].active = true;
|
||||
unlock_voices();
|
||||
}
|
||||
|
||||
/** @brief Refuse a voice index that is not in the table. */
|
||||
static akerr_ErrorContext *check_voice(int voice)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((voice < 0) || (voice >= AKGL_AUDIO_MAX_VOICES)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Voice %d is not in the range 0 to %d",
|
||||
voice,
|
||||
AKGL_AUDIO_MAX_VOICES - 1);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fill SDL's request from the voice table.
|
||||
*
|
||||
* SDL holds the stream lock for the duration of this callback, which is the
|
||||
* same lock lock_voices() takes, so the voice table cannot change underneath a
|
||||
* mix in progress.
|
||||
*/
|
||||
static void SDLCALL audio_stream_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
|
||||
{
|
||||
akerr_ErrorContext *errctx = NULL;
|
||||
int frames = 0;
|
||||
|
||||
while ( additional_amount > 0 ) {
|
||||
frames = additional_amount / (int)sizeof(float32_t);
|
||||
if ( frames > AKGL_AUDIO_MIX_FRAMES ) {
|
||||
frames = AKGL_AUDIO_MIX_FRAMES;
|
||||
}
|
||||
if ( frames <= 0 ) {
|
||||
return;
|
||||
}
|
||||
errctx = akgl_audio_mix(mixbuffer, frames);
|
||||
if ( errctx != NULL ) {
|
||||
// There is nobody to return an error to on the audio thread, and
|
||||
// refusing to write leaves SDL underrunning. Report and go quiet.
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "** AUDIO CALLBACK **");
|
||||
errctx->handled = true;
|
||||
errctx = akerr_release_error(errctx);
|
||||
return;
|
||||
}
|
||||
SDL_PutAudioStreamData(stream, mixbuffer, frames * (int)sizeof(float32_t));
|
||||
additional_amount -= frames * (int)sizeof(float32_t);
|
||||
}
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_init(void)
|
||||
{
|
||||
SDL_AudioSpec spec;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
if ( audiostream != NULL ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
ensure_voices();
|
||||
|
||||
spec.format = SDL_AUDIO_F32;
|
||||
spec.channels = 1;
|
||||
spec.freq = AKGL_AUDIO_SAMPLE_RATE;
|
||||
|
||||
audiostream = SDL_OpenAudioDeviceStream(
|
||||
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
|
||||
&spec,
|
||||
&audio_stream_callback,
|
||||
NULL);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
audiostream,
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't open an audio device: %s",
|
||||
SDL_GetError());
|
||||
|
||||
// Devices open paused so a caller can set a stream up before it is heard.
|
||||
// Nothing here needs that, and a caller who expected akgl_audio_tone() to
|
||||
// make a sound would otherwise get silence with no error to explain it.
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_ResumeAudioStreamDevice(audiostream),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't start the audio device: %s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_shutdown(void)
|
||||
{
|
||||
SDL_AudioStream *closing = audiostream;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
// Clear the pointer before destroying the stream: lock_voices() checks it,
|
||||
// and SDL_DestroyAudioStream can run the callback one last time.
|
||||
audiostream = NULL;
|
||||
if ( closing != NULL ) {
|
||||
SDL_DestroyAudioStream(closing);
|
||||
}
|
||||
reset_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_tone(int voice, float32_t hz, uint32_t ms)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_NONZERO_RETURN(errctx, (hz <= 0.0f), AKERR_OUTOFBOUNDS, "Frequency %f is not positive", hz);
|
||||
FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");
|
||||
|
||||
// A step of 0 is what says "one pitch, held", so this also clears a sweep
|
||||
// left on the voice by an earlier akgl_audio_sweep().
|
||||
start_note(voice, hz, hz, 0.0f, ms);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_sweep(int voice, float32_t from_hz, float32_t to_hz, float32_t step_hz, uint32_t ms)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_NONZERO_RETURN(errctx, (from_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Start frequency %f is not positive", from_hz);
|
||||
FAIL_NONZERO_RETURN(errctx, (to_hz <= 0.0f), AKERR_OUTOFBOUNDS, "Target frequency %f is not positive", to_hz);
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(step_hz <= 0.0f),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Step %f is not positive; the direction of a sweep comes from the two frequencies",
|
||||
step_hz);
|
||||
FAIL_ZERO_RETURN(errctx, ms, AKERR_OUTOFBOUNDS, "A tone needs a duration; use akgl_audio_stop to silence a voice");
|
||||
|
||||
start_note(voice, from_hz, to_hz, step_hz, ms);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_stop(int voice)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
akgl_audio_voices[voice].active = false;
|
||||
akgl_audio_voices[voice].elapsed_frames = 0;
|
||||
akgl_audio_voices[voice].phase = 0.0f;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_waveform(int voice, akgl_AudioWaveform waveform)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((waveform < AKGL_AUDIO_WAVE_TRIANGLE) || (waveform > AKGL_AUDIO_WAVE_SINE)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Waveform %d is not one of the %d shapes",
|
||||
(int)waveform,
|
||||
(int)AKGL_AUDIO_WAVE_SINE + 1);
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
akgl_audio_voices[voice].waveform = waveform;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_envelope(int voice, uint32_t attack, uint32_t decay, float32_t sustain, uint32_t release)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((sustain < 0.0f) || (sustain > 1.0f)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Sustain level %f is not between 0.0 and 1.0",
|
||||
sustain);
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
akgl_audio_voices[voice].attack_frames = frames_for_ms(attack);
|
||||
akgl_audio_voices[voice].decay_frames = frames_for_ms(decay);
|
||||
akgl_audio_voices[voice].release_frames = frames_for_ms(release);
|
||||
akgl_audio_voices[voice].sustain = sustain;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_volume(float32_t level)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((level < 0.0f) || (level > 1.0f)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Volume level %f is not between 0.0 and 1.0",
|
||||
level);
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
mastervolume = level;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_voice_active(int voice, bool *active)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PASS(errctx, check_voice(voice));
|
||||
FAIL_ZERO_RETURN(errctx, active, AKERR_NULLPOINTER, "NULL activity destination");
|
||||
|
||||
ensure_voices();
|
||||
lock_voices();
|
||||
*active = akgl_audio_voices[voice].active;
|
||||
unlock_voices();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_audio_mix(float32_t *dest, int frames)
|
||||
{
|
||||
akgl_AudioVoice *voice = NULL;
|
||||
float32_t sum = 0.0f;
|
||||
bool sweeping = false;
|
||||
int i = 0;
|
||||
int v = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL sample destination");
|
||||
FAIL_NONZERO_RETURN(errctx, (frames < 0), AKERR_OUTOFBOUNDS, "Frame count %d is negative", frames);
|
||||
|
||||
for ( i = 0; i < frames; i++ ) {
|
||||
sum = 0.0f;
|
||||
for ( v = 0; v < AKGL_AUDIO_MAX_VOICES; v++ ) {
|
||||
voice = &akgl_audio_voices[v];
|
||||
if ( voice->active == false ) {
|
||||
continue;
|
||||
}
|
||||
if ( voice->elapsed_frames >= (voice->duration_frames + voice->release_frames) ) {
|
||||
// Gate and release are both spent. The voice goes quiet on its
|
||||
// own so a caller does not have to keep a clock to stop it.
|
||||
voice->active = false;
|
||||
voice->elapsed_frames = 0;
|
||||
voice->phase = 0.0f;
|
||||
continue;
|
||||
}
|
||||
sweeping = (voice->sweep_step_hz > 0.0f);
|
||||
if ( sweeping ) {
|
||||
voice->hz = voice_sweep_hz(voice);
|
||||
} else {
|
||||
// Derived from the frame counter rather than accumulated one
|
||||
// increment at a time. A float increment of hz/rate is not exact,
|
||||
// and adding it 44100 times a second walks the pitch off over the
|
||||
// length of a held note.
|
||||
voice->phase = (float32_t)SDL_fmod(
|
||||
((double)voice->elapsed_frames * (double)voice->hz) / (double)AKGL_AUDIO_SAMPLE_RATE,
|
||||
1.0);
|
||||
}
|
||||
sum += voice_oscillator(voice) * voice_envelope(voice);
|
||||
if ( sweeping ) {
|
||||
// The derived form above assumes one frequency for the whole note.
|
||||
// Under a sweep it would jump the waveform at every step -- an
|
||||
// audible click -- so a swept voice accumulates instead and takes
|
||||
// the drift the derived form exists to avoid.
|
||||
voice->phase = (float32_t)SDL_fmod(
|
||||
(double)voice->phase + ((double)voice->hz / (double)AKGL_AUDIO_SAMPLE_RATE),
|
||||
1.0);
|
||||
}
|
||||
voice->elapsed_frames += 1;
|
||||
}
|
||||
sum = sum * mastervolume;
|
||||
// Three voices at full level can sum past full scale. Clamping rather
|
||||
// than scaling by the voice count keeps a single voice at the level it
|
||||
// was asked for instead of a third of it.
|
||||
if ( sum > 1.0f ) {
|
||||
sum = 1.0f;
|
||||
} else if ( sum < -1.0f ) {
|
||||
sum = -1.0f;
|
||||
}
|
||||
dest[i] = sum;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -27,10 +27,10 @@ akerr_ErrorContext *akgl_character_initialize(akgl_Character *obj, char *name)
|
||||
strncpy(obj->name, name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH);
|
||||
obj->state_sprites = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, obj->state_sprites, AKERR_NULLPOINTER, "Unable to initialize SDL_PropertiesID for character state map");
|
||||
|
||||
|
||||
obj->sprite_add = &akgl_character_sprite_add;
|
||||
obj->sprite_get = &akgl_character_sprite_get;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_CHARACTER, name, (void *)obj),
|
||||
@@ -91,12 +91,28 @@ void akgl_character_state_sprites_iterate(void *userdata, SDL_PropertiesID regis
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Character load json state int from strings.
|
||||
* @param states JSON array of actor-state names.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief OR a JSON array of actor-state names together into one bitmask.
|
||||
*
|
||||
* This is what lets a character JSON say `["FACE_LEFT", "MOVING_LEFT"]` rather
|
||||
* than a number: each name is looked up in #AKGL_REGISTRY_ACTOR_STATE_STRINGS
|
||||
* and OR-ed in. A name that is not a known state is an error rather than being
|
||||
* skipped -- a typo in a state name would otherwise bind a sprite to the wrong
|
||||
* combination and show up as art that never appears.
|
||||
*
|
||||
* @param states JSON array of state-name strings. Required. An empty array is
|
||||
* legal and leaves @p dest as it was.
|
||||
* @param dest Receives the OR of every named bit. Required in practice, and
|
||||
* **not** checked -- the guard that should test it tests @p states
|
||||
* a second time instead, so a `NULL` here is a crash. It is OR-ed
|
||||
* into rather than assigned, so the caller must zero it first.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p states is `NULL`.
|
||||
* @throws AKERR_KEY If a name is not in the actor-state registry. The message
|
||||
* quotes it. Note that bits 11 and 12 are unreachable by name; see
|
||||
* akgl_registry_init_actor_state_strings.
|
||||
* @throws AKERR_TYPE If an array element is not a string.
|
||||
* @throws AKERR_OUTOFBOUNDS If the array is indexed past its end.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_t *states, int *dest)
|
||||
{
|
||||
@@ -106,7 +122,7 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL states array");
|
||||
FAIL_ZERO_RETURN(errctx, states, AKERR_NULLPOINTER, "NULL destination integer");
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstring));
|
||||
for ( i = 0; i < json_array_size((json_t *)states) ; i++ ) {
|
||||
@@ -123,11 +139,27 @@ static akerr_ErrorContext *akgl_character_load_json_state_int_from_strings(json_
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Character load json inner.
|
||||
* @param json JSON object to parse.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @brief Name a character and build its state-to-sprite map from a JSON document.
|
||||
*
|
||||
* The half of akgl_character_load_json that deals with identity and sprites; the
|
||||
* caller handles the numeric fields afterwards. Reads `name`, initializes the
|
||||
* character with it, then walks `sprite_mappings` binding each named sprite to
|
||||
* the bitmask its `state` array adds up to.
|
||||
*
|
||||
* Every sprite named must already be in #AKGL_REGISTRY_SPRITE.
|
||||
*
|
||||
* @param json The parsed character document. Required in practice; not checked,
|
||||
* and passed straight to accessors that report it.
|
||||
* @param obj The pooled character to fill in. Required, unchecked -- it is
|
||||
* handed to akgl_character_initialize, which does check it.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If a mapping names a sprite that is not registered.
|
||||
* The message names the character, the state, and the sprite.
|
||||
* @throws AKERR_KEY If `name`, `sprite_mappings`, or a mapping's `sprite` or
|
||||
* `state` is absent, or if a state name is unknown.
|
||||
* @throws AKERR_TYPE If one of those keys has the wrong JSON type.
|
||||
* @throws AKERR_OUTOFBOUNDS If an array is indexed past its end.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Character *obj)
|
||||
{
|
||||
@@ -140,7 +172,7 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
|
||||
akgl_String *tmpstr = NULL;
|
||||
akgl_String *tmpstr2 = NULL;
|
||||
int stateval = 0;
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr));
|
||||
CATCH(errctx, akgl_character_initialize((akgl_Character *)obj, tmpstr->data));
|
||||
@@ -155,15 +187,15 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
|
||||
NULL
|
||||
);
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &tmpstr2));
|
||||
|
||||
|
||||
CATCH(errctx, akgl_get_json_array_value((json_t *)curmapping, "state", &statearray));
|
||||
CATCH(errctx, akgl_character_load_json_state_int_from_strings(statearray, &stateval));
|
||||
|
||||
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)curmapping, "sprite", &tmpstr));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
spriteptr,
|
||||
AKERR_NULLPOINTER,
|
||||
AKERR_NULLPOINTER,
|
||||
"Character %s for state %b references sprite %s but not found in the registry",
|
||||
tmpstr2->data,
|
||||
stateval,
|
||||
@@ -186,11 +218,11 @@ static akerr_ErrorContext *akgl_character_load_json_inner(json_t *json, akgl_Cha
|
||||
akerr_ErrorContext *akgl_character_load_json(char *filename)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
json_t *json;
|
||||
json_t *json = NULL;
|
||||
json_error_t error;
|
||||
akgl_Character *obj = NULL;
|
||||
//akgl_String *tmpstr = NULL;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_character(&obj));
|
||||
@@ -213,6 +245,13 @@ akerr_ErrorContext *akgl_character_load_json(char *filename)
|
||||
CATCH(errctx, akgl_get_json_number_value(json, "acceleration_y", &obj->ay));
|
||||
} CLEANUP {
|
||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||
// The character keeps nothing that points into the document -- every
|
||||
// field above is copied out of it -- so the whole parsed tree goes back
|
||||
// here, on the success path as well as the failure one.
|
||||
if ( json != NULL ) {
|
||||
json_decref(json);
|
||||
json = NULL;
|
||||
}
|
||||
if ( errctx != NULL ) {
|
||||
if ( errctx->status != 0 ) {
|
||||
IGNORE(akgl_heap_release_character(obj));
|
||||
|
||||
404
src/controller.c
404
src/controller.c
@@ -12,6 +12,176 @@
|
||||
|
||||
akgl_ControlMap GAME_ControlMaps[AKGL_MAX_CONTROL_MAPS];
|
||||
|
||||
/*
|
||||
* Keystrokes waiting for akgl_controller_poll_key() and
|
||||
* akgl_controller_poll_keystroke(). Filled by akgl_controller_handle_event()
|
||||
* before it consults the control maps, so a key that also drives an actor is
|
||||
* still delivered to a polling caller.
|
||||
*
|
||||
* head is the next slot to read, count is how many are waiting. Both index a
|
||||
* fixed array rather than a queue object, which is the whole point: a host that
|
||||
* never polls cannot make this grow.
|
||||
*/
|
||||
static akgl_Keystroke keybuffer[AKGL_CONTROLLER_KEY_BUFFER];
|
||||
static int keybuffer_head = 0;
|
||||
static int keybuffer_count = 0;
|
||||
|
||||
/*
|
||||
* Whether the newest entry is a key press that has not been given its composed
|
||||
* text yet. SDL reports one keystroke as two events -- the key going down, and
|
||||
* then the text it composed to, if it composed to anything -- so the text that
|
||||
* arrives next belongs to the press that arrived last. Without this flag a
|
||||
* press dropped by a full buffer would hand its text to whatever older press
|
||||
* happened to still be sitting at the end of the ring.
|
||||
*/
|
||||
static bool keybuffer_awaiting_text = false;
|
||||
|
||||
/**
|
||||
* @brief Length in bytes of the UTF-8 sequence @p src begins with, or 0.
|
||||
*
|
||||
* 0 both for a byte that cannot start a sequence and for a sequence the string
|
||||
* ends in the middle of. SDL should hand over neither, and reading past a
|
||||
* terminator to find out otherwise is not a risk worth taking for input that
|
||||
* arrives from a keyboard driver.
|
||||
*
|
||||
* @param src UTF-8 text, NUL terminated. Required.
|
||||
*/
|
||||
static size_t utf8_sequence_length(const char *src)
|
||||
{
|
||||
unsigned char lead = (unsigned char)src[0];
|
||||
size_t len = 0;
|
||||
size_t i = 0;
|
||||
|
||||
if ( lead < 0x80 ) {
|
||||
return 1;
|
||||
} else if ( (lead & 0xe0) == 0xc0 ) {
|
||||
len = 2;
|
||||
} else if ( (lead & 0xf0) == 0xe0 ) {
|
||||
len = 3;
|
||||
} else if ( (lead & 0xf8) == 0xf0 ) {
|
||||
len = 4;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
for ( i = 1; i < len; i++ ) {
|
||||
// A NUL fails this too, which is what stops the scan at the end of a
|
||||
// truncated string instead of past it.
|
||||
if ( ((unsigned char)src[i] & 0xc0) != 0x80 ) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Copy as much of @p src as fits in @p dest without splitting a character.
|
||||
*
|
||||
* SDL hands over whatever the platform composed, which for an input method can
|
||||
* be several characters at once, and the ring entry holds one. Truncating on a
|
||||
* byte boundary would leave a partial UTF-8 sequence that nothing downstream
|
||||
* can render, so this stops at the last code point that fits whole.
|
||||
*
|
||||
* @param dest Receives the prefix, always NUL terminated. Required.
|
||||
* @param destsize Bytes available in @p dest, terminator included.
|
||||
* @param src UTF-8 text to copy. Required.
|
||||
*/
|
||||
static void copy_utf8_prefix(char *dest, size_t destsize, const char *src)
|
||||
{
|
||||
size_t used = 0;
|
||||
size_t len = 0;
|
||||
|
||||
dest[0] = '\0';
|
||||
while ( src[used] != '\0' ) {
|
||||
len = utf8_sequence_length(&src[used]);
|
||||
if ( len == 0 ) {
|
||||
// Not valid UTF-8 from here on. Keep what is already known good.
|
||||
break;
|
||||
}
|
||||
if ( (used + len) >= destsize ) {
|
||||
break;
|
||||
}
|
||||
memcpy(&dest[used], &src[used], len);
|
||||
used += len;
|
||||
}
|
||||
dest[used] = '\0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Record one key press, discarding it if the buffer is already full.
|
||||
*
|
||||
* Dropping the newest rather than overwriting the oldest is deliberate. A
|
||||
* caller reading a line of input wants the characters that were typed first;
|
||||
* overwriting would hand it the tail of what the user typed and silently lose
|
||||
* the head.
|
||||
*
|
||||
* The entry starts with no text. Whatever the press composes to arrives as a
|
||||
* separate event and is attached by keybuffer_attach_text().
|
||||
*/
|
||||
static void keybuffer_push_key(SDL_Keycode key, SDL_Keymod mod)
|
||||
{
|
||||
int tail = 0;
|
||||
|
||||
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
|
||||
keybuffer_awaiting_text = false;
|
||||
return;
|
||||
}
|
||||
tail = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||
keybuffer[tail].key = key;
|
||||
keybuffer[tail].mod = mod;
|
||||
keybuffer[tail].text[0] = '\0';
|
||||
keybuffer_count += 1;
|
||||
keybuffer_awaiting_text = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Give composed text to the press it belongs to, or buffer it alone.
|
||||
*
|
||||
* An entry with no keycode is not a degenerate case to be avoided: an input
|
||||
* method or a dead key finishes a character with no key press of its own, and a
|
||||
* line editor wants that character. akgl_controller_poll_keystroke() reports it;
|
||||
* akgl_controller_poll_key(), which has no keycode to give, discards it.
|
||||
*/
|
||||
static void keybuffer_attach_text(const char *text)
|
||||
{
|
||||
int newest = 0;
|
||||
|
||||
if ( (keybuffer_awaiting_text == true) && (keybuffer_count > 0) ) {
|
||||
newest = (keybuffer_head + keybuffer_count - 1) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||
copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
|
||||
keybuffer_awaiting_text = false;
|
||||
return;
|
||||
}
|
||||
if ( keybuffer_count >= AKGL_CONTROLLER_KEY_BUFFER ) {
|
||||
return;
|
||||
}
|
||||
newest = (keybuffer_head + keybuffer_count) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||
keybuffer[newest].key = 0;
|
||||
keybuffer[newest].mod = 0;
|
||||
copy_utf8_prefix(keybuffer[newest].text, AKGL_CONTROLLER_KEYSTROKE_TEXT, text);
|
||||
keybuffer_count += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Take the oldest entry, whatever it carries.
|
||||
*
|
||||
* @param dest Receives the entry. Required.
|
||||
* @return `true` when an entry was taken, `false` when the buffer was empty.
|
||||
*/
|
||||
static bool keybuffer_take(akgl_Keystroke *dest)
|
||||
{
|
||||
if ( keybuffer_count == 0 ) {
|
||||
return false;
|
||||
}
|
||||
memcpy(dest, &keybuffer[keybuffer_head], sizeof(akgl_Keystroke));
|
||||
keybuffer_head = (keybuffer_head + 1) % AKGL_CONTROLLER_KEY_BUFFER;
|
||||
keybuffer_count -= 1;
|
||||
if ( keybuffer_count == 0 ) {
|
||||
// Nothing is left for a text event to attach itself to.
|
||||
keybuffer_awaiting_text = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
|
||||
{
|
||||
int count;
|
||||
@@ -19,11 +189,20 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_list_keyboards(void)
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
FAIL_ZERO_RETURN(e, keyboards, AKERR_NULLPOINTER, "%s", SDL_GetError());
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *name = SDL_GetKeyboardNameForID(keyboards[i]);
|
||||
SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name);
|
||||
}
|
||||
|
||||
// The array is SDL's to allocate and ours to free -- that is the contract on
|
||||
// every SDL_Get*s() enumeration. Released in CLEANUP so the loop can fail
|
||||
// without taking the array with it.
|
||||
ATTEMPT {
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *name = SDL_GetKeyboardNameForID(keyboards[i]);
|
||||
SDL_Log("Keyboard %d: ID %u, Name: %s\n", i, keyboards[i], name);
|
||||
}
|
||||
} CLEANUP {
|
||||
SDL_free(keyboards);
|
||||
keyboards = NULL;
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
@@ -65,6 +244,18 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
|
||||
// Before the control maps, not after: a key bound to an actor is still a
|
||||
// key an interpreter polling with akgl_controller_poll_key() wants to see,
|
||||
// and the scan below returns as soon as a binding claims the event.
|
||||
if ( event->type == SDL_EVENT_KEY_DOWN ) {
|
||||
keybuffer_push_key(event->key.key, event->key.mod);
|
||||
} else if ( event->type == SDL_EVENT_TEXT_INPUT ) {
|
||||
// The character SDL composed for the press recorded a moment ago. A
|
||||
// keycode cannot express it: the layout, the shift state, a compose key
|
||||
// and a dead key all have their say between the two events.
|
||||
keybuffer_attach_text(event->text.text);
|
||||
}
|
||||
|
||||
ATTEMPT {
|
||||
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
||||
curmap = &GAME_ControlMaps[i];
|
||||
@@ -80,7 +271,7 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
|
||||
// This controlmap processes this control
|
||||
eventButtonComboMatch = (
|
||||
((event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
|
||||
event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) &&
|
||||
event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) &&
|
||||
event->gbutton.which == curmap->jsid &&
|
||||
event->gbutton.button == curcontrol->button) ||
|
||||
((event->type == SDL_EVENT_KEY_DOWN ||
|
||||
@@ -104,7 +295,7 @@ akerr_ErrorContext *akgl_controller_handle_event(void *appstate, SDL_Event *even
|
||||
}
|
||||
}
|
||||
}
|
||||
_akgl_controller_handle_event_success:
|
||||
_akgl_controller_handle_event_success:
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
@@ -112,16 +303,34 @@ _akgl_controller_handle_event_success:
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gamepad handle button down.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Set the movement and facing bits on the "player" actor for a D-pad or arrow press.
|
||||
*
|
||||
* A whole-application shortcut rather than a control-map binding: it looks the
|
||||
* actor up by the literal name `"player"` in #AKGL_REGISTRY_ACTOR instead of
|
||||
* being told which actor to act on, so it only ever drives one. The
|
||||
* akgl_Actor_cmhf_* handlers are the general form.
|
||||
*
|
||||
* Facing follows movement unless the actor sets `movement_controls_face`, which
|
||||
* is how an actor that aims independently of the direction it walks opts out.
|
||||
*
|
||||
* Declared nowhere -- `controller.h` advertises this as
|
||||
* `akgl_controller_handle_button_down`, which does not exist. TODO.md, "Known
|
||||
* and still open" item 10.
|
||||
*
|
||||
* @param appstate Passed through from SDL. Required as a non-`NULL` token only;
|
||||
* never read.
|
||||
* @param event The button or key event. Required. Both the gamepad and
|
||||
* keyboard unions are read on every call, so the arm that does
|
||||
* not correspond to the event type is read as garbage -- which
|
||||
* works only because no real button and keycode pair collides.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
|
||||
* no actor registered under the name "player".
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
||||
{
|
||||
akgl_Actor *player = NULL;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
@@ -130,39 +339,39 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
||||
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||
|
||||
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
SDL_Log("Processing dpad down : state %d", player->state);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||
if ( !player->movement_controls_face ) {
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_DOWN);
|
||||
}
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
|
||||
event->key.key == SDLK_UP ) {
|
||||
event->key.key == SDLK_UP ) {
|
||||
SDL_Log("Processing dpad up");
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||||
if ( !player->movement_controls_face ) {
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_UP);
|
||||
}
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
|
||||
event->key.key == SDLK_LEFT ) {
|
||||
event->key.key == SDLK_LEFT ) {
|
||||
SDL_Log("Processing dpad left");
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
|
||||
if ( !player->movement_controls_face ) {
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
}
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
|
||||
event->key.key == SDLK_RIGHT ) {
|
||||
event->key.key == SDLK_RIGHT ) {
|
||||
SDL_Log("Processing dpad right");
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
||||
if ( !player->movement_controls_face ) {
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT);
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
AKGL_BITMASK_ADD(player->state, AKGL_ACTOR_STATE_FACE_RIGHT);
|
||||
}
|
||||
SDL_Log("New state : %d", player->state);
|
||||
}
|
||||
@@ -170,16 +379,25 @@ akerr_ErrorContext *gamepad_handle_button_down(void *appstate, SDL_Event *event)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gamepad handle button up.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Clear the movement bit on the "player" actor for a D-pad or arrow release, and reset its animation.
|
||||
*
|
||||
* The counterpart to gamepad_handle_button_down. It clears only the movement
|
||||
* bit, leaving the facing bits alone so the actor keeps looking the way it was
|
||||
* walking, and rewinds `curSpriteFrameId` to 0 so the next step starts from the
|
||||
* first frame of the walk cycle rather than wherever it stopped.
|
||||
*
|
||||
* Declared nowhere; see gamepad_handle_button_down.
|
||||
*
|
||||
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
|
||||
* @param event The button or key release event. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`, or if there is
|
||||
* no actor registered under the name "player".
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
||||
{
|
||||
akgl_Actor *player = NULL;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
@@ -188,25 +406,25 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
||||
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "Player actor does not exist");
|
||||
|
||||
if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_DOWN ||
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
event->key.key == SDLK_DOWN ) {
|
||||
SDL_Log("processing down release");
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||
player->curSpriteFrameId = 0;
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_UP ||
|
||||
event->key.key == SDLK_UP ) {
|
||||
event->key.key == SDLK_UP ) {
|
||||
SDL_Log("processing up release");
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||||
player->curSpriteFrameId = 0;
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT ||
|
||||
event->key.key == SDLK_RIGHT) {
|
||||
event->key.key == SDLK_RIGHT) {
|
||||
SDL_Log("processing right release");
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
||||
player->curSpriteFrameId = 0;
|
||||
SDL_Log("New state : %d", player->state);
|
||||
} else if ( event->gbutton.button == SDL_GAMEPAD_BUTTON_DPAD_LEFT ||
|
||||
event->key.key == SDLK_LEFT ) {
|
||||
event->key.key == SDLK_LEFT ) {
|
||||
SDL_Log("processing left release");
|
||||
AKGL_BITMASK_DEL(player->state, AKGL_ACTOR_STATE_MOVING_LEFT);
|
||||
player->curSpriteFrameId = 0;
|
||||
@@ -216,35 +434,52 @@ akerr_ErrorContext *gamepad_handle_button_up(void *appstate, SDL_Event *event)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gamepad handle added.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Open a gamepad that has just been plugged in, and log its mapping.
|
||||
*
|
||||
* SDL delivers no button events from an unopened gamepad, so a device that
|
||||
* appears mid-session has to be opened here the way akgl_controller_open_gamepads
|
||||
* opens the ones present at startup. A gamepad SDL has already opened is logged
|
||||
* and left alone.
|
||||
*
|
||||
* The mapping is logged because a controller with no entry in the database
|
||||
* produces no button events at all, and that is otherwise indistinguishable
|
||||
* from a broken binding.
|
||||
*
|
||||
* Declared nowhere; see gamepad_handle_button_down.
|
||||
*
|
||||
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
|
||||
* @param event The SDL_EVENT_GAMEPAD_ADDED event. Required. The joystick id
|
||||
* is read out of the `gbutton` arm rather than `gdevice`, which
|
||||
* works because the two share their `which` field.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
|
||||
*
|
||||
* @note A failed `SDL_OpenGamepad` is logged rather than reported: this returns
|
||||
* success and the device stays silent.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
||||
{
|
||||
SDL_JoystickID which;
|
||||
SDL_Gamepad *gamepad = NULL;
|
||||
char *mapping = NULL;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
|
||||
|
||||
which = event->gbutton.which;
|
||||
gamepad = SDL_GetGamepadFromID(which);
|
||||
|
||||
|
||||
if (!gamepad) {
|
||||
SDL_Log("Gamepad #%u add, but not opened: %s", (unsigned int) which, SDL_GetError());
|
||||
gamepad = SDL_OpenGamepad(which);
|
||||
SDL_Log("Gamepad #%u opened: %s", (unsigned int) which, SDL_GetError());
|
||||
mapping = SDL_GetGamepadMapping(gamepad);
|
||||
if ( mapping == NULL ) {
|
||||
SDL_Log("Gamepad #%u has no mapping!", (unsigned int) which);
|
||||
SDL_Log("Gamepad #%u has no mapping!", (unsigned int) which);
|
||||
} else if ( mapping != NULL ) {
|
||||
SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping);
|
||||
SDL_free(mapping);
|
||||
SDL_Log("Gamepad #%u mapping : %s", (unsigned int) which, mapping);
|
||||
SDL_free(mapping);
|
||||
}
|
||||
} else {
|
||||
SDL_Log("Gamepad #%u ('%s') added", (unsigned int) which, SDL_GetGamepadName(gamepad));
|
||||
@@ -253,11 +488,19 @@ akerr_ErrorContext *gamepad_handle_added(void *appstate, SDL_Event *event)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gamepad handle removed.
|
||||
* @param appstate Application state supplied by SDL.
|
||||
* @param event SDL input event to process.
|
||||
* @brief Close a gamepad that has been unplugged.
|
||||
*
|
||||
* An unplugged device SDL still has open is a leaked handle, so this closes it.
|
||||
* A removal for a device that was never opened is logged and otherwise ignored.
|
||||
* Any control map still holding that `jsid` simply stops matching -- the map is
|
||||
* not torn down.
|
||||
*
|
||||
* Declared nowhere; see gamepad_handle_button_down.
|
||||
*
|
||||
* @param appstate Passed through from SDL. Required as a non-`NULL` token only.
|
||||
* @param event The SDL_EVENT_GAMEPAD_REMOVED event. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p appstate or @p event is `NULL`.
|
||||
*/
|
||||
akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
|
||||
{
|
||||
@@ -270,7 +513,7 @@ akerr_ErrorContext *gamepad_handle_removed(void *appstate, SDL_Event *event)
|
||||
|
||||
which = event->gbutton.which;
|
||||
gamepad = SDL_GetGamepadFromID(which);
|
||||
|
||||
|
||||
if (gamepad) {
|
||||
SDL_CloseGamepad(gamepad); /* the joystick was unplugged. */
|
||||
}
|
||||
@@ -292,14 +535,14 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_pushmap(int controlmapid, akg
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, char *actorname, int kbid, int jsid)
|
||||
{
|
||||
akgl_ControlMap *controlmap;
|
||||
akgl_Control control;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
// set up the control map
|
||||
@@ -308,12 +551,12 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
||||
controlmap = &GAME_ControlMaps[controlmapid];
|
||||
controlmap->kbid = kbid;
|
||||
controlmap->jsid = jsid;
|
||||
|
||||
|
||||
controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, controlmap->target, AKGL_ERR_REGISTRY, "Actor %s not found in registry", actorname);
|
||||
|
||||
// ---- KEYBOARD CONTROLS ----
|
||||
|
||||
|
||||
// Move down
|
||||
control.key = SDLK_DOWN;
|
||||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||
@@ -329,7 +572,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
||||
control.handler_on = &akgl_Actor_cmhf_up_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_up_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
|
||||
// Move left
|
||||
control.key = SDLK_LEFT;
|
||||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||
@@ -337,7 +580,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
||||
control.handler_on = &akgl_Actor_cmhf_left_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_left_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
|
||||
// Move right
|
||||
control.key = SDLK_RIGHT;
|
||||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||
@@ -363,7 +606,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
||||
control.handler_on = &akgl_Actor_cmhf_up_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_up_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
|
||||
// Move left
|
||||
control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
|
||||
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||||
@@ -371,7 +614,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
||||
control.handler_on = &akgl_Actor_cmhf_left_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_left_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
|
||||
// Move right
|
||||
control.button = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
|
||||
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||||
@@ -379,10 +622,55 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_default(int controlmapid, cha
|
||||
control.handler_on = &akgl_Actor_cmhf_right_on;
|
||||
control.handler_off = &akgl_Actor_cmhf_right_off;
|
||||
CATCH(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
|
||||
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_key(int *keycode, bool *available)
|
||||
{
|
||||
akgl_Keystroke keystroke;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, keycode, AKERR_NULLPOINTER, "NULL keycode destination");
|
||||
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
|
||||
|
||||
// An empty buffer is the ordinary case, not a failure: the caller is asking
|
||||
// whether a key is waiting, and the answer is no.
|
||||
*keycode = 0;
|
||||
*available = false;
|
||||
while ( keybuffer_take(&keystroke) == true ) {
|
||||
if ( keystroke.key == 0 ) {
|
||||
// Composed text with no key behind it. This form has no way to
|
||||
// report it, so it is dropped rather than handed back as key 0.
|
||||
continue;
|
||||
}
|
||||
*keycode = (int)keystroke.key;
|
||||
*available = true;
|
||||
break;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL keystroke destination");
|
||||
FAIL_ZERO_RETURN(errctx, available, AKERR_NULLPOINTER, "NULL availability destination");
|
||||
|
||||
memset(dest, 0x00, sizeof(akgl_Keystroke));
|
||||
*available = keybuffer_take(dest);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_flush_keys(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
keybuffer_head = 0;
|
||||
keybuffer_count = 0;
|
||||
keybuffer_awaiting_text = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
575
src/draw.c
575
src/draw.c
@@ -6,14 +6,34 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_mixer/SDL_mixer.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
|
||||
/** @brief One horizontal run of pixels the flood fill has still to examine. */
|
||||
typedef struct {
|
||||
int x1; /**< Leftmost column of the run, inclusive. */
|
||||
int x2; /**< Rightmost column of the run, inclusive. */
|
||||
int y; /**< The row the run is on. */
|
||||
} FloodSpan;
|
||||
|
||||
/*
|
||||
* The flood fill's working stack. File scope and fixed size rather than a local
|
||||
* array because AKGL_DRAW_MAX_FLOOD_SPANS spans is 48 KB, which does not belong
|
||||
* on the stack of a function a game may call every frame. The consequence is
|
||||
* that akgl_draw_flood_fill is not reentrant -- it is a single-threaded
|
||||
* immediate-mode operation on a single render target, and so is everything else
|
||||
* that touches an SDL_Renderer.
|
||||
*/
|
||||
static FloodSpan floodspans[AKGL_DRAW_MAX_FLOOD_SPANS];
|
||||
|
||||
/* Draw a Gimpish background pattern to show transparency in the image */
|
||||
void akgl_draw_background(int w, int h)
|
||||
{
|
||||
SDL_Color col[2] = {
|
||||
{ 0x66, 0x66, 0x66, 0xff },
|
||||
{ 0x99, 0x99, 0x99, 0xff },
|
||||
{ 0x66, 0x66, 0x66, 0xff },
|
||||
{ 0x99, 0x99, 0x99, 0xff },
|
||||
};
|
||||
int i, x, y;
|
||||
SDL_FRect rect;
|
||||
@@ -22,14 +42,549 @@ void akgl_draw_background(int w, int h)
|
||||
rect.w = (float)dx;
|
||||
rect.h = (float)dy;
|
||||
for (y = 0; y < h; y += dy) {
|
||||
for (x = 0; x < w; x += dx) {
|
||||
/* use an 8x8 checkerboard pattern */
|
||||
i = (((x ^ y) >> 3) & 1);
|
||||
SDL_SetRenderDrawColor(renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
|
||||
for (x = 0; x < w; x += dx) {
|
||||
/* use an 8x8 checkerboard pattern */
|
||||
i = (((x ^ y) >> 3) & 1);
|
||||
SDL_SetRenderDrawColor(renderer->sdl_renderer, col[i].r, col[i].g, col[i].b, col[i].a);
|
||||
|
||||
rect.x = (float)x;
|
||||
rect.y = (float)y;
|
||||
SDL_RenderFillRect(renderer->sdl_renderer, &rect);
|
||||
}
|
||||
rect.x = (float)x;
|
||||
rect.y = (float)y;
|
||||
SDL_RenderFillRect(renderer->sdl_renderer, &rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remember the renderer's draw color and replace it with @p color.
|
||||
*
|
||||
* @p previous is written before anything that can fail, so a caller may restore
|
||||
* it unconditionally from a CLEANUP block.
|
||||
*
|
||||
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`;
|
||||
* every caller has already checked both.
|
||||
* @param color The colour to install.
|
||||
* @param previous Receives the colour that was in place. Assumed non-`NULL`.
|
||||
* Pre-filled with opaque black, so it is safe to restore from
|
||||
* even if the query below fails.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL If the current draw colour cannot be read or the new one
|
||||
* cannot be set. The message carries `SDL_GetError()`.
|
||||
*/
|
||||
static akerr_ErrorContext *push_draw_color(akgl_RenderBackend *self, SDL_Color color, SDL_Color *previous)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
previous->r = 0x00;
|
||||
previous->g = 0x00;
|
||||
previous->b = 0x00;
|
||||
previous->a = SDL_ALPHA_OPAQUE;
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_GetRenderDrawColor(self->sdl_renderer, &previous->r, &previous->g, &previous->b, &previous->a),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetRenderDrawColor(self->sdl_renderer, color.r, color.g, color.b, color.a),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Put back the draw color push_draw_color() recorded.
|
||||
*
|
||||
* Called from `CLEANUP` blocks under `IGNORE()`, so its error is logged rather
|
||||
* than propagated -- failing to restore a colour must not mask the failure the
|
||||
* cleanup is unwinding from.
|
||||
*
|
||||
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`.
|
||||
* @param previous The colour push_draw_color() recorded. Assumed non-`NULL`.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKGL_ERR_SDL If the draw colour cannot be set.
|
||||
*/
|
||||
static akerr_ErrorContext *pop_draw_color(akgl_RenderBackend *self, SDL_Color *previous)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetRenderDrawColor(self->sdl_renderer, previous->r, previous->g, previous->b, previous->a),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fill the four-connected region of @p oldpixel around a seed pixel.
|
||||
*
|
||||
* A scanline fill: each entry on the stack is a run of pixels on one row that
|
||||
* still has to be examined. Finding a matching pixel expands it to the whole
|
||||
* run it belongs to, fills that run, and pushes the rows above and below.
|
||||
* Filled pixels no longer match @p oldpixel, which is what terminates it.
|
||||
*
|
||||
* @p surface must be SDL_PIXELFORMAT_RGBA32; the fill compares and writes whole
|
||||
* 32-bit words rather than going through SDL_ReadSurfacePixel per pixel.
|
||||
*
|
||||
* @p dirty is set to the bounding box of everything written, so the caller can
|
||||
* put back only the pixels that changed.
|
||||
*
|
||||
* Running out of stack leaves the region partially filled and reports
|
||||
* AKERR_OUTOFBOUNDS. There is no way to unwind a partial fill short of keeping
|
||||
* a copy of the whole surface, and the caller asked for a bounded operation.
|
||||
*
|
||||
* @param surface The pixels to fill, in SDL_PIXELFORMAT_RGBA32. Assumed
|
||||
* non-`NULL` and locked-or-lockless; the caller converts.
|
||||
* @param x Seed column. Assumed inside the surface -- akgl_draw_flood_fill
|
||||
* has already range-checked it.
|
||||
* @param y Seed row, likewise.
|
||||
* @param oldpixel The packed pixel value the region is made of. Everything else
|
||||
* is a boundary.
|
||||
* @param newpixel The packed pixel value to write. Must differ from @p oldpixel:
|
||||
* if they are equal the fill never terminates making progress,
|
||||
* which is why the caller checks that case first.
|
||||
* @param dirty Receives the bounding box of everything written, so the caller
|
||||
* can put back only the pixels that changed. Assumed non-`NULL`.
|
||||
* Left describing an empty box if nothing matched.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_OUTOFBOUNDS If the region needs more than
|
||||
* #AKGL_DRAW_MAX_FLOOD_SPANS pending spans. @p dirty is then not written
|
||||
* and the surface is partially filled.
|
||||
*/
|
||||
static akerr_ErrorContext *flood_region(SDL_Surface *surface, int x, int y, uint32_t oldpixel, uint32_t newpixel, SDL_Rect *dirty)
|
||||
{
|
||||
uint32_t *pixels = (uint32_t *)surface->pixels;
|
||||
int pitch = surface->pitch / (int)sizeof(uint32_t);
|
||||
int count = 0;
|
||||
int col = 0;
|
||||
int left = 0;
|
||||
int right = 0;
|
||||
int i = 0;
|
||||
int minx = surface->w;
|
||||
int miny = surface->h;
|
||||
int maxx = -1;
|
||||
int maxy = -1;
|
||||
FloodSpan span;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
floodspans[0].x1 = x;
|
||||
floodspans[0].x2 = x;
|
||||
floodspans[0].y = y;
|
||||
count = 1;
|
||||
|
||||
while ( count > 0 ) {
|
||||
count -= 1;
|
||||
span = floodspans[count];
|
||||
col = span.x1;
|
||||
while ( col <= span.x2 ) {
|
||||
if ( pixels[(span.y * pitch) + col] != oldpixel ) {
|
||||
col += 1;
|
||||
continue;
|
||||
}
|
||||
left = col;
|
||||
while ( left > 0 && pixels[(span.y * pitch) + (left - 1)] == oldpixel ) {
|
||||
left -= 1;
|
||||
}
|
||||
right = col;
|
||||
while ( right < (surface->w - 1) && pixels[(span.y * pitch) + (right + 1)] == oldpixel ) {
|
||||
right += 1;
|
||||
}
|
||||
for ( i = left; i <= right; i++ ) {
|
||||
pixels[(span.y * pitch) + i] = newpixel;
|
||||
}
|
||||
if ( left < minx ) {
|
||||
minx = left;
|
||||
}
|
||||
if ( right > maxx ) {
|
||||
maxx = right;
|
||||
}
|
||||
if ( span.y < miny ) {
|
||||
miny = span.y;
|
||||
}
|
||||
if ( span.y > maxy ) {
|
||||
maxy = span.y;
|
||||
}
|
||||
// Two pushes per run, so the check is for room for both.
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((count + 2) > AKGL_DRAW_MAX_FLOOD_SPANS),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Region needs more than %d pending spans; it is partially filled",
|
||||
AKGL_DRAW_MAX_FLOOD_SPANS);
|
||||
if ( span.y > 0 ) {
|
||||
floodspans[count].x1 = left;
|
||||
floodspans[count].x2 = right;
|
||||
floodspans[count].y = span.y - 1;
|
||||
count += 1;
|
||||
}
|
||||
if ( span.y < (surface->h - 1) ) {
|
||||
floodspans[count].x1 = left;
|
||||
floodspans[count].x2 = right;
|
||||
floodspans[count].y = span.y + 1;
|
||||
count += 1;
|
||||
}
|
||||
col = right + 1;
|
||||
}
|
||||
}
|
||||
|
||||
dirty->x = minx;
|
||||
dirty->y = miny;
|
||||
dirty->w = (maxx - minx) + 1;
|
||||
dirty->h = (maxy - miny) + 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_point(akgl_RenderBackend *self, float32_t x, float32_t y, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderPoint(self->sdl_renderer, x, y),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_line(akgl_RenderBackend *self, float32_t x1, float32_t y1, float32_t x2, float32_t y2, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderLine(self->sdl_renderer, x1, y1, x2, y2),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderRect(self->sdl_renderer, rect),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rect(akgl_RenderBackend *self, SDL_FRect *rect, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "rect");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderFillRect(self->sdl_renderer, rect),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_circle(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, SDL_Color color)
|
||||
{
|
||||
SDL_Color previous;
|
||||
SDL_FPoint octants[8];
|
||||
int centerx = 0;
|
||||
int centery = 0;
|
||||
int r = 0;
|
||||
int offsetx = 0;
|
||||
int offsety = 0;
|
||||
int decision = 0;
|
||||
bool plotted = true;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_NONZERO_RETURN(errctx, (radius < 0), AKERR_OUTOFBOUNDS, "Negative radius %f", radius);
|
||||
|
||||
centerx = (int)SDL_lroundf(x);
|
||||
centery = (int)SDL_lroundf(y);
|
||||
r = (int)SDL_lroundf(radius);
|
||||
offsety = r;
|
||||
// The midpoint decision variable, started so the first step chooses between
|
||||
// (0, r) and (1, r-1) correctly.
|
||||
decision = 1 - r;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||
while ( offsety >= offsetx ) {
|
||||
// Eight-way symmetry: one computed point in the second octant gives
|
||||
// the seven others by reflection.
|
||||
octants[0].x = (float)(centerx + offsetx);
|
||||
octants[0].y = (float)(centery + offsety);
|
||||
octants[1].x = (float)(centerx - offsetx);
|
||||
octants[1].y = (float)(centery + offsety);
|
||||
octants[2].x = (float)(centerx + offsetx);
|
||||
octants[2].y = (float)(centery - offsety);
|
||||
octants[3].x = (float)(centerx - offsetx);
|
||||
octants[3].y = (float)(centery - offsety);
|
||||
octants[4].x = (float)(centerx + offsety);
|
||||
octants[4].y = (float)(centery + offsetx);
|
||||
octants[5].x = (float)(centerx - offsety);
|
||||
octants[5].y = (float)(centery + offsetx);
|
||||
octants[6].x = (float)(centerx + offsety);
|
||||
octants[6].y = (float)(centery - offsetx);
|
||||
octants[7].x = (float)(centerx - offsety);
|
||||
octants[7].y = (float)(centery - offsetx);
|
||||
// A CATCH here would break this loop rather than leave the function,
|
||||
// so failure is recorded and reported once the loop is done.
|
||||
if ( !SDL_RenderPoints(self->sdl_renderer, octants, 8) ) {
|
||||
plotted = false;
|
||||
}
|
||||
offsetx += 1;
|
||||
if ( decision < 0 ) {
|
||||
decision += (2 * offsetx) + 1;
|
||||
} else {
|
||||
offsety -= 1;
|
||||
decision += 2 * (offsetx - offsety) + 1;
|
||||
}
|
||||
}
|
||||
FAIL_ZERO_BREAK(errctx, plotted, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
} CLEANUP {
|
||||
IGNORE(pop_draw_color(self, &previous));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_flood_fill(akgl_RenderBackend *self, int x, int y, SDL_Color color)
|
||||
{
|
||||
SDL_Surface *target = NULL;
|
||||
SDL_Surface *rgba = NULL;
|
||||
SDL_Texture *patch = NULL;
|
||||
// Only written by a successful flood_region(), and only read after one, but
|
||||
// the paths in between are far enough apart that the compiler cannot see it.
|
||||
SDL_Rect dirty = { 0, 0, 0, 0 };
|
||||
SDL_FRect src;
|
||||
SDL_FRect dest;
|
||||
uint32_t *pixels = NULL;
|
||||
uint32_t oldpixel = 0;
|
||||
uint32_t newpixel = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
((x < 0) || (y < 0) || (x >= width) || (y >= height)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Seed pixel %d,%d is outside the %dx%d render target",
|
||||
x, y, width, height);
|
||||
|
||||
target = SDL_RenderReadPixels(self->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, target, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
// The fill works on 32-bit words, so the layout has to be known rather
|
||||
// than whatever the render target happens to use.
|
||||
rgba = SDL_ConvertSurface(target, SDL_PIXELFORMAT_RGBA32);
|
||||
FAIL_ZERO_BREAK(errctx, rgba, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
pixels = (uint32_t *)rgba->pixels;
|
||||
oldpixel = pixels[(y * (rgba->pitch / (int)sizeof(uint32_t))) + x];
|
||||
newpixel = SDL_MapSurfaceRGBA(rgba, color.r, color.g, color.b, color.a);
|
||||
if ( oldpixel == newpixel ) {
|
||||
// Already the requested color. Walking it would compare filled
|
||||
// pixels against themselves and find nothing, so say so up front.
|
||||
SUCCEED_BREAK(errctx);
|
||||
}
|
||||
CATCH(errctx, flood_region(rgba, x, y, oldpixel, newpixel, &dirty));
|
||||
|
||||
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, rgba);
|
||||
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
// Replace rather than blend: this is a framebuffer operation, and the
|
||||
// pixels being put back are the ones that were just read out of it.
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
|
||||
// Only the bounding box of what changed goes back to the target.
|
||||
src.x = (float)dirty.x;
|
||||
src.y = (float)dirty.y;
|
||||
src.w = (float)dirty.w;
|
||||
src.h = (float)dirty.h;
|
||||
dest = src;
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderTexture(self->sdl_renderer, patch, &src, &dest),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
if ( patch != NULL ) {
|
||||
SDL_DestroyTexture(patch);
|
||||
}
|
||||
if ( rgba != NULL ) {
|
||||
SDL_DestroySurface(rgba);
|
||||
}
|
||||
if ( target != NULL ) {
|
||||
SDL_DestroySurface(target);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *self, SDL_Rect *src, SDL_Surface **dest)
|
||||
{
|
||||
SDL_Surface *saved = NULL;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||
ATTEMPT {
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
((src->w <= 0) || (src->h <= 0)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Region %dx%d has no area",
|
||||
src->w, src->h);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetCurrentRenderOutputSize(self->sdl_renderer, &width, &height),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
// SDL clips a read to the target and hands back a smaller surface than
|
||||
// was asked for, which a caller pasting it back would not notice.
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
((src->x < 0) || (src->y < 0) ||
|
||||
((src->x + src->w) > width) || ((src->y + src->h) > height)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Region %d,%d %dx%d does not fit inside the %dx%d render target",
|
||||
src->x, src->y, src->w, src->h, width, height);
|
||||
|
||||
saved = SDL_RenderReadPixels(self->sdl_renderer, src);
|
||||
FAIL_ZERO_BREAK(errctx, saved, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
if ( *dest == NULL ) {
|
||||
*dest = saved;
|
||||
// Ownership has moved to the caller; CLEANUP must not free it.
|
||||
saved = NULL;
|
||||
} else {
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
(((*dest)->w != src->w) || ((*dest)->h != src->h)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Destination surface is %dx%d, region is %dx%d",
|
||||
(*dest)->w, (*dest)->h, src->w, src->h);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_SetSurfaceBlendMode(saved, SDL_BLENDMODE_NONE),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_BlitSurface(saved, NULL, *dest, NULL),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
}
|
||||
} CLEANUP {
|
||||
if ( saved != NULL ) {
|
||||
SDL_DestroySurface(saved);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y)
|
||||
{
|
||||
SDL_Texture *patch = NULL;
|
||||
SDL_FRect dest;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, src, AKERR_NULLPOINTER, "src");
|
||||
ATTEMPT {
|
||||
patch = SDL_CreateTextureFromSurface(self->sdl_renderer, src);
|
||||
FAIL_ZERO_BREAK(errctx, patch, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
// Replace what is on the target, the way GSHAPE does by default.
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_SetTextureBlendMode(patch, SDL_BLENDMODE_NONE),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
dest.x = x;
|
||||
dest.y = y;
|
||||
dest.w = (float)src->w;
|
||||
dest.h = (float)src->h;
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_RenderTexture(self->sdl_renderer, patch, NULL, &dest),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
} CLEANUP {
|
||||
if ( patch != NULL ) {
|
||||
SDL_DestroyTexture(patch);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
24
src/error.c
Normal file
24
src/error.c
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @file error.c
|
||||
* @brief Implements the error subsystem: claims and names the libakgl status band.
|
||||
*/
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
|
||||
akerr_ErrorContext *akgl_error_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
// Claim the whole band before naming anything in it: libakerror refuses a
|
||||
// name for a status we do not own. Any collision propagates to the caller
|
||||
// -- another component owning part of our range is an initialization
|
||||
// failure, not a warning.
|
||||
PASS(errctx, akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, AKGL_ERR_OWNER));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_SDL, "SDL Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_REGISTRY, "Registry Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_HEAP, "Heap Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_BEHAVIOR, "Behavior Error"));
|
||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt"));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
355
src/game.c
355
src/game.c
@@ -21,6 +21,7 @@
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/iterator.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/SDL_GameControllerDB.h>
|
||||
|
||||
SDL_Window *window = NULL;
|
||||
@@ -42,6 +43,86 @@ MIX_Mixer *akgl_mixer = NULL;
|
||||
MIX_Track *akgl_tracks[AKGL_GAME_AUDIO_MAX_TRACKS];
|
||||
akgl_Game game;
|
||||
|
||||
static akerr_ErrorContext *write_exact(const void *ptr, size_t size, size_t nmemb, FILE *fp)
|
||||
{
|
||||
size_t transferred;
|
||||
|
||||
return aksl_fwrite(ptr, size, nmemb, fp, &transferred);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Widest name field any of the save tables writes.
|
||||
*
|
||||
* The spritesheet table is the widest at #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH;
|
||||
* the other three are name lengths half that or less.
|
||||
*/
|
||||
#define AKGL_GAME_SAVE_MAX_NAME_FIELD AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH
|
||||
|
||||
/**
|
||||
* @brief Fail the build if a table ever declares a field wider than the staging buffer.
|
||||
*
|
||||
* A negative array size is a compile error, so widening one of these constants
|
||||
* without widening #AKGL_GAME_SAVE_MAX_NAME_FIELD cannot get past the compiler
|
||||
* and turn back into the overread this buffer exists to prevent.
|
||||
*/
|
||||
typedef char akgl_game_save_name_field_fits[
|
||||
((AKGL_ACTOR_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_SPRITE_MAX_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD) &&
|
||||
(AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH <= AKGL_GAME_SAVE_MAX_NAME_FIELD)) ? 1 : -1];
|
||||
|
||||
/**
|
||||
* @brief Write a registry key as a fixed-width, zero-padded field.
|
||||
*
|
||||
* The name tables are read back without a length prefix, so every entry has to
|
||||
* occupy exactly @p width bytes. Writing @p width bytes *from the key itself*
|
||||
* is what the four iterators used to do, and the key is an SDL allocation sized
|
||||
* to the name -- 40 bytes for `benchactor0`. That read past the end of it on
|
||||
* every entry, and put whatever was next to it in the heap into the save file:
|
||||
* up to half a kilobyte of this process's memory per registered object, in a
|
||||
* file a player might send to somebody else.
|
||||
*
|
||||
* Staging through a zeroed buffer costs one `memset` per entry and makes the
|
||||
* padding deterministic, which the reader wanted anyway.
|
||||
*
|
||||
* @param name The registry key. Required, and NUL-terminated -- it comes from
|
||||
* SDL's property table, which stores C strings.
|
||||
* @param width Field width in bytes, including the padding. Required to be at
|
||||
* most #AKGL_GAME_SAVE_MAX_NAME_FIELD; the compile-time check
|
||||
* above is what keeps that true for the four call sites.
|
||||
* @param fp Open output stream. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER If @p name or @p fp is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If @p width exceeds the staging buffer.
|
||||
*/
|
||||
static akerr_ErrorContext *write_name_field(const char *name, size_t width, FILE *fp)
|
||||
{
|
||||
char field[AKGL_GAME_SAVE_MAX_NAME_FIELD];
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL name");
|
||||
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
FAIL_NONZERO_RETURN(
|
||||
e,
|
||||
(width > sizeof(field)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Name field width %zu exceeds the staging buffer (%zu)",
|
||||
width,
|
||||
sizeof(field));
|
||||
|
||||
memset(&field, 0x00, sizeof(field));
|
||||
strncpy((char *)&field, name, (width - 1));
|
||||
PASS(e, write_exact((char *)&field, 1, width, fp));
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *read_exact(void *ptr, size_t size, size_t nmemb, FILE *fp)
|
||||
{
|
||||
size_t transferred;
|
||||
|
||||
return aksl_fread(ptr, size, nmemb, fp, &transferred);
|
||||
}
|
||||
|
||||
void akgl_game_lowfps(void)
|
||||
{
|
||||
SDL_Log("Low FPS! %d", game.fps);
|
||||
@@ -52,9 +133,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
|
||||
{
|
||||
int screenwidth = 0;
|
||||
int screenheight = 0;
|
||||
|
||||
|
||||
int i = 0;
|
||||
PREPARE_ERROR(e);
|
||||
// First, before anything that can raise: everything below reports through
|
||||
// AKGL_ERR_* codes, and a code raised before its name is registered prints
|
||||
// as "Unknown Error" in the stack trace the caller is left holding.
|
||||
PASS(e, akgl_error_init());
|
||||
strncpy((char *)&game.libversion, AKGL_VERSION, 32);
|
||||
game.gameStartTime = SDL_GetTicksNS();
|
||||
game.lastIterTime = game.gameStartTime;
|
||||
@@ -75,13 +160,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
|
||||
PASS(e, akgl_registry_init_music());
|
||||
PASS(e, akgl_registry_init_properties());
|
||||
PASS(e, akgl_registry_init_actor_state_strings());
|
||||
|
||||
|
||||
SDL_SetAppMetadata(game.name, game.version, game.uri);
|
||||
|
||||
for ( i = 0 ; i < AKGL_MAX_CONTROL_MAPS; i++ ) {
|
||||
memset(&GAME_ControlMaps[i], 0x00, sizeof(akgl_ControlMap));
|
||||
}
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
e,
|
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD | SDL_INIT_AUDIO),
|
||||
@@ -123,7 +208,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_init()
|
||||
renderer = &_akgl_renderer;
|
||||
physics = &_akgl_physics;
|
||||
gamemap = &_akgl_gamemap;
|
||||
|
||||
|
||||
PASS(e, akgl_game_state_unlock());
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
@@ -171,10 +256,24 @@ void akgl_game_updateFPS()
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Serializes an actor name-to-pointer entry.
|
||||
* @param userdata Open output stream supplied by the caller.
|
||||
* @param props Actor registry being iterated.
|
||||
* @param name Actor registry key to serialize.
|
||||
* @brief Write one actor's name and save-time address to the name table.
|
||||
*
|
||||
* An `SDL_EnumerateProperties` callback. The name is written at a fixed
|
||||
* #AKGL_ACTOR_MAX_NAME_LENGTH bytes so the reader can walk the table without a
|
||||
* length prefix, and the pointer is written raw -- it is not a pointer the
|
||||
* loader will ever dereference, only a key it maps back to the object that has
|
||||
* the same name next run.
|
||||
*
|
||||
* @param userdata The open output stream, as a `FILE *`. Required.
|
||||
* @param props #AKGL_REGISTRY_ACTOR, supplied by SDL.
|
||||
* @param name The actor's registry key. Read for
|
||||
* #AKGL_ACTOR_MAX_NAME_LENGTH bytes regardless of its actual
|
||||
* length, so a shorter key is read past its end.
|
||||
*
|
||||
* @warning Ends in `FINISH_NORETURN`: an unhandled error here logs a stack trace
|
||||
* and then calls libakerror's unhandled-error handler, which by default
|
||||
* **exits the process**. A failed write during a save terminates the
|
||||
* game rather than returning an error.
|
||||
*/
|
||||
void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
@@ -183,19 +282,26 @@ void akgl_game_save_actorname_iterator(void *userdata, SDL_PropertiesID props, c
|
||||
PREPARE_ERROR(e);
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, write_name_field(name, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||
actor = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(e, aksl_fwrite(&actor, 1, sizeof(akgl_Actor *), fp));
|
||||
CATCH(e, write_exact(&actor, 1, sizeof(akgl_Actor *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game save spritename iterator.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param props SDL property collection being iterated.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @brief Write one sprite's name and save-time address to the name table.
|
||||
*
|
||||
* As akgl_game_save_actorname_iterator, but the name field is
|
||||
* #AKGL_SPRITE_MAX_NAME_LENGTH wide.
|
||||
*
|
||||
* @param userdata The open output stream, as a `FILE *`. Required.
|
||||
* @param props #AKGL_REGISTRY_SPRITE, supplied by SDL.
|
||||
* @param name The sprite's registry key, written at fixed width.
|
||||
*
|
||||
* @warning Terminates the process on an unhandled error. See
|
||||
* akgl_game_save_actorname_iterator.
|
||||
*/
|
||||
void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
@@ -205,18 +311,29 @@ void akgl_game_save_spritename_iterator(void *userdata, SDL_PropertiesID props,
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
sprite = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite(&sprite, 1, sizeof(akgl_Sprite *), fp));
|
||||
CATCH(e, write_name_field(name, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact(&sprite, 1, sizeof(akgl_Sprite *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game save spritesheetname iterator.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param props SDL property collection being iterated.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @brief Write one spritesheet's name and save-time address to the name table.
|
||||
*
|
||||
* As akgl_game_save_actorname_iterator, but the name field is
|
||||
* #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH wide -- the widest of the four, since
|
||||
* a spritesheet is keyed by its resolved path.
|
||||
*
|
||||
* @param userdata The open output stream, as a `FILE *`. Required.
|
||||
* @param props #AKGL_REGISTRY_SPRITESHEET, supplied by SDL.
|
||||
* @param name The spritesheet's registry key, written at fixed width.
|
||||
*
|
||||
* @warning Terminates the process on an unhandled error. See
|
||||
* akgl_game_save_actorname_iterator.
|
||||
* @note This is the table the loader disagrees with: it reads every table at
|
||||
* #AKGL_ACTOR_MAX_NAME_LENGTH, so a save containing any spritesheet cannot
|
||||
* be read back. TODO.md, "Known and still open" item 7.
|
||||
*/
|
||||
void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
@@ -226,18 +343,25 @@ void akgl_game_save_spritesheetname_iterator(void *userdata, SDL_PropertiesID pr
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
spritesheet = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
|
||||
CATCH(e, write_name_field(name, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||
CATCH(e, write_exact(&spritesheet, 1, sizeof(akgl_SpriteSheet *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game save charactername iterator.
|
||||
* @param userdata Caller data supplied to the SDL property iterator.
|
||||
* @param props SDL property collection being iterated.
|
||||
* @param name Registry key or human-readable object name.
|
||||
* @brief Write one character's name and save-time address to the name table.
|
||||
*
|
||||
* As akgl_game_save_actorname_iterator, but the name field is
|
||||
* #AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH wide.
|
||||
*
|
||||
* @param userdata The open output stream, as a `FILE *`. Required.
|
||||
* @param props #AKGL_REGISTRY_CHARACTER, supplied by SDL.
|
||||
* @param name The character's registry key, written at fixed width.
|
||||
*
|
||||
* @warning Terminates the process on an unhandled error. See
|
||||
* akgl_game_save_actorname_iterator.
|
||||
*/
|
||||
void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID props, const char *name)
|
||||
{
|
||||
@@ -247,18 +371,31 @@ void akgl_game_save_charactername_iterator(void *userdata, SDL_PropertiesID prop
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fp, AKERR_NULLPOINTER, "NULL file pointer");
|
||||
character = SDL_GetPointerProperty(props, name, NULL);
|
||||
CATCH(e, aksl_fwrite((char *)name, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite(&character, 1, sizeof(akgl_Character *), fp));
|
||||
CATCH(e, write_name_field(name, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact(&character, 1, sizeof(akgl_Character *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game save actors.
|
||||
* @param fp Open save-game stream.
|
||||
* @brief Write all four name-to-address tables, each with its terminator.
|
||||
*
|
||||
* Actors, sprites, spritesheets, characters, in that order -- which is the order
|
||||
* akgl_game_load reads them back in. Each table is the registry enumerated one
|
||||
* entry at a time, followed by a zeroed name field and a zeroed pointer that the
|
||||
* reader stops on.
|
||||
*
|
||||
* @param fp The open save-game stream. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p fp is `NULL`.
|
||||
* @throws AKERR_IO On a stream error or a short write while emitting a
|
||||
* terminator.
|
||||
*
|
||||
* @note Only the terminators are written through the error-reporting path. The
|
||||
* entries themselves go through `SDL_EnumerateProperties`, whose callbacks
|
||||
* cannot report failure upward and instead terminate the process -- so a
|
||||
* write error mid-table never reaches this function's return value.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
|
||||
{
|
||||
@@ -281,29 +418,29 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save_actors(FILE *fp)
|
||||
AKGL_REGISTRY_ACTOR,
|
||||
&akgl_game_save_actorname_iterator,
|
||||
(void *)fp);
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_ACTOR_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Actor *), fp));
|
||||
// write the sprite name pointer table
|
||||
SDL_EnumerateProperties(
|
||||
AKGL_REGISTRY_SPRITE,
|
||||
&akgl_game_save_spritename_iterator,
|
||||
(void *)fp);
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Sprite *), fp));
|
||||
// write the spritesheet name pointer table
|
||||
SDL_EnumerateProperties(
|
||||
AKGL_REGISTRY_SPRITESHEET,
|
||||
&akgl_game_save_spritesheetname_iterator,
|
||||
(void *)fp);
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH, fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_SpriteSheet *), fp));
|
||||
// write the character name pointer table
|
||||
SDL_EnumerateProperties(
|
||||
AKGL_REGISTRY_CHARACTER,
|
||||
&akgl_game_save_charactername_iterator,
|
||||
(void *)fp);
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
||||
CATCH(e, aksl_fwrite((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, AKGL_SPRITE_MAX_CHARACTER_NAME_LENGTH, fp));
|
||||
CATCH(e, write_exact((void *)&nullbuf, 1, sizeof(akgl_Character *), fp));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
@@ -318,7 +455,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(e, fpath, AKERR_NULLPOINTER, "NULL file path");
|
||||
CATCH(e, aksl_fopen(fpath, "wb", &fp));
|
||||
CATCH(e, aksl_fwrite(&game, 1, sizeof(akgl_Game), fp));
|
||||
CATCH(e, write_exact(&game, 1, sizeof(akgl_Game), fp));
|
||||
CATCH(e, akgl_game_save_actors(fp));
|
||||
} CLEANUP {
|
||||
// CLEANUP must precede PROCESS: with the two transposed, the fclose
|
||||
@@ -333,14 +470,40 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_save(char *fpath)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game load objectnamemap.
|
||||
* @param fp Open save-game stream.
|
||||
* @param map Tilemap to inspect, draw, or modify.
|
||||
* @param namelength Fixed serialized name-field length.
|
||||
* @param ptrlength Serialized pointer-field length.
|
||||
* @param registry SDL property registry being queried.
|
||||
* @brief Read one name table back, building an old-address to current-object map.
|
||||
*
|
||||
* This is the mechanism that makes a save file portable across runs. Objects are
|
||||
* serialized carrying the addresses they had at save time; those addresses mean
|
||||
* nothing now, but the table says which *name* lived at each one, and the name
|
||||
* still resolves through the registry. So each entry becomes
|
||||
* `"0x7f..." -> current object`, and a deserialized pointer can be translated by
|
||||
* looking up its printed form.
|
||||
*
|
||||
* The keys are the old pointers rendered with `%p`, because SDL property sets
|
||||
* take string keys only.
|
||||
*
|
||||
* Reading stops at the terminator -- a zeroed name and a zeroed pointer.
|
||||
*
|
||||
* @param fp The open save-game stream, positioned at the start of a
|
||||
* table. Required.
|
||||
* @param map The property set to fill in. Required, and created by the
|
||||
* caller.
|
||||
* @param namelength Width of the table's name field, which must match what the
|
||||
* corresponding save iterator wrote. It also sizes a
|
||||
* variable-length array on the stack.
|
||||
* @param ptrlength Width of the table's pointer field, i.e. `sizeof` the
|
||||
* pointer type that table holds.
|
||||
* @param registry The registry to resolve names against -- #AKGL_REGISTRY_ACTOR
|
||||
* for the actor table, and so on.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @throws AKERR_EOF If the stream ends before a terminator is found, which is
|
||||
* what a truncated save file looks like.
|
||||
* @throws AKERR_IO On a stream error.
|
||||
* @throws AKERR_NULLPOINTER If @p fp is `NULL`, by way of the read wrapper.
|
||||
*
|
||||
* @note A name in the table that is no longer in @p registry maps to `NULL`
|
||||
* rather than being reported: the entry is written with whatever
|
||||
* `SDL_GetPointerProperty` returned.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_PropertiesID map, int namelength, int ptrlength, SDL_PropertiesID registry)
|
||||
{
|
||||
@@ -358,27 +521,27 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
|
||||
// corrupt name table as a successful load.
|
||||
while ( done == false ) {
|
||||
ATTEMPT {
|
||||
CATCH(e, aksl_fread((void *)&objname, 1, namelength, fp));
|
||||
CATCH(e, aksl_fread((void *)&ptr, 1, ptrlength, fp));
|
||||
// End of the map
|
||||
if ( ptr == 0x00 && objname[0] == 0x00 ) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
// The map allows us to say "Object X has a reference to object Y at
|
||||
// address Z. The object they had at address Z was named A. Our current
|
||||
// instance of object named A is at address B. So we map address Z to
|
||||
// address B, so that we can reconnect function pointers on objects loaded
|
||||
// from the save game state."
|
||||
|
||||
// SDL_Properties objects can only use string keys, so we can't use the
|
||||
// old pointer as a key without first converting it to a string.
|
||||
CATCH(e, aksl_memset((void *)&ptrstring, 0x00, 32));
|
||||
snprintf((char *)&ptrstring, 32, "%p", ptr);
|
||||
SDL_SetPointerProperty(
|
||||
map,
|
||||
ptrstring,
|
||||
SDL_GetPointerProperty(registry, objname, NULL));
|
||||
CATCH(e, read_exact((void *)&objname, 1, namelength, fp));
|
||||
CATCH(e, read_exact((void *)&ptr, 1, ptrlength, fp));
|
||||
// End of the map
|
||||
if ( ptr == 0x00 && objname[0] == 0x00 ) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
// The map allows us to say "Object X has a reference to object Y at
|
||||
// address Z. The object they had at address Z was named A. Our current
|
||||
// instance of object named A is at address B. So we map address Z to
|
||||
// address B, so that we can reconnect function pointers on objects loaded
|
||||
// from the save game state."
|
||||
|
||||
// SDL_Properties objects can only use string keys, so we can't use the
|
||||
// old pointer as a key without first converting it to a string.
|
||||
CATCH(e, aksl_memset((void *)&ptrstring, 0x00, 32));
|
||||
snprintf((char *)&ptrstring, 32, "%p", ptr);
|
||||
SDL_SetPointerProperty(
|
||||
map,
|
||||
ptrstring,
|
||||
SDL_GetPointerProperty(registry, objname, NULL));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
@@ -387,14 +550,24 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_objectnamemap(FILE *fp, SDL_Pr
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Game load versioncmp.
|
||||
* @param versiontype Description of the version being compared.
|
||||
* @param newversion Version read from the save data.
|
||||
* @param curversion Version supported by the running library.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_API When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_VALUE When the corresponding validation or operation fails.
|
||||
* @brief Refuse a save file unless its version matches the running one exactly.
|
||||
*
|
||||
* Both strings are parsed as semver and compared with `"="` -- exact equality,
|
||||
* not compatibility. That is deliberate and strict: a save file is a raw memory
|
||||
* image of `akgl_Game` and the object tables, so any change to a struct layout
|
||||
* invalidates it, and semver has no way to say "the layout did not move".
|
||||
*
|
||||
* @param versiontype What is being compared -- `"library"` or `"game"`. Used
|
||||
* only to build the message, but required, since a bare
|
||||
* "incompatible version" error would not say which.
|
||||
* @param newversion The version read out of the save file. Required.
|
||||
* @param curversion The version of the running program or library. Required.
|
||||
* @return `NULL` when the two match, otherwise an error context owned by the
|
||||
* caller.
|
||||
* @throws AKERR_NULLPOINTER If any of the three arguments is `NULL`.
|
||||
* @throws AKERR_VALUE If either string is not valid semver. The message says
|
||||
* which side it came from.
|
||||
* @throws AKERR_API If both parse but are not equal. The message quotes both.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype, char *newversion, char *curversion)
|
||||
{
|
||||
@@ -404,9 +577,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load_versioncmp(char *versiontype,
|
||||
FAIL_ZERO_RETURN(e, versiontype, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, curversion, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, newversion, AKERR_NULLPOINTER, "NULL argument");
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
// Check save game library version
|
||||
// Check save game library version
|
||||
FAIL_NONZERO_BREAK(
|
||||
e,
|
||||
semver_parse((const char *)curversion, ¤t_version),
|
||||
@@ -445,13 +618,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
|
||||
SDL_PropertiesID spritesheetmap;
|
||||
SDL_PropertiesID charactermap;
|
||||
FILE *fp = NULL;
|
||||
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, fpath, AKERR_NULLPOINTER, "NULL file path");
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, aksl_fopen(fpath, "rb", &fp));
|
||||
CATCH(e, aksl_fread((void *)&savegame, 1, sizeof(akgl_Game), fp));
|
||||
CATCH(e, read_exact((void *)&savegame, 1, sizeof(akgl_Game), fp));
|
||||
CATCH(e, akgl_game_load_versioncmp("library", (char *)&savegame.libversion, (char *)AKGL_VERSION));
|
||||
CATCH(e, akgl_game_load_versioncmp("game", (char *)&savegame.version, (char *)&game.version));
|
||||
FAIL_NONZERO_RETURN(
|
||||
@@ -464,28 +637,28 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_load(char *fpath)
|
||||
strncmp((char *)&savegame.uri, (char *)&game.uri, 256),
|
||||
AKERR_API,
|
||||
"Savegame is not compatible with this game");
|
||||
|
||||
|
||||
memcpy((void *)&game, (void *)&savegame, sizeof(akgl_Game));
|
||||
// Load actor name map
|
||||
actormap = SDL_CreateProperties();
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, actormap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Actor *), AKGL_REGISTRY_ACTOR));
|
||||
// Load sprite name map
|
||||
spritemap = SDL_CreateProperties();
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, spritemap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Sprite *), AKGL_REGISTRY_SPRITE));
|
||||
// Load spritesheet name map
|
||||
spritesheetmap = SDL_CreateProperties();
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, spritesheetmap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_SpriteSheet *), AKGL_REGISTRY_SPRITESHEET));
|
||||
// Load character name map
|
||||
charactermap = SDL_CreateProperties();
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
|
||||
// Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers
|
||||
CATCH(e, akgl_game_load_objectnamemap(fp, charactermap, AKGL_ACTOR_MAX_NAME_LENGTH, sizeof(akgl_Character *), AKGL_REGISTRY_CHARACTER));
|
||||
// Now that we have all of our pointer maps built, we can load the actual binary objects and reset their pointers
|
||||
} CLEANUP {
|
||||
if ( fp != NULL ) {
|
||||
fclose(fp);
|
||||
}
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
@@ -502,7 +675,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
|
||||
if ( opflags == NULL ) {
|
||||
opflags = &defflags;
|
||||
}
|
||||
|
||||
|
||||
PASS(e, akgl_game_state_lock());
|
||||
|
||||
akgl_game_updateFPS();
|
||||
@@ -521,8 +694,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_game_update(akgl_Iterator *opflags)
|
||||
} else {
|
||||
actor->scale = 1.0;
|
||||
}
|
||||
PASS(e, actor->updatefunc(actor));
|
||||
}
|
||||
PASS(e, actor->updatefunc(actor));
|
||||
}
|
||||
}
|
||||
PASS(e, physics->simulate(physics, NULL));
|
||||
PASS(e, renderer->draw_world(renderer, NULL));
|
||||
|
||||
@@ -24,11 +24,6 @@ akerr_ErrorContext *akgl_heap_init()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
akerr_name_for_status(AKGL_ERR_SDL, "SDL Error");
|
||||
akerr_name_for_status(AKGL_ERR_REGISTRY, "Registry Error");
|
||||
akerr_name_for_status(AKGL_ERR_HEAP, "Heap Error");
|
||||
akerr_name_for_status(AKGL_ERR_BEHAVIOR, "Behavior Error");
|
||||
akerr_name_for_status(AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt");
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
|
||||
memset(&HEAP_SPRITE[i], 0x00, sizeof(akgl_Sprite));
|
||||
@@ -146,7 +141,7 @@ akerr_ErrorContext *akgl_heap_release_character(akgl_Character *basechar)
|
||||
akgl_Iterator opflags;
|
||||
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "NULL character reference");
|
||||
AKGL_BITMASK_CLEAR(opflags.flags);
|
||||
|
||||
|
||||
if ( basechar->refcount > 0 ) {
|
||||
basechar->refcount -= 1;
|
||||
}
|
||||
@@ -201,4 +196,3 @@ akerr_ErrorContext *akgl_heap_release_string(akgl_String *ptr)
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,135 +15,135 @@
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_object_value(json_t *obj, char *key, json_t **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_boolean_value(json_t *obj, char *key, bool *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_boolean_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_boolean(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_boolean_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_integer_value(json_t *obj, char *key, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_integer_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL object pointer");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_integer_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_number_value(json_t *obj, char *key, float *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_number_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_number_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_double_value(json_t *obj, char *key, double *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_number_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_number(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = json_number_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_string_value(json_t *obj, char *key, akgl_String **dest)
|
||||
{
|
||||
json_t *value = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = NULL;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, key, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
|
||||
value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
ATTEMPT {
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(errctx, akgl_heap_next_string(dest));
|
||||
CATCH(errctx, akgl_string_initialize(*dest, NULL));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, false);
|
||||
|
||||
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
|
||||
SUCCEED_RETURN(errctx);
|
||||
value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
ATTEMPT {
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(errctx, akgl_heap_next_string(dest));
|
||||
CATCH(errctx, akgl_string_initialize(*dest, NULL));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, false);
|
||||
|
||||
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_array_value(json_t *obj, char *key, json_t **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_object_get(obj, key);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_KEY, "Key %s not found in object", key);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_array(value)), AKERR_TYPE, "Key %s in object has incorrect type", key);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_array_index_object(json_t *array, int index, json_t **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_object(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
*dest = value;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_array_index_integer(json_t *array, int index, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
*dest = json_integer_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_integer(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
*dest = json_integer_value(value);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_get_json_array_index_string(json_t *array, int index, akgl_String **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
ATTEMPT {
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(errctx, akgl_heap_next_string(dest));
|
||||
CATCH(errctx, akgl_string_initialize(*dest, NULL));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, false);
|
||||
|
||||
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, array, AKERR_NULLPOINTER, "NULL pointer reference");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination pointer reference");
|
||||
json_t *value = json_array_get(array, index);
|
||||
FAIL_ZERO_RETURN(errctx, value, AKERR_OUTOFBOUNDS, "Index %d out of bounds for array", index);
|
||||
FAIL_ZERO_RETURN(errctx, (json_is_string(value)), AKERR_TYPE, "Index %d in object has incorrect type", index);
|
||||
ATTEMPT {
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(errctx, akgl_heap_next_string(dest));
|
||||
CATCH(errctx, akgl_string_initialize(*dest, NULL));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, false);
|
||||
|
||||
strncpy((char *)&(*dest)->data, json_string_value(value), AKGL_MAX_STRING_LENGTH);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext *err, void *defval, void *dest, uint32_t defsize)
|
||||
@@ -153,11 +153,11 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
int docopy = 0;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(e, err, AKERR_NULLPOINTER, "err");
|
||||
FAIL_ZERO_RETURN(e, defval, AKERR_NULLPOINTER, "defval");
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dest");
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
} CLEANUP {
|
||||
} PROCESS(err) {
|
||||
@@ -165,6 +165,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_json_with_default(akerr_ErrorContext
|
||||
} HANDLE_GROUP(err, AKERR_INDEX) {
|
||||
memcpy(dest, defval, defsize);
|
||||
} FINISH(err, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_null(akgl_PhysicsBackend *s
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
|
||||
|
||||
self->gravity = akgl_physics_null_gravity;
|
||||
self->collide = akgl_physics_null_collide;
|
||||
self->move = akgl_physics_null_move;
|
||||
@@ -65,7 +65,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_gravity(akgl_PhysicsBacke
|
||||
// Assume Z origin is - (behind the camera)
|
||||
actor->ez -= (self->gravity_z * dt);
|
||||
}
|
||||
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_arcade_move(struct akgl_PhysicsB
|
||||
FAIL_ZERO_RETURN(e, actor, AKERR_NULLPOINTER, "actor");
|
||||
actor->x += actor->vx * dt;
|
||||
actor->y += actor->vy * dt;
|
||||
actor->z += actor->vz * dt;
|
||||
actor->z += actor->vz * dt;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
PASS(e, akgl_heap_next_string(&tmp));
|
||||
|
||||
|
||||
self->gravity = akgl_physics_arcade_gravity;
|
||||
self->collide = akgl_physics_arcade_collide;
|
||||
self->move = akgl_physics_arcade_move;
|
||||
@@ -117,7 +117,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_init_arcade(akgl_PhysicsBackend
|
||||
IGNORE(akgl_heap_release_string(tmp));
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
@@ -143,8 +143,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
|
||||
if ( opflags == NULL ) {
|
||||
opflags = &defflags;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||
actor = &HEAP_ACTOR[i];
|
||||
if ( actor->refcount == 0 ) {
|
||||
@@ -164,21 +164,21 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// thrust is a function of acceleration on a given axis
|
||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) ||
|
||||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
|
||||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) {
|
||||
actor->tx += actor->ax * dt;
|
||||
}
|
||||
if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) ||
|
||||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
||||
AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) {
|
||||
actor->ty += actor->ay * dt;
|
||||
}
|
||||
|
||||
// velocity equals thrust unless thrust exceeds max speed
|
||||
if ( fabsf(actor->tx) > fabsf(actor->sx) ) {
|
||||
if ( actor->tx < 0 ) {
|
||||
actor->tx = -actor->sx;
|
||||
actor->tx = -actor->sx;
|
||||
} else {
|
||||
actor->tx = actor->sx;
|
||||
}
|
||||
@@ -216,14 +216,14 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_simulate(akgl_PhysicsBackend *se
|
||||
actor->vy = actor->ey + actor->ty;
|
||||
actor->vz = actor->ez + actor->tz;
|
||||
|
||||
PASS(e, self->move(self, actor, dt));
|
||||
PASS(e, self->move(self, actor, dt));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} HANDLE(e, AKGL_ERR_LOGICINTERRUPT) {
|
||||
// noop
|
||||
} FINISH(e, true);
|
||||
}
|
||||
self->gravity_time = curtime;
|
||||
self->gravity_time = curtime;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_physics_factory(akgl_PhysicsBackend *sel
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
FAIL_ZERO_RETURN(e, type, AKERR_NULLPOINTER, "type");
|
||||
|
||||
|
||||
if ( strncmp(type->data, "null", 4) == 0) {
|
||||
PASS(e, akgl_physics_init_null(self));
|
||||
SUCCEED_RETURN(e);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
#include <jansson.h>
|
||||
|
||||
@@ -95,7 +96,7 @@ akerr_ErrorContext *akgl_registry_init_actor_state_strings()
|
||||
|
||||
akerr_ErrorContext *akgl_registry_init_sprite()
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
AKGL_REGISTRY_SPRITE = SDL_CreateProperties();
|
||||
FAIL_ZERO_RETURN(errctx, AKGL_REGISTRY_SPRITE, AKERR_NULLPOINTER, "Error initializing sprite registry");
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -123,12 +124,19 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
|
||||
json_t *props = NULL;
|
||||
const char *pkey = NULL;
|
||||
json_t *pvalue = NULL;
|
||||
|
||||
|
||||
json_error_t error;
|
||||
akgl_String *tmpstr;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "null filename");
|
||||
// One ATTEMPT around the whole function, rather than one for the parse and
|
||||
// a bare loop after it. `props` is a borrowed reference into `json` and is
|
||||
// read by the loop, so the document cannot be released until the loop is
|
||||
// done -- and with the loop outside the block, every path out of it leaked
|
||||
// the document. The per-property ATTEMPT stays nested inside the loop,
|
||||
// which is what AGENTS.md requires: a CATCH written directly in a loop
|
||||
// breaks the loop rather than leaving the function.
|
||||
ATTEMPT {
|
||||
SDL_Log("Loading from %s", fname);
|
||||
json = json_load_file(fname, 0, &error);
|
||||
@@ -141,21 +149,28 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_registry_load_properties(char *fname)
|
||||
error.line,
|
||||
error.text);
|
||||
CATCH(errctx, akgl_get_json_object_value(json, "properties", &props));
|
||||
|
||||
json_object_foreach(props, pkey, pvalue) {
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
CATCH(errctx, akgl_get_json_string_value(props, (char *)pkey, &tmpstr));
|
||||
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, pkey, tmpstr->data);
|
||||
SDL_Log("Set property %s = %s", pkey, tmpstr->data);
|
||||
CATCH(errctx, akgl_heap_release_string(tmpstr));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
}
|
||||
} CLEANUP {
|
||||
// Every value was copied into the property store on the way past, so
|
||||
// nothing outlives the document.
|
||||
if ( json != NULL ) {
|
||||
json_decref(json);
|
||||
json = NULL;
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
json_object_foreach(props, pkey, pvalue) {
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
CATCH(errctx, akgl_get_json_string_value(props, (char *)pkey, &tmpstr));
|
||||
SDL_SetStringProperty(AKGL_REGISTRY_PROPERTIES, pkey, tmpstr->data);
|
||||
SDL_Log("Set property %s = %s", pkey, tmpstr->data);
|
||||
CATCH(errctx, akgl_heap_release_string(tmpstr));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
}
|
||||
SDL_Log("Properties loaded");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -171,6 +186,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_set_property(char *name, char *src)
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **dest, char *def)
|
||||
{
|
||||
const char *value = NULL;
|
||||
size_t valuelen = 0;
|
||||
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, name, AKERR_NULLPOINTER, "NULL char *");
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "NULL akgl_String *");
|
||||
@@ -178,15 +196,34 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_get_property(char *name, akgl_String **d
|
||||
if ( *dest == NULL ) {
|
||||
CATCH(e, akgl_heap_next_string(dest));
|
||||
}
|
||||
CATCH(e,
|
||||
aksl_memcpy(
|
||||
(*dest)->data,
|
||||
(void *)SDL_GetStringProperty(AKGL_REGISTRY_PROPERTIES, (const char *)name, (const char *)def),
|
||||
AKGL_MAX_STRING_LENGTH)
|
||||
);
|
||||
// Copy the value's own length, not the destination's capacity. What
|
||||
// SDL hands back is its strdup of the value -- four bytes for "0.0" --
|
||||
// so copying a fixed AKGL_MAX_STRING_LENGTH read up to 4 KiB past the
|
||||
// end of somebody else's allocation on every call. It returned garbage
|
||||
// past the terminator, and it would have faulted outright on a value
|
||||
// that happened to sit at the end of a page.
|
||||
value = SDL_GetStringProperty(AKGL_REGISTRY_PROPERTIES, (const char *)name, (const char *)def);
|
||||
// An unset property with a NULL default still reports AKERR_NULLPOINTER,
|
||||
// which is what the header has always promised. It used to arrive from
|
||||
// inside aksl_memcpy; now it is raised here, before anything is measured.
|
||||
FAIL_ZERO_BREAK(
|
||||
e,
|
||||
value,
|
||||
AKERR_NULLPOINTER,
|
||||
"Property %s is not set and no default was given",
|
||||
name);
|
||||
valuelen = strlen(value);
|
||||
FAIL_NONZERO_BREAK(
|
||||
e,
|
||||
(valuelen >= AKGL_MAX_STRING_LENGTH),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Property %s is %zu bytes, which does not fit an akgl_String (%d)",
|
||||
name,
|
||||
valuelen,
|
||||
AKGL_MAX_STRING_LENGTH);
|
||||
CATCH(e, aksl_memcpy((*dest)->data, (void *)value, (valuelen + 1)));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,19 +30,31 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_init2d(akgl_RenderBackend *self)
|
||||
SDL_Log("Initializing screen (%sx%s = %dx%d)", width->data, height->data, screenwidth, screenheight);
|
||||
PASS(e, akgl_heap_release_string(width));
|
||||
PASS(e, akgl_heap_release_string(height));
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(
|
||||
e,
|
||||
SDL_CreateWindowAndRenderer(game.uri, screenwidth, screenheight, 0, &window, &self->sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
|
||||
|
||||
camera->x = 0;
|
||||
camera->y = 0;
|
||||
camera->w = screenwidth;
|
||||
camera->h = screenheight;
|
||||
|
||||
PASS(e, akgl_render_bind2d(self));
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_bind2d(akgl_RenderBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, self, AKERR_NULLPOINTER, "self");
|
||||
|
||||
// Deliberately does not touch self->sdl_renderer: a host that owns its own
|
||||
// window has already put one there, and this is the only way it gets a
|
||||
// usable backend without libakgl creating a second window.
|
||||
self->shutdown = &akgl_render_2d_shutdown;
|
||||
self->frame_start = &akgl_render_2d_frame_start;
|
||||
self->frame_end = &akgl_render_2d_frame_end;
|
||||
@@ -133,9 +145,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_render_2d_draw_world(akgl_RenderBackend
|
||||
if ( actor->layer != i ) {
|
||||
continue;
|
||||
}
|
||||
PASS(e, actor->renderfunc(actor));
|
||||
PASS(e, actor->renderfunc(actor));
|
||||
}
|
||||
}
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
56
src/sprite.c
56
src/sprite.c
@@ -41,12 +41,31 @@ akerr_ErrorContext *akgl_sprite_sheet_coords_for_frame(akgl_Sprite *self, SDL_FR
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sprite load json spritesheet.
|
||||
* @param json JSON object to parse.
|
||||
* @param sheet Spritesheet used by the sprite.
|
||||
* @param relative_path Base directory for relative asset references.
|
||||
* @brief Find or load the spritesheet a sprite definition names.
|
||||
*
|
||||
* Resolves the `spritesheet.filename` path relative to the sprite definition's
|
||||
* own directory, then looks that resolved path up in #AKGL_REGISTRY_SPRITESHEET.
|
||||
* A hit is reused as is -- which is what keeps ten sprites cut from one image
|
||||
* down to one texture -- and a miss claims a sheet from the pool and loads it,
|
||||
* reading `frame_width` and `frame_height` only in that case.
|
||||
*
|
||||
* @param json The parsed sprite document, whose `spritesheet` object
|
||||
* this reads. Required in practice; not checked.
|
||||
* @param sheet Receives the sheet, whether found or freshly loaded.
|
||||
* Required; not checked. On the reuse path the sheet's
|
||||
* reference count is *not* incremented, so the sprite
|
||||
* borrows it.
|
||||
* @param relative_path Directory to resolve the image path against -- the
|
||||
* directory holding the sprite JSON. Required.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @throws AKERR_KEY If the document has no `spritesheet` object, or that object
|
||||
* has no `filename` -- or, on the load path, no `frame_width` or
|
||||
* `frame_height`.
|
||||
* @throws AKERR_TYPE If one of those has the wrong JSON type.
|
||||
* @throws AKERR_OUTOFBOUNDS If the joined path is too long for a pooled string.
|
||||
* @throws ENOENT If the image path does not exist.
|
||||
* @throws AKGL_ERR_SDL If the image cannot be decoded.
|
||||
* @throws AKGL_ERR_HEAP If the spritesheet or string pool is exhausted.
|
||||
*/
|
||||
static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_SpriteSheet **sheet, char *relative_path)
|
||||
{
|
||||
@@ -85,10 +104,10 @@ static akerr_ErrorContext *akgl_sprite_load_json_spritesheet(json_t *json, akgl_
|
||||
IGNORE(akgl_heap_release_string(tmpstr));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -100,7 +119,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
akgl_String *spritename = NULL;
|
||||
akgl_String *filename_copy = NULL;
|
||||
int i = 0;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, filename, AKERR_NULLPOINTER, "Received null filename");
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_sprite(&obj));
|
||||
@@ -125,7 +144,7 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
AKERR_NULLPOINTER,
|
||||
"Error while loading sprite from %s on line %d: %s", filename, error.line, error.text
|
||||
);
|
||||
|
||||
|
||||
CATCH(errctx, akgl_sprite_load_json_spritesheet((json_t *)json, &sheet, dirname(filename_copy->data)));
|
||||
CATCH(errctx, akgl_get_json_string_value((json_t *)json, "name", &spritename));
|
||||
CATCH(errctx,
|
||||
@@ -134,20 +153,27 @@ akerr_ErrorContext *akgl_sprite_load_json(char *filename)
|
||||
spritename->data,
|
||||
(akgl_SpriteSheet *)sheet)
|
||||
);
|
||||
|
||||
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "width", &obj->width));
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "height", &obj->height));
|
||||
CATCH(errctx, akgl_get_json_integer_value((json_t *)json, "speed", &obj->speed));
|
||||
obj->speed = obj->speed * AKGL_TIME_ONESEC_MS;
|
||||
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loop", &obj->loop));
|
||||
CATCH(errctx, akgl_get_json_boolean_value((json_t *)json, "loopReverse", &obj->loopReverse));
|
||||
|
||||
|
||||
CATCH(errctx, akgl_get_json_array_value((json_t *)json, "frames", &frames));
|
||||
obj->frames = json_array_size((json_t *)frames);
|
||||
for ( i = 0 ; i < obj->frames; i++ ) {
|
||||
CATCH(errctx, akgl_get_json_array_index_integer((json_t *)frames, i, (uint32_t *)&obj->frameids[i]));
|
||||
}
|
||||
} CLEANUP {
|
||||
// The sprite copies every field it wants out of the document and the
|
||||
// sheet is found by resolved path, so nothing here points into the
|
||||
// parsed tree once this block runs. Released on both paths.
|
||||
if ( json != NULL ) {
|
||||
json_decref(json);
|
||||
json = NULL;
|
||||
}
|
||||
if ( errctx != NULL && errctx->status != 0 ) {
|
||||
IGNORE(akgl_heap_release_sprite(obj));
|
||||
IGNORE(akgl_heap_release_spritesheet(sheet));
|
||||
@@ -166,7 +192,7 @@ akerr_ErrorContext *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_Sp
|
||||
FAIL_ZERO_RETURN(errctx, spr, AKERR_NULLPOINTER, "Null sprite reference");
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Empty sprite name");
|
||||
FAIL_ZERO_RETURN(errctx, sheet, AKERR_NULLPOINTER, "Null spritesheet reference");
|
||||
|
||||
|
||||
memset(spr, 0x00, sizeof(akgl_Sprite));
|
||||
memcpy(spr->name, name, AKGL_SPRITE_MAX_NAME_LENGTH);
|
||||
spr->sheet = sheet;
|
||||
@@ -191,14 +217,14 @@ akerr_ErrorContext *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int spr
|
||||
memset(sheet, 0x00, sizeof(akgl_SpriteSheet));
|
||||
|
||||
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
|
||||
|
||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
strncpy((char *)&sheet->name, filename, AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH);
|
||||
|
||||
|
||||
//snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), filename);
|
||||
sheet->texture = IMG_LoadTexture(renderer->sdl_renderer, filename);
|
||||
FAIL_ZERO_BREAK(errctx, sheet->texture, AKGL_ERR_SDL, "Failed loading asset %s : %s", filename, SDL_GetError());
|
||||
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_SPRITESHEET, (char *)sheet->name, (void *)sheet),
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
|
||||
akerr_ErrorContext *akgl_string_initialize(akgl_String *obj, char *init)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Attempted to initialize NULL string reference");
|
||||
if ( init != NULL ) {
|
||||
strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH);
|
||||
} else {
|
||||
memset(&obj->data, 0x00, sizeof(akgl_String));
|
||||
}
|
||||
obj->refcount = 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "Attempted to initialize NULL string reference");
|
||||
if ( init != NULL ) {
|
||||
strncpy((char *)&obj->data, init, AKGL_MAX_STRING_LENGTH);
|
||||
} else {
|
||||
memset(&obj->data, 0x00, sizeof(akgl_String));
|
||||
}
|
||||
obj->refcount = 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_string_copy(akgl_String *src, akgl_String *dst, int count)
|
||||
|
||||
80
src/text.c
80
src/text.c
@@ -13,12 +13,19 @@
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath, int size)
|
||||
{
|
||||
TTF_Font *font = NULL;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null filepath");
|
||||
FAIL_ZERO_RETURN(errctx, filepath, AKERR_NULLPOINTER, "Null filepath");
|
||||
font = TTF_OpenFont(filepath, size);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
// Loading over an existing name used to abandon the font it displaced --
|
||||
// there was nothing in the library that could close one. The old font goes
|
||||
// back first now, and only once the new one has opened, so a failed load
|
||||
// leaves the caller with the font they already had.
|
||||
if ( SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL) != NULL ) {
|
||||
PASS(errctx, akgl_text_unloadfont(name));
|
||||
}
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetPointerProperty(AKGL_REGISTRY_FONT, name, (void *)font),
|
||||
@@ -30,15 +37,39 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name)
|
||||
{
|
||||
TTF_Font *font = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "Null font name");
|
||||
|
||||
font = (TTF_Font *)SDL_GetPointerProperty(AKGL_REGISTRY_FONT, name, NULL);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "No font named %s in the registry", name);
|
||||
|
||||
// Cleared before the close, so a font is never reachable through the
|
||||
// registry after it has been handed back to SDL_ttf.
|
||||
SDL_ClearProperty(AKGL_REGISTRY_FONT, name);
|
||||
TTF_CloseFont(font);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *text, SDL_Color color, int wraplength, int x, int y)
|
||||
{
|
||||
SDL_Surface *textsurf = NULL;
|
||||
SDL_Texture *texture = NULL;
|
||||
SDL_FRect dest;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
||||
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
||||
// Checked before anything is rasterized, and checked at all because a
|
||||
// backend that has an SDL_Renderer but was never run through
|
||||
// akgl_render_bind2d has a NULL draw_texture -- which is exactly the state
|
||||
// akgl_render_init2d used to be the only escape from.
|
||||
FAIL_ZERO_RETURN(errctx, renderer, AKERR_NULLPOINTER, "No renderer backend");
|
||||
FAIL_ZERO_RETURN(errctx, renderer->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||
FAIL_ZERO_RETURN(errctx, renderer->draw_texture, AKERR_NULLPOINTER, "Renderer backend has no draw_texture");
|
||||
if ( wraplength > 0 ) {
|
||||
textsurf = TTF_RenderText_Blended_Wrapped(
|
||||
font,
|
||||
@@ -64,3 +95,46 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_rendertextat(TTF_Font *font, char *
|
||||
SDL_DestroySurface(textsurf);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure(TTF_Font *font, char *text, int *w, int *h)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
||||
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
||||
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
|
||||
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
|
||||
// A zero length means "the string is null terminated", not "the empty
|
||||
// string" -- an empty text measures 0 wide and one line high.
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
TTF_GetStringSize(font, text, 0, w, h),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_measure_wrapped(TTF_Font *font, char *text, int wraplength, int *w, int *h)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, font, AKERR_NULLPOINTER, "NULL font");
|
||||
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "NULL text string");
|
||||
FAIL_ZERO_RETURN(errctx, w, AKERR_NULLPOINTER, "NULL width destination");
|
||||
FAIL_ZERO_RETURN(errctx, h, AKERR_NULLPOINTER, "NULL height destination");
|
||||
// SDL_ttf takes the wrap width as an int and reads a negative one as a
|
||||
// very large unsigned width, which silently disables wrapping instead of
|
||||
// reporting anything. Refuse it here rather than return a wrong measurement.
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
(wraplength < 0),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"Wrap length %d is negative",
|
||||
wraplength);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
TTF_GetStringSizeWrapped(font, text, 0, wraplength, w, h),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
253
src/tilemap.c
253
src/tilemap.c
@@ -60,7 +60,7 @@ akerr_ErrorContext *akgl_get_json_tilemap_property(json_t *obj, char *key, char
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
FAIL_RETURN(errctx, AKERR_KEY, "Property not found in properties map");
|
||||
}
|
||||
|
||||
@@ -68,12 +68,12 @@ akerr_ErrorContext *akgl_get_json_properties_string(json_t *obj, char *key, akgl
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
json_t *property;
|
||||
|
||||
|
||||
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "string", &property));
|
||||
PASS(errctx, akgl_heap_next_string(dest));
|
||||
PASS(errctx, akgl_string_initialize(*dest, NULL));
|
||||
PASS(errctx, akgl_get_json_string_value(property, "value", dest));
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -83,17 +83,26 @@ akerr_ErrorContext *akgl_get_json_properties_integer(json_t *obj, char *key, int
|
||||
json_t *property = NULL;
|
||||
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "int", &property));
|
||||
PASS(errctx, akgl_get_json_integer_value(property, "value", dest));
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get json properties number.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a Tiled custom property declared as `number`.
|
||||
*
|
||||
* Tiled writes `float` for a floating-point property, so this matches nothing
|
||||
* Tiled produces today -- akgl_get_json_properties_float is the one the loader
|
||||
* uses. Kept because a hand-written or older map may still say `number`.
|
||||
*
|
||||
* @param obj The Tiled object whose `properties` array to search. Required.
|
||||
* @param key The property name. Required.
|
||||
* @param dest Receives the value as a `float`. Not written on any failure path.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
|
||||
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
|
||||
* @throws AKERR_TYPE If the property is not declared `number`, or its `value` is
|
||||
* not numeric.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, float *dest)
|
||||
{
|
||||
@@ -101,17 +110,25 @@ akerr_ErrorContext *akgl_get_json_properties_number(json_t *obj, char *key, floa
|
||||
json_t *property = NULL;
|
||||
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "number", &property));
|
||||
PASS(errctx, akgl_get_json_number_value(property, "value", dest));
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get json properties float.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a Tiled custom property declared as `float`.
|
||||
*
|
||||
* The spelling Tiled actually writes for a floating-point property. This is how
|
||||
* the `scale` on a perspective marker is read.
|
||||
*
|
||||
* @param obj The Tiled object whose `properties` array to search. Required.
|
||||
* @param key The property name. Required.
|
||||
* @param dest Receives the value. Not written on any failure path.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
|
||||
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
|
||||
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
|
||||
* not numeric.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float *dest)
|
||||
{
|
||||
@@ -119,17 +136,27 @@ akerr_ErrorContext *akgl_get_json_properties_float(json_t *obj, char *key, float
|
||||
json_t *property = NULL;
|
||||
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
|
||||
PASS(errctx, akgl_get_json_number_value(property, "value", dest));
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get json properties double.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param key JSON object key or property name to locate.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @brief Read a Tiled custom property declared as `float`, at full precision.
|
||||
*
|
||||
* Same declared type as akgl_get_json_properties_float, but writing a `double`.
|
||||
* The map's physics constants are `double`, which is the reason both exist.
|
||||
*
|
||||
* @param obj The Tiled object whose `properties` array to search. Required.
|
||||
* @param key The property name. Required.
|
||||
* @param dest Receives the value. Not written on any failure path.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_* Propagates an error reported by a delegated operation.
|
||||
* @throws AKERR_NULLPOINTER If @p obj or @p key is `NULL`.
|
||||
* @throws AKERR_KEY If the property is absent, or @p obj has no properties.
|
||||
* akgl_tilemap_load_physics passes this status to
|
||||
* akgl_get_json_with_default, which turns it into a default of 0.0.
|
||||
* @throws AKERR_TYPE If the property is not declared `float`, or its `value` is
|
||||
* not numeric.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, double *dest)
|
||||
{
|
||||
@@ -137,7 +164,7 @@ akerr_ErrorContext *akgl_get_json_properties_double(json_t *obj, char *key, doub
|
||||
json_t *property = NULL;
|
||||
PASS(errctx, akgl_get_json_tilemap_property(obj, key, "float", &property));
|
||||
PASS(errctx, akgl_get_json_double_value(property, "value", dest));
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -146,7 +173,7 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
|
||||
PREPARE_ERROR(e);
|
||||
akgl_String *tmpstr = NULL;
|
||||
akgl_String *tmppath = NULL;
|
||||
|
||||
|
||||
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "columns", &dest->tilesets[tsidx].columns));
|
||||
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "firstgid", &dest->tilesets[tsidx].firstgid));
|
||||
PASS(e, akgl_get_json_integer_value((json_t *)tileset, "imageheight", &dest->tilesets[tsidx].imageheight));
|
||||
@@ -164,8 +191,8 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
|
||||
(char *)&tmpstr->data,
|
||||
AKGL_TILEMAP_MAX_TILESET_NAME_SIZE
|
||||
);
|
||||
|
||||
CATCH(e, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr));
|
||||
|
||||
CATCH(e, akgl_get_json_string_value((json_t *)tileset, "image", &tmpstr));
|
||||
CATCH(e, akgl_path_relative((char *)&dirname->data, (char *)&tmpstr->data, tmppath));
|
||||
strncpy((char *)&dest->tilesets[tsidx].imagefilename, tmppath->data, AKGL_MAX_STRING_LENGTH);
|
||||
} CLEANUP {
|
||||
@@ -173,10 +200,10 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets_each(json_t *tileset, akgl_Tilema
|
||||
IGNORE(akgl_heap_release_string(tmppath));
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
|
||||
|
||||
dest->tilesets[tsidx].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)&dest->tilesets[tsidx].imagefilename);
|
||||
FAIL_ZERO_RETURN(e, dest->tilesets[tsidx].texture, AKERR_NULLPOINTER, "Failed loading tileset image : %s", SDL_GetError());
|
||||
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
@@ -199,7 +226,7 @@ akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int
|
||||
for (j = 0; j <= (dest->tilesets[tilesetidx].tilecount); j++) {
|
||||
/*
|
||||
* For a given 8x2 tilemap like this with 10x10 tiles and 0 spacing and 0 margin
|
||||
*
|
||||
*
|
||||
* 01234567
|
||||
* 89ABCDEF
|
||||
*
|
||||
@@ -221,7 +248,7 @@ akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int
|
||||
x_offset = (j * (dest->tilesets[tilesetidx].tilewidth + dest->tilesets[tilesetidx].spacing));
|
||||
y_offset = dest->tilesets[tilesetidx].spacing;
|
||||
}
|
||||
|
||||
|
||||
dest->tilesets[tilesetidx].tile_offsets[j][0] = x_offset;
|
||||
dest->tilesets[tilesetidx].tile_offsets[j][1] = y_offset;
|
||||
/* SDL_Log("Tileset %s index (%d, %d) is offset (%d, %d)",
|
||||
@@ -233,8 +260,8 @@ akerr_ErrorContext *akgl_tilemap_compute_tileset_offsets(akgl_Tilemap *dest, int
|
||||
// SDL_Log("Processed %d total tiles for tileset", j);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root, akgl_String *dirname)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -244,7 +271,7 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
|
||||
json_t *tilesets = NULL;
|
||||
json_t *jstileset = NULL;
|
||||
int i;
|
||||
|
||||
|
||||
dest->numtilesets = 0;
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_get_json_array_value(root, "tilesets", &tilesets))
|
||||
@@ -257,25 +284,51 @@ akerr_ErrorContext *akgl_tilemap_load_tilesets(akgl_Tilemap *dest, json_t *root,
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tilemap load layer object actor.
|
||||
* @param curobj Tilemap object being populated.
|
||||
* @param layerdatavalue JSON object describing the layer entry.
|
||||
* @param layerid Zero-based tilemap layer index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @brief Turn one Tiled object into a live actor, creating it if it is not already registered.
|
||||
*
|
||||
* The object's name is the actor's registry key, which is what lets the same
|
||||
* actor be placed by more than one object: the first placement creates it and
|
||||
* binds the character named in the object's `character` property, and every
|
||||
* later one just takes another reference. Either way the object's position,
|
||||
* visibility, and layer are copied onto the actor and the object keeps a
|
||||
* pointer to it.
|
||||
*
|
||||
* Not declared in tilemap.h -- reachable only through
|
||||
* akgl_tilemap_load_layer_objects.
|
||||
*
|
||||
* @param curobj The tilemap object, with its `name`, `x`, `y`, and
|
||||
* `visible` already read from JSON. Required, unchecked,
|
||||
* dereferenced at once.
|
||||
* @param layerdatavalue The object's JSON, for the custom properties --
|
||||
* `character` and `state` -- that are not part of Tiled's
|
||||
* own object fields. Required.
|
||||
* @param layerid The layer this object sits on, copied onto the actor.
|
||||
* @param dirname Directory to resolve paths against. Accepted for
|
||||
* signature consistency; nothing here uses it.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @throws AKERR_KEY If the object's name is empty -- an actor has to be
|
||||
* addressable -- if the `character` or `state` property is absent, or if
|
||||
* the named character is not in #AKGL_REGISTRY_CHARACTER.
|
||||
* @throws AKERR_TYPE If `character` is not declared `string` or `state` not
|
||||
* declared `int`.
|
||||
* @throws AKERR_NULLPOINTER If akgl_actor_initialize refuses the name.
|
||||
* @throws AKGL_ERR_HEAP If the actor or string pool is exhausted.
|
||||
*
|
||||
* @note An existing actor's `character` and position are overwritten by each
|
||||
* later placement, so two objects naming one actor leave it wherever the
|
||||
* last one put it rather than producing two.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *curobj, json_t *layerdatavalue, int layerid, akgl_String *dirname)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_String *tmpstr = NULL;
|
||||
akgl_Actor *actorobj = NULL;
|
||||
|
||||
|
||||
curobj->type = AKGL_TILEMAP_OBJECT_TYPE_ACTOR;
|
||||
if ( strlen((char *)&curobj->name) == 0 ) {
|
||||
FAIL_RETURN(errctx, AKERR_KEY, "Actor in tile object layer cannot have empty name");
|
||||
@@ -304,7 +357,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_object_actor(akgl_TilemapObject *cur
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
actorobj->layer = layerid;
|
||||
actorobj->x = curobj->x;
|
||||
actorobj->y = curobj->y;
|
||||
@@ -340,7 +393,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *
|
||||
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "x", &curobj->x));
|
||||
PASS(errctx, akgl_get_json_number_value((json_t *)layerdatavalue, "y", &curobj->y));
|
||||
PASS(errctx, akgl_get_json_boolean_value((json_t *)layerdatavalue, "visible", &curobj->visible));
|
||||
|
||||
|
||||
PASS(errctx, akgl_get_json_string_value((json_t *)layerdatavalue, "type", &tmpstr));
|
||||
if ( strcmp(tmpstr->data, "actor") == 0 ) {
|
||||
PASS(errctx, akgl_tilemap_load_layer_object_actor(curobj, layerdatavalue, layerid, dirname));
|
||||
@@ -354,11 +407,11 @@ akerr_ErrorContext *akgl_tilemap_load_layer_objects(akgl_Tilemap *dest, json_t *
|
||||
dest->p_vanishing_y = curobj->y;
|
||||
PASS(errctx, akgl_get_json_integer_value((json_t *)layerdatavalue, "height", &dest->p_vanishing_h));
|
||||
PASS(errctx, akgl_get_json_properties_float((json_t *)layerdatavalue, "scale", &dest->p_vanishing_scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
layerdatavalue = NULL;
|
||||
}
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -387,19 +440,37 @@ akerr_ErrorContext *akgl_tilemap_load_layer_tile(akgl_Tilemap *dest, json_t *roo
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tilemap load layer image.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param layerid Zero-based tilemap layer index.
|
||||
* @param dirname Directory containing paths referenced by the document.
|
||||
* @brief Load one image layer: upload its image as a single texture.
|
||||
*
|
||||
* An image layer is one picture rather than a grid, which is what a parallax
|
||||
* backdrop wants. The layer's `width` and `height` are taken from the loaded
|
||||
* texture rather than from the JSON, so they are always the true pixel size.
|
||||
*
|
||||
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load_layers.
|
||||
*
|
||||
* @param dest The map to load into. Required.
|
||||
* @param root The layer's JSON object. Required.
|
||||
* @param layerid Which layer slot to fill. Not bounds-checked.
|
||||
* @param dirname Directory to resolve the layer's `image` path against.
|
||||
* Required. Joined with a `/` rather than run through
|
||||
* akgl_path_relative, so unlike a tileset image this path is not
|
||||
* canonicalized and an absolute one will not work.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKGL_ERR_SDL When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p dest, @p root, or @p dirname is `NULL`.
|
||||
* @throws AKERR_KEY If the layer has no `image`.
|
||||
* @throws AKERR_TYPE If `image` is not a string.
|
||||
* @throws AKGL_ERR_SDL If the image cannot be loaded. The message carries
|
||||
* `SDL_GetError()`.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @note The joined path is truncated at
|
||||
* #AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE without complaint, so an
|
||||
* over-long path fails as a missing file rather than as a length error.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *root, int layerid, akgl_String *dirname)
|
||||
{
|
||||
@@ -417,15 +488,15 @@ akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *ro
|
||||
CATCH(errctx, akgl_get_json_string_value(root, "image", &tmpstr));
|
||||
|
||||
DISABLE_GCC_WARNING_FORMAT_TRUNCATION
|
||||
snprintf((char *)&fpath->data,
|
||||
AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE,
|
||||
"%s/%s",
|
||||
dirname->data,
|
||||
tmpstr->data
|
||||
);
|
||||
snprintf((char *)&fpath->data,
|
||||
AKGL_TILEMAP_MAX_TILESET_FILENAME_SIZE,
|
||||
"%s/%s",
|
||||
dirname->data,
|
||||
tmpstr->data
|
||||
);
|
||||
RESTORE_GCC_WARNINGS
|
||||
|
||||
dest->layers[layerid].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)fpath->data);
|
||||
|
||||
dest->layers[layerid].texture = IMG_LoadTexture(renderer->sdl_renderer, (char *)fpath->data);
|
||||
FAIL_ZERO_BREAK(errctx, dest->layers[layerid].texture, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
dest->layers[layerid].width = dest->layers[layerid].texture->w;
|
||||
dest->layers[layerid].height = dest->layers[layerid].texture->h;
|
||||
@@ -434,7 +505,7 @@ akerr_ErrorContext *akgl_tilemap_load_layer_image(akgl_Tilemap *dest, json_t *ro
|
||||
IGNORE(akgl_heap_release_string(fpath));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -450,7 +521,7 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
|
||||
int i;
|
||||
int layerid = 0;
|
||||
int tmpint = 0;
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_get_json_array_value(root, "layers", &layers));
|
||||
dest->numlayers = json_array_size((json_t *)layers);
|
||||
@@ -488,11 +559,31 @@ akerr_ErrorContext *akgl_tilemap_load_layers(akgl_Tilemap *dest, json_t *root, a
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tilemap load physics.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY When the corresponding validation or operation fails.
|
||||
* @brief Give the map its own physics backend, if it asked for one.
|
||||
*
|
||||
* A map with a `physics.model` custom property gets its own backend, configured
|
||||
* from `physics.gravity.x`/`.y`/`.z` and `physics.drag.x`/`.y`/`.z`, and
|
||||
* `use_own_physics` is set so a caller knows to simulate through it. Each
|
||||
* constant defaults to 0.0 when absent, so a map can name a model and override
|
||||
* only what it cares about.
|
||||
*
|
||||
* Absence is the normal case at every level: a map with no properties at all, or
|
||||
* with properties but no `physics.model`, uses the game's global backend and
|
||||
* this returns success. That is why the AKERR_KEY handlers here are not error
|
||||
* paths -- they are the "map did not ask" path.
|
||||
*
|
||||
* Not declared in tilemap.h -- reachable only through akgl_tilemap_load.
|
||||
*
|
||||
* @param dest The map to configure. Required in practice; not checked here,
|
||||
* since akgl_tilemap_load has already done so.
|
||||
* @param root The map's root JSON object. Required, likewise.
|
||||
* @return `NULL` on success -- including when the map declared no physics at
|
||||
* all -- otherwise an error context owned by the caller.
|
||||
* @throws AKERR_KEY If `physics.model` names a backend that does not exist.
|
||||
* @throws AKERR_TYPE If `physics.model` is not declared `string`, or one of the
|
||||
* six constants is not declared `float`.
|
||||
* @throws AKERR_NULLPOINTER If akgl_physics_factory refuses its arguments.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
|
||||
{
|
||||
@@ -500,7 +591,7 @@ akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
|
||||
json_t *props = NULL;
|
||||
akgl_String *tmpval = NULL;
|
||||
double defzero = 0.0;
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_heap_next_string(&tmpval));
|
||||
CATCH(e, akgl_get_json_array_value((json_t *)root, "properties", &props));
|
||||
@@ -523,7 +614,7 @@ akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
|
||||
);
|
||||
PASS(e, akgl_physics_factory(&dest->physics, tmpval));
|
||||
dest->use_own_physics = true;
|
||||
|
||||
|
||||
CATCH(e, akgl_get_json_with_default(
|
||||
akgl_get_json_properties_double(
|
||||
root, "physics.gravity.x", &dest->physics.gravity_x
|
||||
@@ -572,7 +663,7 @@ akerr_ErrorContext *akgl_tilemap_load_physics(akgl_Tilemap *dest, json_t *root)
|
||||
} HANDLE(e, AKERR_KEY) {
|
||||
SDL_Log("Map uses game physics");
|
||||
} FINISH(e, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
@@ -583,7 +674,7 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
|
||||
//akgl_String *tmpstr = NULL;
|
||||
json_error_t error;
|
||||
akgl_String *dirnamestr = NULL;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, fname, AKERR_NULLPOINTER, "load_tilemap received null filename");
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "load_tilemap received null tilemap");
|
||||
|
||||
@@ -596,9 +687,9 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
|
||||
//CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
//CATCH(errctx, akgl_string_initialize(tmpstr, NULL));
|
||||
//SDL_snprintf(tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), fname);
|
||||
CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data));
|
||||
CATCH(errctx, aksl_realpath(fname, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
|
||||
dirname((char *)&dirnamestr->data);
|
||||
|
||||
|
||||
json = json_load_file(fname, 0, &error);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
@@ -638,6 +729,14 @@ akerr_ErrorContext *akgl_tilemap_load(char *fname, akgl_Tilemap *dest)
|
||||
}
|
||||
} CLEANUP {
|
||||
//IGNORE(akgl_heap_release_string(tmpstr));
|
||||
// The map is built entirely out of copies -- layer data, tileset
|
||||
// geometry, object names -- so the document is dead the moment the
|
||||
// loaders above return, whether they succeeded or not. It is also the
|
||||
// largest of the four: a map's JSON is the size of its layer data.
|
||||
if ( json != NULL ) {
|
||||
json_decref(json);
|
||||
json = NULL;
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -696,13 +795,13 @@ akerr_ErrorContext *akgl_tilemap_draw(akgl_Tilemap *map, SDL_FRect *viewport, in
|
||||
dest.x = 0;
|
||||
dest.y = 0;
|
||||
src.w = map->layers[layeridx].width;
|
||||
src.h = map->layers[layeridx].height;
|
||||
src.h = map->layers[layeridx].height;
|
||||
dest.w = map->layers[layeridx].width;
|
||||
dest.h = map->layers[layeridx].height;
|
||||
PASS(errctx, renderer->draw_texture(renderer, map->layers[layeridx].texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
dest.x = 0;
|
||||
dest.y = 0;
|
||||
dest.w = map->tilewidth;
|
||||
@@ -778,7 +877,7 @@ akerr_ErrorContext *akgl_tilemap_draw_tileset(akgl_Tilemap *map, int tilesetidx)
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "akgl_tilemap_draw_tileset received NULL pointer to tilemap");
|
||||
FAIL_NONZERO_RETURN(errctx, (tilesetidx >= map->numtilesets), AKERR_OUTOFBOUNDS, "akgl_tilemap_draw_tileset received a tileset index out of bounds");
|
||||
|
||||
|
||||
for ( tilenum = 0; tilenum < map->tilesets[tilesetidx].tilecount; tilenum++) {
|
||||
// Render this tile to the correct screen position
|
||||
// FIXME: These conditionals are probably not very efficient. Need a better way of getting
|
||||
|
||||
264
src/util.c
264
src/util.c
@@ -21,13 +21,34 @@
|
||||
#include <akstdlib.h>
|
||||
|
||||
/**
|
||||
* @brief Path relative root.
|
||||
* @param root Base directory used to resolve the path.
|
||||
* @param path Path to resolve or transform.
|
||||
* @param dst Output destination populated by the function.
|
||||
* @brief Resolve @p path against @p root and write the absolute result to @p dst.
|
||||
*
|
||||
* The second half of akgl_path_relative: joins the two with a `/` and resolves
|
||||
* the join, so symlinks and `..` are folded out and the result is absolute. It
|
||||
* is the fallback, reached only once resolving @p path on its own has failed.
|
||||
*
|
||||
* Not declared in util.h -- it is reachable only through akgl_path_relative.
|
||||
*
|
||||
* @param root Directory to resolve against, normally `dirname` of the file that
|
||||
* named @p path. Required. A trailing `/` is harmless; the join adds
|
||||
* one unconditionally and `realpath` collapses the double.
|
||||
* @param path Relative path to resolve. Required. An absolute @p path still gets
|
||||
* @p root pasted in front of it, which will not exist -- so callers
|
||||
* must not send absolute paths down this branch.
|
||||
* @param dst Receives the resolved absolute path. Required, and must already be
|
||||
* a claimed pool string.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_OUTOFBOUNDS When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dst is `NULL`.
|
||||
* @throws AKERR_OUTOFBOUNDS If `root + path` is at least #AKGL_MAX_STRING_LENGTH
|
||||
* bytes. The message reports both the combined length and the limit.
|
||||
* @throws ENOENT If the joined path does not exist. Any other `errno`
|
||||
* `realpath(3)` raises -- EACCES, ELOOP, ENOTDIR -- propagates likewise.
|
||||
* @throws AKGL_ERR_HEAP If the string pool cannot supply the two scratch buffers.
|
||||
*
|
||||
* @note The AKERR_OUTOFBOUNDS check uses `FAIL_RETURN` from inside the `ATTEMPT`
|
||||
* block, which returns past `CLEANUP` -- so on that one path the two
|
||||
* scratch strings are never released. This is exactly the hazard AGENTS.md
|
||||
* warns about; it wants a `FAIL_BREAK`.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String *dst)
|
||||
{
|
||||
@@ -38,14 +59,14 @@ akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String
|
||||
int rootlen;
|
||||
int pathlen;
|
||||
int count;
|
||||
|
||||
|
||||
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
|
||||
|
||||
PASS(e, akgl_heap_next_string(&strbuf));
|
||||
PASS(e, akgl_heap_next_string(&pathbuf));
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
// Is it relative to the root?
|
||||
rootlen = strlen(root);
|
||||
@@ -54,10 +75,17 @@ akerr_ErrorContext *akgl_path_relative_root(char *root, char *path, akgl_String
|
||||
FAIL_RETURN(e, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH);
|
||||
}
|
||||
DISABLE_GCC_WARNING_FORMAT_TRUNCATION
|
||||
CATCH(e, aksl_sprintf(&count, (char *)&pathbuf->data, "%s/%s", root, path));
|
||||
CATCH(e, aksl_snprintf(
|
||||
&count,
|
||||
(char *)&pathbuf->data,
|
||||
sizeof(pathbuf->data),
|
||||
"%s/%s",
|
||||
root,
|
||||
path
|
||||
));
|
||||
RESTORE_GCC_WARNINGS
|
||||
CATCH(e, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data));
|
||||
CATCH(e, akgl_string_copy(strbuf, dst, 0));
|
||||
CATCH(e, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data, sizeof(strbuf->data)));
|
||||
CATCH(e, akgl_string_copy(strbuf, dst, 0));
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(strbuf));
|
||||
IGNORE(akgl_heap_release_string(pathbuf));
|
||||
@@ -71,36 +99,62 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
|
||||
PREPARE_ERROR(e);
|
||||
akgl_String *strbuf;
|
||||
char *result;
|
||||
|
||||
bool relative_to_root = false;
|
||||
|
||||
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "NULL argument");
|
||||
|
||||
PASS(e, akgl_heap_next_string(&strbuf));
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
// Is path relative to our current working directory?
|
||||
CATCH(e, aksl_realpath(path, (char *)&strbuf->data));
|
||||
CATCH(e, aksl_realpath(path, (char *)&strbuf->data, sizeof(strbuf->data)));
|
||||
// Yes it is. strbuf->data contains the absolute path.
|
||||
CATCH(e, akgl_string_copy(strbuf, dst, 0));
|
||||
CATCH(e, akgl_string_copy(strbuf, dst, 0));
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(strbuf));
|
||||
} PROCESS(e) {
|
||||
} HANDLE(e, ENOENT) {
|
||||
// Path is not relative to our current working directory
|
||||
// Noop - execution proceeds after the break
|
||||
return akgl_path_relative_root(root, path, dst);
|
||||
// Path is not relative to our current working directory. Resolve it
|
||||
// against root instead -- but after FINISH, not from in here. Returning
|
||||
// from inside a HANDLE block skips the RELEASE_ERROR that FINISH ends
|
||||
// with, so the handled context is never given back to
|
||||
// AKERR_ARRAY_ERROR. That leaks one context per call, and the 129th
|
||||
// call takes the whole process down with "Unable to pull an error
|
||||
// context from the array!". Every map load resolves several paths this
|
||||
// way.
|
||||
relative_to_root = true;
|
||||
} FINISH(e, true);
|
||||
|
||||
if ( relative_to_root == true ) {
|
||||
PASS(e, akgl_path_relative_root(root, path, dst));
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Path relative from.
|
||||
* @param path Path to resolve or transform.
|
||||
* @param from Base path from which the result is made relative.
|
||||
* @param dst Output destination populated by the function.
|
||||
* @brief Intended to resolve @p path against the directory holding @p from. Unfinished.
|
||||
*
|
||||
* The shape is there -- resolve @p from, take its `dirname` -- but the body
|
||||
* stops before it does anything with @p path, and `*dst` is never written. Not
|
||||
* declared in util.h, and nothing in the tree calls it.
|
||||
*
|
||||
* @param path The path to resolve. Required, and currently ignored past the
|
||||
* `NULL` check.
|
||||
* @param from A file whose directory @p path should be taken as relative to.
|
||||
* Required. Must exist -- it is resolved with `realpath(3)`.
|
||||
* @param dst Intended to receive the resolved path. Never written. Note the
|
||||
* double indirection, which disagrees with the rest of the
|
||||
* `akgl_path_relative*` family; see TODO.md item 40.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When the corresponding validation or operation fails.
|
||||
* @throws AKERR_NULLPOINTER If @p path or @p from is `NULL`.
|
||||
* @throws ENOENT If @p from does not exist, along with any other `errno`
|
||||
* `realpath(3)` raises.
|
||||
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
|
||||
*
|
||||
* @warning Known defect: the scratch string it claims is never released, so 256
|
||||
* calls exhaust the string pool. TODO.md, "Known and still open" item 4.
|
||||
*/
|
||||
akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String **dst)
|
||||
{
|
||||
@@ -109,104 +163,104 @@ akerr_ErrorContext *akgl_path_relative_from(char *path, char *from, akgl_String
|
||||
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "path");
|
||||
FAIL_ZERO_RETURN(e, from, AKERR_NULLPOINTER, "from");
|
||||
PASS(e, akgl_heap_next_string(&dirnamestr));
|
||||
PASS(e, aksl_realpath(from, (char *)&dirnamestr->data));
|
||||
PASS(e, aksl_realpath(from, (char *)&dirnamestr->data, sizeof(dirnamestr->data)));
|
||||
dirname((char *)&dirnamestr->data);
|
||||
|
||||
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_rectangle_points(RectanglePoints *dest, SDL_FRect *rect)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
|
||||
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference");
|
||||
dest->topleft.x = rect->x;
|
||||
dest->topleft.y = rect->y;
|
||||
dest->bottomleft.x = rect->x;
|
||||
dest->bottomleft.y = rect->y + rect->h;
|
||||
dest->topright.x = rect->x + rect->w;
|
||||
dest->topright.y = rect->y;
|
||||
dest->bottomright.x = rect->x + rect->w;
|
||||
dest->bottomright.y = rect->y + rect->h;
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
|
||||
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference");
|
||||
dest->topleft.x = rect->x;
|
||||
dest->topleft.y = rect->y;
|
||||
dest->bottomleft.x = rect->x;
|
||||
dest->bottomleft.y = rect->y + rect->h;
|
||||
dest->topright.x = rect->x + rect->w;
|
||||
dest->topright.y = rect->y;
|
||||
dest->bottomright.x = rect->x + rect->w;
|
||||
dest->bottomright.y = rect->y + rect->h;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_collide_point_rectangle(point *p, RectanglePoints *rp, bool *collide)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference");
|
||||
FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
|
||||
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference");
|
||||
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
|
||||
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
|
||||
*collide = true;
|
||||
} else {
|
||||
*collide = false;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference");
|
||||
FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL RectanglePoints reference");
|
||||
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference");
|
||||
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
|
||||
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
|
||||
*collide = true;
|
||||
} else {
|
||||
*collide = false;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
|
||||
{
|
||||
RectanglePoints r1p;
|
||||
RectanglePoints r2p;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference");
|
||||
FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference");
|
||||
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL collision flag reference");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(&r1p, r1));
|
||||
CATCH(errctx, akgl_rectangle_points(&r2p, r2));
|
||||
|
||||
// is the upper left corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the upper left corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the top right corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the top right corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom left corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom left corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom right corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom right corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
RectanglePoints r1p;
|
||||
RectanglePoints r2p;
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference");
|
||||
FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference");
|
||||
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL collision flag reference");
|
||||
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
*collide = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(&r1p, r1));
|
||||
CATCH(errctx, akgl_rectangle_points(&r2p, r2));
|
||||
|
||||
// is the upper left corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topleft, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the upper left corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topleft, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the top right corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.topright, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the top right corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.topright, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom left corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomleft, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom left corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomleft, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom right corner of r1 contacting r2?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r1p.bottomright, &r2p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
// is the bottom right corner of r2 contacting r1?
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&r2p.bottomright, &r1p, collide));
|
||||
if ( *collide == true ) { SUCCEED_RETURN(errctx); }
|
||||
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
*collide = false;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
|
||||
akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer");
|
||||
FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer");
|
||||
FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal");
|
||||
SUCCEED_RETURN(errctx);
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer");
|
||||
FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer");
|
||||
FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout)
|
||||
@@ -228,7 +282,7 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
|
||||
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
s1 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
|
||||
FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
||||
|
||||
|
||||
if ( writeout != NULL ) {
|
||||
snprintf((char *)&tmpstring->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), writeout);
|
||||
FAIL_ZERO_BREAK(
|
||||
@@ -239,13 +293,13 @@ akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, in
|
||||
(char *)&tmpstring->data,
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
|
||||
SDL_RenderClear(renderer->sdl_renderer);
|
||||
|
||||
|
||||
CATCH(errctx, renderer->draw_texture(renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
||||
s2 = SDL_RenderReadPixels(renderer->sdl_renderer, &read);
|
||||
FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
||||
|
||||
|
||||
CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));
|
||||
} CLEANUP {
|
||||
if ( s1 != NULL )
|
||||
|
||||
15
src/version.c
Normal file
15
src/version.c
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @file version.c
|
||||
* @brief Implements the runtime half of the version API.
|
||||
*/
|
||||
|
||||
#include <akgl/version.h>
|
||||
|
||||
const char *akgl_version(void)
|
||||
{
|
||||
// AKGL_VERSION is baked in when this translation unit is compiled, so what
|
||||
// comes back is the version of the shared library the caller linked -- not
|
||||
// the version of the header the caller built against. That asymmetry is the
|
||||
// whole point: comparing the two detects a stale libakgl on the loader path.
|
||||
return AKGL_VERSION;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#define UNHANDLED_ERROR_TERMINATION_BEHAVIOR \
|
||||
#define UNHANDLED_ERROR_TERMINATION_BEHAVIOR \
|
||||
handle_unhandled_error(errctx);
|
||||
|
||||
#include <akerror.h>
|
||||
@@ -58,7 +58,7 @@ akerr_ErrorContext *akgl_actor_render_noop(akgl_Actor *obj)
|
||||
akerr_ErrorContext *test_registry_actor_iterator_nullpointers(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
|
||||
akerr_ErrorUnhandledErrorHandler defaulthandler = akerr_handler_unhandled_error;
|
||||
|
||||
akerr_handler_unhandled_error = handle_unhandled_error_noexit;
|
||||
@@ -151,7 +151,7 @@ akerr_ErrorContext *test_registry_actor_iterator_updaterender(void)
|
||||
} FINISH(unhandled_error_context, true);
|
||||
|
||||
akerr_handler_unhandled_error = defaulthandler;
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
|
||||
{
|
||||
akgl_Actor *testactor = NULL;
|
||||
akgl_Character *testchar = NULL;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
@@ -167,10 +167,10 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
printf("Handled\n");
|
||||
printf("Handled\n");
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_actor(&testactor));
|
||||
|
||||
@@ -178,13 +178,13 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
|
||||
testactor->layer = 0;
|
||||
testactor->updatefunc = &akgl_actor_update_noop;
|
||||
testactor->renderfunc = &akgl_actor_render_noop;
|
||||
|
||||
|
||||
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_actor(testactor));
|
||||
IGNORE(akgl_heap_release_actor(testactor));
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
printf("Handled\n");
|
||||
printf("Handled\n");
|
||||
} FINISH(errctx, true);
|
||||
|
||||
ATTEMPT {
|
||||
@@ -195,13 +195,13 @@ akerr_ErrorContext *test_akgl_actor_set_character(void)
|
||||
testactor->layer = 0;
|
||||
testactor->updatefunc = &akgl_actor_update_noop;
|
||||
testactor->renderfunc = &akgl_actor_render_noop;
|
||||
|
||||
|
||||
CATCH(errctx, akgl_character_initialize(testchar, "test"));
|
||||
|
||||
|
||||
CATCH(errctx, akgl_actor_set_character(testactor, "test"));
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_actor(testactor));
|
||||
IGNORE(akgl_heap_release_character(testchar));
|
||||
IGNORE(akgl_heap_release_actor(testactor));
|
||||
IGNORE(akgl_heap_release_character(testchar));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
@@ -248,7 +248,7 @@ akerr_ErrorContext *test_actor_manage_children(void)
|
||||
// Expected behavior
|
||||
SDL_Log("addchild throws AKERR_RELATIONSHIP when child already has a parent");
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_actor(&child));
|
||||
CATCH(errctx, parent->addchild(parent, child));
|
||||
@@ -284,7 +284,7 @@ akerr_ErrorContext *test_actor_manage_children(void)
|
||||
"Child %s was not removed from the registry",
|
||||
(char *)&tmpstring->data);
|
||||
}
|
||||
_test_actor_addchild_heaprelease_cleanup:
|
||||
_test_actor_addchild_heaprelease_cleanup:
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
@@ -309,7 +309,7 @@ _test_actor_addchild_heaprelease_cleanup:
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -903,6 +903,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_registry_init_actor());
|
||||
CATCH(errctx, akgl_registry_init_sprite());
|
||||
CATCH(errctx, akgl_registry_init_spritesheet());
|
||||
|
||||
121
tests/assets/akgl_test_mono.LICENSE.txt
Normal file
121
tests/assets/akgl_test_mono.LICENSE.txt
Normal file
@@ -0,0 +1,121 @@
|
||||
tests/assets/akgl_test_mono.ttf
|
||||
================================
|
||||
|
||||
A subset of Liberation Mono Regular, cut down to printable ASCII (U+0020 to
|
||||
U+007E) so the text suite has a font fixture that is 10 KB rather than 320 KB.
|
||||
It is monospaced, which is what the measurement tests rely on: the width of an
|
||||
N-character string is exactly N times the width of one character, in any font
|
||||
size, so the assertions do not have to hardcode glyph metrics.
|
||||
|
||||
Generated with:
|
||||
|
||||
pyftsubset /usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf \
|
||||
--unicodes=U+0020-007E --layout-features='' --no-hinting \
|
||||
--desubroutinize --name-IDs='*' --output-file=akgl_test_mono.ttf
|
||||
|
||||
then renamed to "AKGL Test Mono" through the fontTools name table. The rename is
|
||||
required, not cosmetic: "Liberation" is a Reserved Font Name under the license
|
||||
below, and a modified copy may not carry it.
|
||||
|
||||
Copyright (c) 2012 Red Hat, Inc. with Reserved Font Name Liberation.
|
||||
Digitized data copyright (c) 2010 Google Corporation with Reserved Font Arimo,
|
||||
Tinos and Cousine.
|
||||
|
||||
Licensed under the SIL Open Font License, Version 1.1, reproduced in full below.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
|
||||
PREAMBLE The goals of the Open Font License (OFL) are to stimulate
|
||||
worldwide development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to provide
|
||||
a free and open framework in which fonts may be shared and improved in
|
||||
partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves.
|
||||
The fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such.
|
||||
This may include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components
|
||||
as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting ? in part or in whole ?
|
||||
any of the components of the Original Version, by changing formats or
|
||||
by porting the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical writer
|
||||
or other person who contributed to the Font Software.
|
||||
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole, must
|
||||
be distributed entirely under this license, and must not be distributed
|
||||
under any other license. The requirement for fonts to remain under
|
||||
this license does not apply to any document created using the Font
|
||||
Software.
|
||||
|
||||
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are not met.
|
||||
|
||||
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||
DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
tests/assets/akgl_test_mono.ttf
Normal file
BIN
tests/assets/akgl_test_mono.ttf
Normal file
Binary file not shown.
625
tests/audio.c
Normal file
625
tests/audio.c
Normal file
@@ -0,0 +1,625 @@
|
||||
/**
|
||||
* @file audio.c
|
||||
* @brief Unit tests for the tone generator.
|
||||
*
|
||||
* Almost everything here drives akgl_audio_mix() by hand rather than opening a
|
||||
* device. That is not a workaround: a device pulls samples on SDL's audio
|
||||
* thread at whatever rate it likes, so a test that opened one and then asserted
|
||||
* on voice state would be racing the callback. Pulling the samples ourselves
|
||||
* makes the synthesis deterministic -- the same input produces the same
|
||||
* waveform, sample for sample, every run.
|
||||
*
|
||||
* The device is opened once, at the end, to prove akgl_audio_init() and
|
||||
* akgl_audio_shutdown() work against the dummy driver.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/audio.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/**
|
||||
* @brief Frames of output most tests generate at a time.
|
||||
*
|
||||
* Enough to hold a 10 ms attack and a 10 ms decay back to back at 44100 frames
|
||||
* per second, which is the longest single stretch any test here inspects.
|
||||
*/
|
||||
#define TEST_MIX_FRAMES 1024
|
||||
|
||||
/** @brief 441 Hz at 44100 frames per second is exactly 100 frames per cycle. */
|
||||
#define TEST_TONE_HZ 441.0f
|
||||
/** @brief Frames in one cycle of TEST_TONE_HZ. */
|
||||
#define TEST_TONE_PERIOD 100
|
||||
|
||||
/** @brief Somewhere to mix into. */
|
||||
static float32_t samples[TEST_MIX_FRAMES];
|
||||
|
||||
/** @brief Silence every voice and put the master level back. */
|
||||
static akerr_ErrorContext *reset_audio(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
||||
CATCH(errctx, akgl_audio_stop(i));
|
||||
CATCH(errctx, akgl_audio_waveform(i, AKGL_AUDIO_WAVE_SQUARE));
|
||||
CATCH(errctx, akgl_audio_envelope(i, 0, 0, 1.0f, 0));
|
||||
}
|
||||
CATCH(errctx, akgl_audio_volume(1.0f));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief Report the largest absolute sample in the first @p frames. */
|
||||
static float32_t peak_of(int frames)
|
||||
{
|
||||
float32_t peak = 0.0f;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < frames; i++ ) {
|
||||
if ( fabsf(samples[i]) > peak ) {
|
||||
peak = fabsf(samples[i]);
|
||||
}
|
||||
}
|
||||
return peak;
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_defaults(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
// Deliberately the first thing this file does, and deliberately without
|
||||
// a call to reset_audio(): a voice nobody has configured has to make a
|
||||
// sound. A zeroed voice would have a sustain of 0.0 and be silent, with
|
||||
// no error to say why, which is exactly the trap this defends against.
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
||||
"an unconfigured voice mixed to %f, expected a full-level square wave",
|
||||
samples[0]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
|
||||
"an unconfigured voice is not a square wave (%f half a cycle in)",
|
||||
samples[50]);
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_silence(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool silent = true;
|
||||
bool active = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// With nothing sounding, the mixer produces silence rather than
|
||||
// whatever was left in the buffer.
|
||||
SDL_memset((void *)&samples, 0xff, sizeof(samples));
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing an idle voice table");
|
||||
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
|
||||
TEST_ASSERT_FLAG(silent, samples[i] == 0.0f);
|
||||
}
|
||||
TEST_ASSERT(errctx, silent == true, "an idle voice table did not mix to silence");
|
||||
|
||||
for ( i = 0; i < AKGL_AUDIO_MAX_VOICES; i++ ) {
|
||||
CATCH(errctx, akgl_audio_voice_active(i, &active));
|
||||
TEST_ASSERT(errctx, active == false, "voice %d reports active with nothing playing", i);
|
||||
}
|
||||
|
||||
// Zero frames is a legal request that writes nothing.
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, 0), "mixing zero frames");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_mix(NULL, 16),
|
||||
"mixing into a NULL destination");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_mix(samples, -1),
|
||||
"mixing a negative number of frames");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_square_tone(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool firsthalf = true;
|
||||
bool secondhalf = true;
|
||||
bool active = false;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// A square wave with a flat envelope is the one waveform whose every
|
||||
// sample is known exactly: +1 for the first half of each cycle, -1 for
|
||||
// the second.
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000), "sounding a tone");
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == true, "a sounded voice does not report itself active");
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES), "mixing a square wave");
|
||||
for ( i = 0; i < (TEST_TONE_PERIOD / 2); i++ ) {
|
||||
TEST_ASSERT_FLAG(firsthalf, samples[i] == 1.0f);
|
||||
}
|
||||
for ( i = (TEST_TONE_PERIOD / 2); i < TEST_TONE_PERIOD; i++ ) {
|
||||
TEST_ASSERT_FLAG(secondhalf, samples[i] == -1.0f);
|
||||
}
|
||||
TEST_ASSERT(errctx, firsthalf == true,
|
||||
"the first half cycle of a square wave is not at full positive level");
|
||||
TEST_ASSERT(errctx, secondhalf == true,
|
||||
"the second half cycle of a square wave is not at full negative level");
|
||||
|
||||
// The wave repeats: the second cycle matches the first.
|
||||
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD] == samples[0],
|
||||
"the wave did not repeat after one period");
|
||||
TEST_ASSERT(errctx, samples[TEST_TONE_PERIOD + 60] == samples[60],
|
||||
"the wave did not repeat after one period");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, 0.0f, 100),
|
||||
"sounding a tone at zero hertz");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, -100.0f, 100),
|
||||
"sounding a tone at a negative frequency");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(0, TEST_TONE_HZ, 0),
|
||||
"sounding a tone with no duration");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_tone(-1, TEST_TONE_HZ, 100),
|
||||
"sounding a tone on a negative voice");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_tone(AKGL_AUDIO_MAX_VOICES, TEST_TONE_HZ, 100),
|
||||
"sounding a tone on a voice past the last one");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_waveforms(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool inrange = true;
|
||||
int i = 0;
|
||||
int w = 0;
|
||||
akgl_AudioWaveform shapes[4] = {
|
||||
AKGL_AUDIO_WAVE_TRIANGLE,
|
||||
AKGL_AUDIO_WAVE_SAWTOOTH,
|
||||
AKGL_AUDIO_WAVE_NOISE,
|
||||
AKGL_AUDIO_WAVE_SINE,
|
||||
};
|
||||
|
||||
ATTEMPT {
|
||||
// Every shape has to stay inside full scale and actually move. The
|
||||
// exact sample values differ per shape; these two properties do not.
|
||||
for ( w = 0; w < 4; w++ ) {
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_waveform(0, shapes[w]));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
for ( i = 0; i < TEST_MIX_FRAMES; i++ ) {
|
||||
TEST_ASSERT_FLAG(inrange, (samples[i] >= -1.0f) && (samples[i] <= 1.0f));
|
||||
}
|
||||
TEST_ASSERT_FLAG(inrange, peak_of(TEST_MIX_FRAMES) > 0.5f);
|
||||
}
|
||||
TEST_ASSERT(errctx, inrange == true,
|
||||
"a waveform either left full scale or produced nothing");
|
||||
|
||||
// The triangle is symmetric about the middle of its cycle, which the
|
||||
// square and sawtooth are not -- enough to tell it was really selected.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_waveform(0, AKGL_AUDIO_WAVE_TRIANGLE));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[25], 0.0f,
|
||||
"a triangle wave is at %f a quarter of the way up, expected 0",
|
||||
samples[25]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[50], 1.0f,
|
||||
"a triangle wave is at %f at its peak, expected 1", samples[50]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[75], 0.0f,
|
||||
"a triangle wave is at %f three quarters through, expected 0",
|
||||
samples[75]);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_waveform(0, (akgl_AudioWaveform)(AKGL_AUDIO_WAVE_SINE + 1)),
|
||||
"selecting a waveform past the last one");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_waveform(AKGL_AUDIO_MAX_VOICES, AKGL_AUDIO_WAVE_SINE),
|
||||
"selecting a waveform on a voice past the last one");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_envelope(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool rising = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// A 10 ms attack at 44100 is 441 frames, so the level should climb from
|
||||
// nothing to full across the first 441 samples of a square wave and
|
||||
// then hold at the sustain level.
|
||||
CATCH(errctx, akgl_audio_envelope(0, 10, 0, 1.0f, 0));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 0.0f,
|
||||
"an attack does not start from silence (first sample %f)", samples[0]);
|
||||
TEST_ASSERT(errctx, fabsf(samples[100]) > fabsf(samples[10]),
|
||||
"the attack is not climbing (%f at frame 10, %f at frame 100)",
|
||||
samples[10], samples[100]);
|
||||
// Frames 0..49 are the positive half of the square, so their level is
|
||||
// the envelope value directly.
|
||||
for ( i = 1; i < 50; i++ ) {
|
||||
TEST_ASSERT_FLAG(rising, samples[i] > samples[i - 1]);
|
||||
}
|
||||
TEST_ASSERT(errctx, rising == true, "the attack ramp is not monotonic");
|
||||
|
||||
// Half a millisecond of decay to a half sustain level, no attack: the
|
||||
// very first sample is at full level and it settles at the sustain.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_envelope(0, 0, 10, 0.5f, 0));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
||||
"with no attack the first sample is %f, expected full level", samples[0]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[450], -0.5f,
|
||||
"after the decay the level is %f, expected the 0.5 sustain", samples[450]);
|
||||
|
||||
// Attack and decay together, which is the case where the decay has to
|
||||
// measure from the end of the attack rather than from the start of the
|
||||
// note. 10 ms of each is 441 frames of each at 44100.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_envelope(0, 10, 10, 0.5f, 0));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[441], 1.0f,
|
||||
"at the end of the attack the level is %f, expected full", samples[441]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[661], -(1.0f - (0.5f * (220.0f / 441.0f))),
|
||||
"halfway through the decay the level is %f, expected the halfway ramp",
|
||||
samples[661]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[900], 0.5f,
|
||||
"after the decay the level is %f, expected the 0.5 sustain", samples[900]);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, -0.1f, 1),
|
||||
"setting a negative sustain level");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_envelope(0, 1, 1, 1.5f, 1),
|
||||
"setting a sustain level past full");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_envelope(AKGL_AUDIO_MAX_VOICES, 1, 1, 1.0f, 1),
|
||||
"setting an envelope on a voice past the last one");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_duration_and_release(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool active = false;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// A 10 ms gate is 441 frames. The voice is still sounding while they
|
||||
// are being generated and goes quiet on its own afterwards, without the
|
||||
// caller having to stop it.
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
|
||||
CATCH(errctx, akgl_audio_mix(samples, 440));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == true, "the voice went quiet before its gate closed");
|
||||
|
||||
CATCH(errctx, akgl_audio_mix(samples, 8));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "the voice is still sounding past its gate");
|
||||
CATCH(errctx, akgl_audio_mix(samples, 64));
|
||||
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
|
||||
"a finished voice is still producing sound (peak %f)", peak_of(64));
|
||||
|
||||
// With a release, the voice outlives its gate and fades rather than
|
||||
// stopping dead.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 10));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10));
|
||||
CATCH(errctx, akgl_audio_mix(samples, 441));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == true, "a voice with a release stopped when its gate closed");
|
||||
// The release starts from the level the gate left off at and falls from
|
||||
// there, so the end of this window has to be quieter than its start.
|
||||
CATCH(errctx, akgl_audio_mix(samples, 220));
|
||||
TEST_ASSERT(errctx, fabsf(samples[219]) < fabsf(samples[0]),
|
||||
"the release is not attenuating (%f at its start, %f 220 frames later)",
|
||||
samples[0], samples[219]);
|
||||
TEST_ASSERT(errctx, peak_of(220) > 0.0f, "the release went silent immediately");
|
||||
CATCH(errctx, akgl_audio_mix(samples, 250));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "the voice outlived its gate and its release");
|
||||
|
||||
// Stopping cuts a voice off where it stands, release and all.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_envelope(0, 0, 0, 1.0f, 1000));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, 64));
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_stop(0), "stopping a sounding voice");
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "a stopped voice still reports itself active");
|
||||
CATCH(errctx, akgl_audio_mix(samples, 64));
|
||||
TEST_ASSERT_FEQ(errctx, peak_of(64), 0.0f,
|
||||
"a stopped voice is still producing sound (peak %f)", peak_of(64));
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_stop(AKGL_AUDIO_MAX_VOICES),
|
||||
"stopping a voice past the last one");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_voice_active(AKGL_AUDIO_MAX_VOICES, &active),
|
||||
"asking about a voice past the last one");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_audio_voice_active(0, NULL),
|
||||
"asking about a voice with nowhere to put the answer");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief Mix @p ticks whole sweep steps, one step per call. */
|
||||
static akerr_ErrorContext *mix_sweep_ticks(int ticks)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
for ( i = 0; i < ticks; i++ ) {
|
||||
CATCH(errctx, akgl_audio_mix(samples, AKGL_AUDIO_SWEEP_TICK_FRAMES));
|
||||
}
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_sweep(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool active = false;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// 400 Hz climbing to 500 in 25 Hz steps: four steps, at 60 steps per
|
||||
// second. The pitch is advanced by the mixer's own frame counter, so
|
||||
// these assertions are about exact frames rather than about how often
|
||||
// the caller happened to ask for samples.
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 400.0f, 500.0f, 25.0f, 1000),
|
||||
"sounding a rising sweep");
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
|
||||
"a sweep starts at %f, expected its start frequency",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// One tick's worth of frames is not yet a step: the last frame of that
|
||||
// window is still the first tick.
|
||||
CATCH(errctx, mix_sweep_ticks(1));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
|
||||
"the pitch moved at %f before the first step was due",
|
||||
akgl_audio_voices[0].hz);
|
||||
TEST_ASSERT(errctx, peak_of(AKGL_AUDIO_SWEEP_TICK_FRAMES) > 0.5f,
|
||||
"a sweep is not making a sound (peak %f)",
|
||||
peak_of(AKGL_AUDIO_SWEEP_TICK_FRAMES));
|
||||
|
||||
// The frame after it is.
|
||||
CATCH(errctx, akgl_audio_mix(samples, 1));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 425.0f,
|
||||
"one step in, the pitch is %f, expected 425",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// It stops when it arrives rather than running past the target.
|
||||
CATCH(errctx, mix_sweep_ticks(8));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 500.0f,
|
||||
"a finished sweep sits at %f, expected its target 500",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// Downward is the same sweep with the frequencies the other way round.
|
||||
// The step stays positive; the direction is not its sign.
|
||||
CATCH(errctx, reset_audio());
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 500.0f, 400.0f, 25.0f, 1000),
|
||||
"sounding a falling sweep");
|
||||
CATCH(errctx, mix_sweep_ticks(1));
|
||||
CATCH(errctx, akgl_audio_mix(samples, 1));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 475.0f,
|
||||
"one step down, the pitch is %f, expected 475",
|
||||
akgl_audio_voices[0].hz);
|
||||
CATCH(errctx, mix_sweep_ticks(8));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 400.0f,
|
||||
"a finished falling sweep sits at %f, expected its target 400",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// Equal frequencies are a held tone, not an error: a caller translating
|
||||
// a statement that computes its own limits does not have to special
|
||||
// case them.
|
||||
CATCH(errctx, reset_audio());
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 440.0f, 440.0f, 10.0f, 1000),
|
||||
"sweeping between two identical frequencies");
|
||||
CATCH(errctx, mix_sweep_ticks(4));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, 440.0f,
|
||||
"a sweep with nowhere to go moved to %f", akgl_audio_voices[0].hz);
|
||||
|
||||
// The gate still governs: a sweep with further to go than the note lasts
|
||||
// ends with the note, part way up. 30 ms is 1323 frames, which is one
|
||||
// whole step and most of a second.
|
||||
CATCH(errctx, reset_audio());
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_sweep(0, 400.0f, 4000.0f, 25.0f, 30),
|
||||
"sounding a sweep longer than its gate");
|
||||
CATCH(errctx, mix_sweep_ticks(3));
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "a swept voice outlived its gate");
|
||||
TEST_ASSERT(errctx, akgl_audio_voices[0].hz > 400.0f,
|
||||
"a sweep cut off by its gate never stepped at all (%f)",
|
||||
akgl_audio_voices[0].hz);
|
||||
TEST_ASSERT(errctx, akgl_audio_voices[0].hz < 4000.0f,
|
||||
"a sweep cut off by its gate reached %f anyway",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
// A voice reused for a plain tone stops sweeping.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_sweep(0, 400.0f, 800.0f, 25.0f, 1000));
|
||||
CATCH(errctx, mix_sweep_ticks(2));
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000),
|
||||
"sounding a plain tone on a voice that was sweeping");
|
||||
CATCH(errctx, mix_sweep_ticks(4));
|
||||
TEST_ASSERT_FEQ(errctx, akgl_audio_voices[0].hz, TEST_TONE_HZ,
|
||||
"a plain tone on a swept voice drifted to %f",
|
||||
akgl_audio_voices[0].hz);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(AKGL_AUDIO_MAX_VOICES, 400.0f, 500.0f, 25.0f, 100),
|
||||
"sweeping a voice past the last one");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 0.0f, 500.0f, 25.0f, 100),
|
||||
"sweeping from zero hertz");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 400.0f, -1.0f, 25.0f, 100),
|
||||
"sweeping to a negative frequency");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 400.0f, 500.0f, 0.0f, 100),
|
||||
"sweeping with no step");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 500.0f, 400.0f, -25.0f, 100),
|
||||
"sweeping down with a negative step");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_audio_sweep(0, 400.0f, 500.0f, 25.0f, 0),
|
||||
"sweeping with no duration");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_volume_and_mixing(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, reset_audio());
|
||||
|
||||
// The master level scales everything.
|
||||
CATCH(errctx, akgl_audio_volume(0.25f));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 0.25f,
|
||||
"at a quarter volume the first sample is %f, expected 0.25", samples[0]);
|
||||
|
||||
CATCH(errctx, akgl_audio_volume(0.0f));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, peak_of(TEST_MIX_FRAMES), 0.0f,
|
||||
"at zero volume the peak is %f, expected silence",
|
||||
peak_of(TEST_MIX_FRAMES));
|
||||
|
||||
// Voices sum, and the sum is clamped rather than wrapped: three square
|
||||
// waves in phase at full level would be 3.0 without the clamp.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_tone(2, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 1.0f,
|
||||
"three voices in phase mixed to %f, expected a clamp at 1.0", samples[0]);
|
||||
TEST_ASSERT_FEQ(errctx, samples[50], -1.0f,
|
||||
"three voices in phase mixed to %f, expected a clamp at -1.0", samples[50]);
|
||||
|
||||
// Two voices at different levels sum to their total rather than to
|
||||
// either one of them, which is only visible below the clamp.
|
||||
CATCH(errctx, reset_audio());
|
||||
CATCH(errctx, akgl_audio_volume(0.5f));
|
||||
CATCH(errctx, akgl_audio_envelope(1, 0, 0, 0.5f, 0));
|
||||
CATCH(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_tone(1, TEST_TONE_HZ, 1000));
|
||||
CATCH(errctx, akgl_audio_mix(samples, TEST_MIX_FRAMES));
|
||||
TEST_ASSERT_FEQ(errctx, samples[0], 0.75f,
|
||||
"a full voice and a half voice at half volume mixed to %f, expected 0.75",
|
||||
samples[0]);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(-0.1f),
|
||||
"setting a negative master volume");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS, akgl_audio_volume(1.1f),
|
||||
"setting a master volume past full");
|
||||
} CLEANUP {
|
||||
IGNORE(reset_audio());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_audio_device(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
bool active = true;
|
||||
|
||||
ATTEMPT {
|
||||
// Everything above ran without a device. This is the one test that
|
||||
// opens one, so nothing that asserts on voice state runs after it.
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device");
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_init(), "opening an audio device a second time");
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_tone(0, TEST_TONE_HZ, 10), "sounding a tone on a device");
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing the audio device");
|
||||
CATCH(errctx, akgl_audio_voice_active(0, &active));
|
||||
TEST_ASSERT(errctx, active == false, "shutting down left a voice sounding");
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_audio_shutdown(), "closing an audio device that is not open");
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_audio_shutdown());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_AUDIO),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
|
||||
CATCH(errctx, test_audio_defaults());
|
||||
CATCH(errctx, test_audio_silence());
|
||||
CATCH(errctx, test_audio_square_tone());
|
||||
CATCH(errctx, test_audio_waveforms());
|
||||
CATCH(errctx, test_audio_envelope());
|
||||
CATCH(errctx, test_audio_duration_and_release());
|
||||
CATCH(errctx, test_audio_sweep());
|
||||
CATCH(errctx, test_audio_volume_and_mixing());
|
||||
CATCH(errctx, test_audio_device());
|
||||
} CLEANUP {
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
336
tests/benchutil.h
Normal file
336
tests/benchutil.h
Normal file
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* @file benchutil.h
|
||||
* @brief The timing harness behind the perf suites: run it, record it, report it, hold it to a budget.
|
||||
*
|
||||
* A benchmark here is a timed region: bench_start() takes a timestamp,
|
||||
* bench_stop() takes another and divides by the number of units of work that
|
||||
* happened in between. The unit is whatever the measurement is *about* -- one
|
||||
* call, one actor, one tile, one frame -- and it is printed alongside the
|
||||
* number so nobody has to guess what "42 ns" was 42 nanoseconds of.
|
||||
*
|
||||
* Two things make the numbers usable rather than merely present:
|
||||
*
|
||||
* 1. **Best of #AKGL_BENCH_REPETITIONS.** Calling bench_start() again with a
|
||||
* name already in the table folds the new run into the old entry and keeps
|
||||
* the *lowest* ns/op seen. A benchmark loop therefore repeats itself and the
|
||||
* harness reports the run that was interrupted least. The mean is the wrong
|
||||
* statistic here: every source of noise on a shared machine makes a run
|
||||
* slower and none makes it faster.
|
||||
* 2. **The timed region contains no error checking.** `PASS` and `CATCH` both
|
||||
* call `akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` looking
|
||||
* for a match -- more work than several of the calls being measured. Use
|
||||
* #BENCH_LOOP, which stashes the context and stops at the first failure, and
|
||||
* hand it to `PASS` after the clock has stopped.
|
||||
*
|
||||
* A budget is a per-op ceiling in nanoseconds, and a benchmark that exceeds it
|
||||
* fails the suite. Budgets are set at roughly ten times the measured baseline:
|
||||
* loose enough that a busy machine does not turn CI red, tight enough that an
|
||||
* algorithmic regression -- a linear scan that becomes quadratic, a per-frame
|
||||
* allocation that becomes a per-actor one -- cannot hide. They are enforced
|
||||
* only in an optimized build at full scale; a coverage build measures the
|
||||
* instrumentation, not the library, and reports without judging.
|
||||
*
|
||||
* Set `AKGL_BENCH_SCALE` in the environment to change how long the suite runs:
|
||||
* 0.1 for a tenth of the iterations, 10 for ten times as many. Below 1.0 the
|
||||
* budgets are not enforced, because a short run is a noisy one.
|
||||
*
|
||||
* **Under valgrind these suites become the memory-check suites**, and the scale
|
||||
* drops itself to #AKGL_BENCH_VALGRIND_SCALE without being asked. That is the
|
||||
* whole reason `ctest -T memcheck` does not need benchmark programs of its own:
|
||||
* a leak check wants every path walked *once*, and a benchmark is a program that
|
||||
* walks one path a hundred thousand times. Divide the iteration counts by two
|
||||
* thousand and the same binary is exactly the right shape for memcheck --
|
||||
* broader coverage of asset loading, drawing, and the frame loop than any unit
|
||||
* suite, at a runtime valgrind can survive. Timings from such a run are
|
||||
* meaningless and the report says so.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_BENCHUTIL_H_
|
||||
#define _AKGL_BENCHUTIL_H_
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
/** @brief How many benchmarks one suite can record. */
|
||||
#define AKGL_BENCH_MAX_RESULTS 48
|
||||
/** @brief Longest benchmark name kept, including the terminator. */
|
||||
#define AKGL_BENCH_MAX_NAME 56
|
||||
/** @brief Longest unit name kept, including the terminator. */
|
||||
#define AKGL_BENCH_MAX_UNIT 12
|
||||
/** @brief How many times a benchmark is repeated before the best run is reported. */
|
||||
#define AKGL_BENCH_REPETITIONS 5
|
||||
/** @brief Environment variable scaling every iteration count. */
|
||||
#define AKGL_BENCH_SCALE_ENV "AKGL_BENCH_SCALE"
|
||||
/** @brief Scale forced on when running under valgrind. Enough to walk every path, few enough to finish. */
|
||||
#define AKGL_BENCH_VALGRIND_SCALE 0.0005
|
||||
|
||||
/** @brief One row of the report: what was measured, how fast, and what it was allowed to cost. */
|
||||
typedef struct akgl_Benchmark {
|
||||
char name[AKGL_BENCH_MAX_NAME]; /**< What was measured. Also the key runs are merged under. */
|
||||
char unit[AKGL_BENCH_MAX_UNIT]; /**< What one op is: "call", "actor", "tile", "frame". */
|
||||
double budget_ns; /**< Per-op ceiling in nanoseconds. 0 reports without judging. */
|
||||
double best_ns; /**< Lowest ns/op seen across every run under this name. */
|
||||
uint64_t ops; /**< Units of work in the run that produced best_ns. */
|
||||
uint64_t elapsed_ns; /**< Wall time of that run. */
|
||||
int runs; /**< How many runs were folded in. */
|
||||
} akgl_Benchmark;
|
||||
|
||||
/** @brief Every benchmark this suite has recorded, in the order it first ran. */
|
||||
static akgl_Benchmark bench_results[AKGL_BENCH_MAX_RESULTS];
|
||||
/** @brief How many entries of #bench_results are in use. */
|
||||
static int bench_result_count = 0;
|
||||
/** @brief The entry bench_stop() will write to, claimed by bench_start(). */
|
||||
static akgl_Benchmark *bench_running = NULL;
|
||||
/** @brief `SDL_GetTicksNS()` at the last bench_start(). */
|
||||
static uint64_t bench_started_ns = 0;
|
||||
|
||||
/**
|
||||
* @brief Report whether this process is running under valgrind.
|
||||
*
|
||||
* Detected from `LD_PRELOAD`, which valgrind fills with its own
|
||||
* `vgpreload_*.so` before handing the process over. That is a deliberate choice
|
||||
* over `RUNNING_ON_VALGRIND` from `valgrind/valgrind.h`: the macro is exact, but
|
||||
* it makes the test suite fail to compile anywhere the valgrind headers are not
|
||||
* installed, and a benchmark is not worth a build dependency. The cost of the
|
||||
* check being wrong is a slow run or a fast one, never a wrong answer.
|
||||
*/
|
||||
static bool bench_under_valgrind(void)
|
||||
{
|
||||
static int detected = -1;
|
||||
const char *preload = NULL;
|
||||
|
||||
if ( detected >= 0 ) {
|
||||
return ( detected == 1 );
|
||||
}
|
||||
detected = 0;
|
||||
preload = SDL_getenv("LD_PRELOAD");
|
||||
if ( preload != NULL && strstr(preload, "vgpreload") != NULL ) {
|
||||
detected = 1;
|
||||
}
|
||||
return ( detected == 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Multiplier applied to every iteration count, from `AKGL_BENCH_SCALE`.
|
||||
*
|
||||
* Read once and cached. Anything unparseable, negative, or absent gives 1.0.
|
||||
* Under valgrind the result is capped at #AKGL_BENCH_VALGRIND_SCALE -- the
|
||||
* smaller of the two wins, so asking for an even shorter run still works.
|
||||
*/
|
||||
static double bench_scale(void)
|
||||
{
|
||||
static double scale = -1.0;
|
||||
const char *env = NULL;
|
||||
|
||||
if ( scale >= 0.0 ) {
|
||||
return scale;
|
||||
}
|
||||
scale = 1.0;
|
||||
env = SDL_getenv(AKGL_BENCH_SCALE_ENV);
|
||||
if ( env != NULL ) {
|
||||
scale = SDL_atof(env);
|
||||
if ( scale <= 0.0 ) {
|
||||
scale = 1.0;
|
||||
}
|
||||
}
|
||||
if ( bench_under_valgrind() == true && scale > AKGL_BENCH_VALGRIND_SCALE ) {
|
||||
scale = AKGL_BENCH_VALGRIND_SCALE;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Scale a nominal iteration count, never down to zero.
|
||||
* @param count The count the benchmark was written for.
|
||||
* @return @p count times the scale factor, at least 1.
|
||||
*/
|
||||
static int bench_iterations(int count)
|
||||
{
|
||||
int scaled = (int)((double)count * bench_scale());
|
||||
|
||||
if ( scaled < 1 ) {
|
||||
scaled = 1;
|
||||
}
|
||||
return scaled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Report whether budgets are being enforced in this run.
|
||||
*
|
||||
* An unoptimized build measures the instrumentation rather than the library, a
|
||||
* scaled-down run is too short to trust, and a run under valgrind is measuring
|
||||
* valgrind. All three report without failing.
|
||||
*/
|
||||
static bool bench_budgets_enforced(void)
|
||||
{
|
||||
if ( bench_under_valgrind() == true ) {
|
||||
return false;
|
||||
}
|
||||
#ifdef __OPTIMIZE__
|
||||
return ( bench_scale() >= 1.0 );
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start timing, claiming or reusing the table entry named @p name.
|
||||
*
|
||||
* @param name What is being measured. Truncated at #AKGL_BENCH_MAX_NAME.
|
||||
* Reusing a name folds this run into that entry.
|
||||
* @param unit What one op is. Truncated at #AKGL_BENCH_MAX_UNIT.
|
||||
* @param budget_ns Per-op ceiling in nanoseconds; 0 to report only. The value
|
||||
* from the first run under a name wins.
|
||||
*
|
||||
* @note The table is fixed. Past #AKGL_BENCH_MAX_RESULTS entries the run is
|
||||
* dropped with a message rather than overwriting somebody else's row.
|
||||
*/
|
||||
static void bench_start(char *name, char *unit, double budget_ns)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
bench_running = NULL;
|
||||
for ( i = 0; i < bench_result_count; i++ ) {
|
||||
if ( strncmp(bench_results[i].name, name, AKGL_BENCH_MAX_NAME - 1) == 0 ) {
|
||||
bench_running = &bench_results[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( bench_running == NULL ) {
|
||||
if ( bench_result_count >= AKGL_BENCH_MAX_RESULTS ) {
|
||||
SDL_Log("benchutil: no room for benchmark '%s', raise AKGL_BENCH_MAX_RESULTS", name);
|
||||
return;
|
||||
}
|
||||
bench_running = &bench_results[bench_result_count];
|
||||
bench_result_count += 1;
|
||||
memset(bench_running, 0x00, sizeof(akgl_Benchmark));
|
||||
strncpy(bench_running->name, name, AKGL_BENCH_MAX_NAME - 1);
|
||||
strncpy(bench_running->unit, unit, AKGL_BENCH_MAX_UNIT - 1);
|
||||
bench_running->budget_ns = budget_ns;
|
||||
bench_running->best_ns = -1.0;
|
||||
}
|
||||
bench_started_ns = SDL_GetTicksNS();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop timing and keep the run if it beat every previous one.
|
||||
* @param ops Units of work done since bench_start(). Zero is ignored -- a
|
||||
* benchmark that did nothing has no rate to report.
|
||||
*/
|
||||
static void bench_stop(uint64_t ops)
|
||||
{
|
||||
uint64_t elapsed = SDL_GetTicksNS() - bench_started_ns;
|
||||
double per_op = 0.0;
|
||||
|
||||
if ( bench_running == NULL || ops == 0 ) {
|
||||
return;
|
||||
}
|
||||
per_op = (double)elapsed / (double)ops;
|
||||
bench_running->runs += 1;
|
||||
if ( bench_running->best_ns < 0.0 || per_op < bench_running->best_ns ) {
|
||||
bench_running->best_ns = per_op;
|
||||
bench_running->ops = ops;
|
||||
bench_running->elapsed_ns = elapsed;
|
||||
}
|
||||
bench_running = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run @p stmt @p count times, stopping at the first error.
|
||||
*
|
||||
* The timed loop deliberately holds no `PASS` or `CATCH`: their validity check
|
||||
* walks the whole error array and costs more than some of the calls being
|
||||
* measured. Hand @p errvar to `PASS` once the clock has stopped.
|
||||
*
|
||||
* @param errvar Receives the first non-`NULL` context returned, or `NULL`.
|
||||
* @param i Loop variable, declared by the caller.
|
||||
* @param count How many times to run @p stmt.
|
||||
* @param stmt The call under test. Must evaluate to an `akerr_ErrorContext *`.
|
||||
*/
|
||||
#define BENCH_LOOP(errvar, i, count, stmt) \
|
||||
for ( i = 0; i < (count); i++ ) { \
|
||||
errvar = (stmt); \
|
||||
if ( errvar != NULL ) { \
|
||||
break; \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the recorded benchmarks as a table and count the ones over budget.
|
||||
*
|
||||
* The table goes to stdout so `ctest --output-on-failure` and a plain run of the
|
||||
* executable both show it. Rates are printed as ops per second, which is the
|
||||
* number a frame budget is actually built from.
|
||||
*
|
||||
* @return How many benchmarks exceeded their budget. Always 0 when
|
||||
* bench_budgets_enforced() is false.
|
||||
*/
|
||||
static int bench_report(void)
|
||||
{
|
||||
int i = 0;
|
||||
int over = 0;
|
||||
bool enforced = bench_budgets_enforced();
|
||||
char *verdict = NULL;
|
||||
|
||||
printf("\n");
|
||||
if ( bench_under_valgrind() == true ) {
|
||||
printf("running under valgrind: this is a memory check, not a measurement.\n");
|
||||
printf("The timings below are valgrind's and mean nothing about libakgl.\n");
|
||||
}
|
||||
printf("scale %.4gx, best of %d runs, budgets %s\n",
|
||||
bench_scale(),
|
||||
AKGL_BENCH_REPETITIONS,
|
||||
enforced ? "enforced" : "reported only");
|
||||
printf("%-54s %-6s %9s %12s %16s %10s %s\n",
|
||||
"benchmark", "unit", "ops", "ns/op", "ops/sec", "budget", "verdict");
|
||||
printf("%-54s %-6s %9s %12s %16s %10s %s\n",
|
||||
"------------------------------------------------------",
|
||||
"------", "---------", "------------", "----------------", "----------", "-------");
|
||||
for ( i = 0; i < bench_result_count; i++ ) {
|
||||
verdict = "-";
|
||||
if ( bench_results[i].budget_ns > 0.0 ) {
|
||||
if ( bench_results[i].best_ns > bench_results[i].budget_ns ) {
|
||||
verdict = enforced ? "OVER" : "over";
|
||||
if ( enforced ) {
|
||||
over += 1;
|
||||
}
|
||||
} else {
|
||||
verdict = "ok";
|
||||
}
|
||||
}
|
||||
printf("%-54s %-6s %9llu %12.1f %16.0f %10.0f %s\n",
|
||||
bench_results[i].name,
|
||||
bench_results[i].unit,
|
||||
(unsigned long long)bench_results[i].ops,
|
||||
bench_results[i].best_ns,
|
||||
( bench_results[i].best_ns > 0.0 ) ? (1000000000.0 / bench_results[i].best_ns) : 0.0,
|
||||
bench_results[i].budget_ns,
|
||||
verdict);
|
||||
}
|
||||
printf("\n");
|
||||
fflush(stdout);
|
||||
return over;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fail the enclosing ATTEMPT block if any benchmark blew its budget.
|
||||
*
|
||||
* The report is printed either way -- a run that fails still has to say what the
|
||||
* numbers were.
|
||||
*/
|
||||
#define BENCH_REPORT_BREAK(e) \
|
||||
{ \
|
||||
int __bench_over = bench_report(); \
|
||||
if ( __bench_over > 0 ) { \
|
||||
FAIL_BREAK( \
|
||||
e, \
|
||||
AKGL_ERR_BEHAVIOR, \
|
||||
"%d benchmark(s) exceeded their budget", \
|
||||
__bench_over); \
|
||||
} \
|
||||
}
|
||||
|
||||
#endif // _AKGL_BENCHUTIL_H_
|
||||
@@ -3,29 +3,29 @@
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int mask = 0;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_ALIVE);
|
||||
if ( mask != AKGL_ACTOR_STATE_ALIVE )
|
||||
return 1;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT) )
|
||||
return 1;
|
||||
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_ALIVE);
|
||||
if ( mask != (AKGL_ACTOR_STATE_FACE_LEFT) )
|
||||
return 1;
|
||||
AKGL_BITMASK_CLEAR(mask);
|
||||
if ( mask != 0 )
|
||||
return 1;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
if ( !(AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_FACE_LEFT)) )
|
||||
return 1;
|
||||
mask = AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP;
|
||||
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
if ( mask != AKGL_ACTOR_STATE_ALIVE )
|
||||
return 1;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_DOWN);
|
||||
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN) )
|
||||
return 1;
|
||||
return 0;
|
||||
int mask = 0;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_ALIVE);
|
||||
if ( mask != AKGL_ACTOR_STATE_ALIVE )
|
||||
return 1;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT) )
|
||||
return 1;
|
||||
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_ALIVE);
|
||||
if ( mask != (AKGL_ACTOR_STATE_FACE_LEFT) )
|
||||
return 1;
|
||||
AKGL_BITMASK_CLEAR(mask);
|
||||
if ( mask != 0 )
|
||||
return 1;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_LEFT);
|
||||
if ( !(AKGL_BITMASK_HAS(mask, AKGL_ACTOR_STATE_FACE_LEFT)) )
|
||||
return 1;
|
||||
mask = AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_UP;
|
||||
AKGL_BITMASK_DEL(mask, AKGL_ACTOR_STATE_FACE_ALL);
|
||||
if ( mask != AKGL_ACTOR_STATE_ALIVE )
|
||||
return 1;
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_MOVING_DOWN);
|
||||
AKGL_BITMASK_ADD(mask, AKGL_ACTOR_STATE_FACE_DOWN);
|
||||
if ( mask != (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_MOVING_DOWN | AKGL_ACTOR_STATE_FACE_DOWN) )
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ akerr_ErrorContext *test_character_iterate_state_sprites()
|
||||
akgl_Sprite *testsprite = NULL;
|
||||
akgl_Sprite *testsprite2 = NULL;
|
||||
akgl_Iterator opflags = {.flags = AKGL_ITERATOR_OP_RELEASE, .layerid = 0};
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_character(&testchar));
|
||||
@@ -126,7 +126,7 @@ akerr_ErrorContext *test_character_iterate_state_sprites()
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_sprite(testsprite));
|
||||
IGNORE(akgl_heap_release_sprite(testsprite2));
|
||||
IGNORE(akgl_heap_release_character(testchar));
|
||||
IGNORE(akgl_heap_release_character(testchar));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
@@ -140,7 +140,7 @@ akerr_ErrorContext *test_akgl_character_load_json()
|
||||
akgl_Sprite *comparesprite = NULL;
|
||||
int tsrc = 0;
|
||||
int tsrc2 = 0;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_character(&testcharacter));
|
||||
@@ -158,7 +158,7 @@ akerr_ErrorContext *test_akgl_character_load_json()
|
||||
testsprite2,
|
||||
AKERR_KEY,
|
||||
"Sprite 2 loaded from json but not in registry");
|
||||
|
||||
|
||||
CATCH(errctx, akgl_character_load_json("assets/testcharacter.json"));
|
||||
testcharacter = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
@@ -195,12 +195,13 @@ int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_character", 640, 480, SDL_WINDOW_HIDDEN, &window, &renderer)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
|
||||
int numsprites = 8;
|
||||
char *spritepaths[] = {
|
||||
"assets/sprites/little_guy_walking_left.json",
|
||||
"assets/sprites/little_guy_walking_right.json",
|
||||
"assets/sprites/little_guy_walking_up.json",
|
||||
"assets/sprites/little_guy_walking_down.json",
|
||||
"assets/sprites/little_guy_facing_left.json",
|
||||
"assets/sprites/little_guy_facing_right.json",
|
||||
"assets/sprites/little_guy_facing_up.json",
|
||||
"assets/sprites/little_guy_facing_down.json"
|
||||
"assets/sprites/little_guy_walking_left.json",
|
||||
"assets/sprites/little_guy_walking_right.json",
|
||||
"assets/sprites/little_guy_walking_up.json",
|
||||
"assets/sprites/little_guy_walking_down.json",
|
||||
"assets/sprites/little_guy_facing_left.json",
|
||||
"assets/sprites/little_guy_facing_right.json",
|
||||
"assets/sprites/little_guy_facing_up.json",
|
||||
"assets/sprites/little_guy_facing_down.json"
|
||||
};
|
||||
|
||||
int main(void)
|
||||
@@ -34,15 +34,15 @@ int main(void)
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL3GControlMap *controlmap;
|
||||
actor *actorptr = NULL;
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
|
||||
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
}
|
||||
@@ -55,7 +55,7 @@ int main(void)
|
||||
strcpy((char *)&game.uri, "net.aklabs.libakgl.charviewer");
|
||||
game.screenwidth = 640;
|
||||
game.screenheight = 480;
|
||||
|
||||
|
||||
CATCH(errctx, akgl_GAME_init());
|
||||
|
||||
for ( int i = 0; i < numsprites ; i++) {
|
||||
@@ -96,7 +96,7 @@ int main(void)
|
||||
controlmap->controls[1].target_del_state_off = AKGL_ACTOR_STATE_MOVING_UP;
|
||||
controlmap->controls[1].event_on = SDL_EVENT_KEY_DOWN;
|
||||
controlmap->controls[1].event_off = SDL_EVENT_KEY_UP;
|
||||
|
||||
|
||||
// Move left
|
||||
controlmap->controls[2].key = SDLK_LEFT;
|
||||
//controlmap->controls[2].target_state_gate = AKGL_ACTOR_STATE_MOVING_LEFT;
|
||||
@@ -105,13 +105,13 @@ int main(void)
|
||||
controlmap->controls[2].target_del_state_off = AKGL_ACTOR_STATE_MOVING_LEFT;
|
||||
controlmap->controls[2].event_on = SDL_EVENT_KEY_DOWN;
|
||||
controlmap->controls[2].event_off = SDL_EVENT_KEY_UP;
|
||||
|
||||
|
||||
// Move right
|
||||
controlmap->controls[3].key = SDLK_RIGHT;
|
||||
//controlmap->controls[3].target_state_gate = AKGL_ACTOR_STATE_MOVING_RIGHT;
|
||||
controlmap->controls[3].target_add_state_on = AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT;
|
||||
controlmap->controls[3].target_del_state_on = AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_FACE_ALL;
|
||||
controlmap->controls[3].target_del_state_off = AKGL_ACTOR_STATE_MOVING_RIGHT;
|
||||
controlmap->controls[3].target_del_state_off = AKGL_ACTOR_STATE_MOVING_RIGHT;
|
||||
controlmap->controls[3].event_on = SDL_EVENT_KEY_DOWN;
|
||||
controlmap->controls[3].event_off = SDL_EVENT_KEY_UP;
|
||||
} CLEANUP {
|
||||
@@ -120,6 +120,6 @@ int main(void)
|
||||
LOG_ERROR(errctx);
|
||||
return 1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
// akgl/controller.h uses akgl_Actor without declaring it, so actor.h has to
|
||||
// come first, the same way src/controller.c reaches it through akgl/game.h.
|
||||
// For the akgl_Actor_cmhf_* handlers these tests bind. akgl/controller.h pulls
|
||||
// actor.h in for itself now; tests/headers.c is what keeps it doing so.
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/game.h>
|
||||
@@ -88,6 +88,26 @@ static void make_key_event(SDL_Event *event, uint32_t type, SDL_KeyboardID which
|
||||
event->key.key = key;
|
||||
}
|
||||
|
||||
/** @brief Build a synthetic keyboard event carrying modifier state. */
|
||||
static void make_key_event_mod(SDL_Event *event, uint32_t type, SDL_KeyboardID which, SDL_Keycode key, SDL_Keymod mod)
|
||||
{
|
||||
make_key_event(event, type, which, key);
|
||||
event->key.mod = mod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build the text input event SDL sends after a key press composes.
|
||||
*
|
||||
* @p text is not copied by SDL or by the library at this point, so callers pass
|
||||
* a string literal.
|
||||
*/
|
||||
static void make_text_event(SDL_Event *event, const char *text)
|
||||
{
|
||||
memset(event, 0x00, sizeof(SDL_Event));
|
||||
event->type = SDL_EVENT_TEXT_INPUT;
|
||||
event->text.text = text;
|
||||
}
|
||||
|
||||
/** @brief Build a synthetic gamepad button event. */
|
||||
static void make_button_event(SDL_Event *event, uint32_t type, SDL_JoystickID which, uint8_t button)
|
||||
{
|
||||
@@ -524,6 +544,309 @@ akerr_ErrorContext *test_controller_device_enumeration(void)
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_poll_key(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
int keycode = -1;
|
||||
bool available = true;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, make_player());
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
|
||||
// An empty buffer answers "no key waiting" and succeeds.
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling an empty key buffer");
|
||||
TEST_ASSERT(e, available == false,
|
||||
"polling an empty buffer reported a key was available");
|
||||
TEST_ASSERT(e, keycode == 0,
|
||||
"polling an empty buffer left keycode at %d, expected 0", keycode);
|
||||
|
||||
// A key press pumped through the handler is drained by the poller,
|
||||
// exactly once.
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a key press with no control maps installed");
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling for that key");
|
||||
TEST_ASSERT(e, available == true, "the pressed key was not available to the poller");
|
||||
TEST_ASSERT(e, keycode == SDLK_A,
|
||||
"the poller returned keycode %d, expected %d", keycode, (int)SDLK_A);
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling again");
|
||||
TEST_ASSERT(e, available == false, "the same keystroke was delivered twice");
|
||||
|
||||
// Key releases are not keystrokes.
|
||||
make_key_event(&event, SDL_EVENT_KEY_UP, TEST_KBID, SDLK_A);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a key release");
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a release");
|
||||
TEST_ASSERT(e, available == false, "a key release was buffered as a keystroke");
|
||||
|
||||
// Keys come back in the order they were pressed.
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_1);
|
||||
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_2);
|
||||
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_3);
|
||||
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the first key");
|
||||
TEST_ASSERT(e, keycode == SDLK_1, "the first key out was %d, expected %d",
|
||||
keycode, (int)SDLK_1);
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the second key");
|
||||
TEST_ASSERT(e, keycode == SDLK_2, "the second key out was %d, expected %d",
|
||||
keycode, (int)SDLK_2);
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "draining the third key");
|
||||
TEST_ASSERT(e, keycode == SDLK_3, "the third key out was %d, expected %d",
|
||||
keycode, (int)SDLK_3);
|
||||
|
||||
// A key that a control map also acts on still reaches the poller, so a
|
||||
// game and an embedded interpreter can share one keyboard.
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
CATCH(e, akgl_controller_default(0, "player", TEST_KBID, TEST_JSID));
|
||||
player->state = 0;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a bound key press");
|
||||
TEST_ASSERT(e, AKGL_BITMASK_HAS(player->state, AKGL_ACTOR_STATE_MOVING_LEFT),
|
||||
"the bound key stopped driving the actor (state %d)", player->state);
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling for a key a control map claimed");
|
||||
TEST_ASSERT(e, available == true, "a bound key never reached the key buffer");
|
||||
TEST_ASSERT(e, keycode == SDLK_LEFT, "the poller returned keycode %d, expected %d",
|
||||
keycode, (int)SDLK_LEFT);
|
||||
|
||||
// Flushing discards the backlog.
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B);
|
||||
CATCH(e, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(e, akgl_controller_flush_keys(), "flushing a buffer with a key in it");
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available), "polling after a flush");
|
||||
TEST_ASSERT(e, available == false, "a flush left a keystroke behind");
|
||||
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(NULL, &available),
|
||||
"polling into a NULL keycode");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_controller_poll_key(&keycode, NULL),
|
||||
"polling into a NULL availability flag");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
IGNORE(akgl_controller_flush_keys());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_poll_key_overflow(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
int keycode = -1;
|
||||
bool available = true;
|
||||
bool ordered = true;
|
||||
bool pumped = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(e, akgl_controller_flush_keys());
|
||||
|
||||
// Fill the buffer exactly, then press one more. The overflowing key is
|
||||
// the one that is dropped -- what was typed first survives.
|
||||
for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER + 1; i++ ) {
|
||||
akerr_ErrorContext *pumpresult = NULL;
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_A + i);
|
||||
pumpresult = akgl_controller_handle_event(&appstate_placeholder, &event);
|
||||
if ( pumpresult != NULL ) {
|
||||
pumpresult->handled = true;
|
||||
pumpresult = akerr_release_error(pumpresult);
|
||||
pumped = false;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT(e, pumped == true, "dispatching the overflow key presses failed");
|
||||
|
||||
for ( i = 0; i < AKGL_CONTROLLER_KEY_BUFFER; i++ ) {
|
||||
akerr_ErrorContext *pollresult = akgl_controller_poll_key(&keycode, &available);
|
||||
if ( pollresult != NULL ) {
|
||||
pollresult->handled = true;
|
||||
pollresult = akerr_release_error(pollresult);
|
||||
ordered = false;
|
||||
}
|
||||
TEST_ASSERT_FLAG(ordered, available == true);
|
||||
TEST_ASSERT_FLAG(ordered, keycode == (int)(SDLK_A + i));
|
||||
}
|
||||
TEST_ASSERT(e, ordered == true,
|
||||
"a full buffer did not return the first %d keys in order",
|
||||
AKGL_CONTROLLER_KEY_BUFFER);
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling after draining a full buffer");
|
||||
TEST_ASSERT(e, available == false,
|
||||
"the key pressed past capacity was buffered anyway (keycode %d)", keycode);
|
||||
|
||||
// The buffer is reusable after an overflow rather than wedged.
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_Z);
|
||||
TEST_EXPECT_OK(e, akgl_controller_handle_event(&appstate_placeholder, &event),
|
||||
"dispatching a key after an overflow");
|
||||
TEST_EXPECT_OK(e, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling after an overflow");
|
||||
TEST_ASSERT(e, available == true, "the buffer stayed full after being drained");
|
||||
TEST_ASSERT(e, keycode == SDLK_Z, "the poller returned keycode %d, expected %d",
|
||||
keycode, (int)SDLK_Z);
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
IGNORE(akgl_controller_flush_keys());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_controller_poll_keystroke(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Event event;
|
||||
akgl_Keystroke keystroke;
|
||||
int keycode = -1;
|
||||
bool available = true;
|
||||
|
||||
ATTEMPT {
|
||||
reset_control_maps();
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
|
||||
// An empty buffer answers "nothing waiting" and zeroes the destination.
|
||||
memset(&keystroke, 0xff, sizeof(akgl_Keystroke));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling an empty keystroke buffer");
|
||||
TEST_ASSERT(errctx, available == false,
|
||||
"polling an empty buffer reported a keystroke was available");
|
||||
TEST_ASSERT(errctx, keystroke.key == 0,
|
||||
"polling an empty buffer left a keycode of %d behind", (int)keystroke.key);
|
||||
TEST_ASSERT(errctx, keystroke.text[0] == '\0',
|
||||
"polling an empty buffer left text behind");
|
||||
|
||||
// The case the whole thing exists for: a shifted key. The keycode is
|
||||
// the unshifted one, so the composed character is the only place the
|
||||
// double quote can come from.
|
||||
make_key_event_mod(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_APOSTROPHE, SDL_KMOD_LSHIFT);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
make_text_event(&event, "\"");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for a shifted key");
|
||||
TEST_ASSERT(errctx, available == true, "a shifted key press never reached the buffer");
|
||||
TEST_ASSERT(errctx, keystroke.key == SDLK_APOSTROPHE,
|
||||
"the keystroke reported keycode %d, expected %d",
|
||||
(int)keystroke.key, (int)SDLK_APOSTROPHE);
|
||||
TEST_ASSERT(errctx, (keystroke.mod & SDL_KMOD_SHIFT) != 0,
|
||||
"the keystroke lost its shift state (mod %d)", (int)keystroke.mod);
|
||||
TEST_ASSERT(errctx, strcmp(keystroke.text, "\"") == 0,
|
||||
"the keystroke composed to \"%s\", expected a double quote", keystroke.text);
|
||||
|
||||
// A key that composes to nothing still arrives, with empty text -- that
|
||||
// is how a line editor recognises Backspace and the arrows.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_LEFT);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for a non-printing key");
|
||||
TEST_ASSERT(errctx, keystroke.key == SDLK_LEFT,
|
||||
"a non-printing key reported keycode %d, expected %d",
|
||||
(int)keystroke.key, (int)SDLK_LEFT);
|
||||
TEST_ASSERT(errctx, keystroke.text[0] == '\0',
|
||||
"a non-printing key composed to \"%s\", expected nothing", keystroke.text);
|
||||
|
||||
// Text with no key press behind it -- an input method, or a character
|
||||
// finished by a dead key -- is buffered on its own rather than dropped.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_text_event(&event, "e");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for composed text with no key press");
|
||||
TEST_ASSERT(errctx, available == true, "composed text with no key press was dropped");
|
||||
TEST_ASSERT(errctx, keystroke.key == 0,
|
||||
"text with no key press reported keycode %d, expected 0", (int)keystroke.key);
|
||||
TEST_ASSERT(errctx, strcmp(keystroke.text, "e") == 0,
|
||||
"text with no key press composed to \"%s\", expected \"e\"", keystroke.text);
|
||||
TEST_ASSERT(errctx, keystroke.mod == 0,
|
||||
"a text-only entry reported modifiers %d; the character it carries already "
|
||||
"reflects them", (int)keystroke.mod);
|
||||
|
||||
// The keycode form has nowhere to report that entry, so it discards it
|
||||
// on the way past rather than handing back a keystroke with no key.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_text_event(&event, "e");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_B);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling by keycode past a text-only entry");
|
||||
TEST_ASSERT(errctx, available == true, "the key behind a text-only entry was lost");
|
||||
TEST_ASSERT(errctx, keycode == SDLK_B,
|
||||
"polling by keycode returned %d, expected %d", keycode, (int)SDLK_B);
|
||||
|
||||
// Both pollers drain the same buffer.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_C);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_key(&keycode, &available),
|
||||
"polling by keycode after the same key was taken as a keystroke");
|
||||
TEST_ASSERT(errctx, available == false, "one keystroke was delivered to both pollers");
|
||||
|
||||
// Text that arrives after its press has already been drained becomes an
|
||||
// entry of its own instead of being attached to somebody else's key.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_key_event(&event, SDL_EVENT_KEY_DOWN, TEST_KBID, SDLK_D);
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
CATCH(errctx, akgl_controller_poll_keystroke(&keystroke, &available));
|
||||
make_text_event(&event, "d");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for text that outlived its key press");
|
||||
TEST_ASSERT(errctx, available == true, "text that outlived its key press was dropped");
|
||||
TEST_ASSERT(errctx, keystroke.key == 0,
|
||||
"text that outlived its key press was attached to keycode %d",
|
||||
(int)keystroke.key);
|
||||
|
||||
// More text than an entry holds is cut on a character boundary, never
|
||||
// through the middle of one. Each of these is two bytes, so three fit
|
||||
// in the eight-byte field and the fourth does not.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_text_event(&event, "\xce\xb1\xce\xb1\xce\xb1\xce\xb1");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for text longer than one entry holds");
|
||||
TEST_ASSERT(errctx, strcmp(keystroke.text, "\xce\xb1\xce\xb1\xce\xb1") == 0,
|
||||
"oversized text was truncated to \"%s\", expected three whole characters",
|
||||
keystroke.text);
|
||||
|
||||
// A sequence the string ends in the middle of is dropped rather than
|
||||
// copied by reading past the terminator to find its other half.
|
||||
CATCH(errctx, akgl_controller_flush_keys());
|
||||
make_text_event(&event, "a\xce");
|
||||
CATCH(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
TEST_EXPECT_OK(errctx, akgl_controller_poll_keystroke(&keystroke, &available),
|
||||
"polling for text that ends mid-character");
|
||||
TEST_ASSERT(errctx, strcmp(keystroke.text, "a") == 0,
|
||||
"a truncated character came back as \"%s\", expected just the whole one",
|
||||
keystroke.text);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_controller_poll_keystroke(NULL, &available),
|
||||
"polling into a NULL keystroke");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_controller_poll_keystroke(&keystroke, NULL),
|
||||
"polling into a NULL availability flag");
|
||||
} CLEANUP {
|
||||
reset_control_maps();
|
||||
IGNORE(akgl_controller_flush_keys());
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -532,6 +855,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD),
|
||||
@@ -550,6 +874,9 @@ int main(void)
|
||||
CATCH(errctx, test_controller_gamepad_button_handlers());
|
||||
CATCH(errctx, test_controller_device_events());
|
||||
CATCH(errctx, test_controller_device_enumeration());
|
||||
CATCH(errctx, test_controller_poll_key());
|
||||
CATCH(errctx, test_controller_poll_key_overflow());
|
||||
CATCH(errctx, test_controller_poll_keystroke());
|
||||
} CLEANUP {
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
|
||||
600
tests/draw.c
Normal file
600
tests/draw.c
Normal file
@@ -0,0 +1,600 @@
|
||||
/**
|
||||
* @file draw.c
|
||||
* @brief Unit tests for the immediate-mode drawing primitives.
|
||||
*
|
||||
* Everything here draws into a small software renderer under the dummy video
|
||||
* driver and then reads the target back with SDL_RenderReadPixels, so the
|
||||
* assertions are about pixels that actually changed rather than about SDL
|
||||
* having been called. No window is shown and no display is required.
|
||||
*
|
||||
* The target is deliberately tiny: at 64x64 a full readback is 16 KB, which
|
||||
* makes it cheap to read the whole thing back after every operation.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/renderer.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Width and height of the offscreen target every test draws into. */
|
||||
#define TEST_TARGET_SIZE 64
|
||||
|
||||
/** @brief Opaque black, what each test clears the target to. */
|
||||
static const SDL_Color testblack = { 0x00, 0x00, 0x00, 0xff };
|
||||
/** @brief The color most tests draw with. */
|
||||
static const SDL_Color testred = { 0xff, 0x00, 0x00, 0xff };
|
||||
/** @brief A second color, for tests that need to tell two marks apart. */
|
||||
static const SDL_Color testgreen = { 0x00, 0xff, 0x00, 0xff };
|
||||
|
||||
/** @brief Clear the whole target to opaque black. */
|
||||
static akerr_ErrorContext *clear_target(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_SetRenderDrawColor(renderer->sdl_renderer, 0x00, 0x00, 0x00, 0xff),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_RETURN(
|
||||
errctx,
|
||||
SDL_RenderClear(renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Report whether one pixel of @p shot carries @p color.
|
||||
*
|
||||
* Alpha is not compared: the render target's own alpha depends on the format
|
||||
* SDL picked for it, and none of these tests draw translucently.
|
||||
*/
|
||||
static bool pixel_is(SDL_Surface *shot, int x, int y, SDL_Color color)
|
||||
{
|
||||
uint8_t r = 0;
|
||||
uint8_t g = 0;
|
||||
uint8_t b = 0;
|
||||
uint8_t a = 0;
|
||||
|
||||
if ( shot == NULL ) {
|
||||
return false;
|
||||
}
|
||||
if ( !SDL_ReadSurfacePixel(shot, x, y, &r, &g, &b, &a) ) {
|
||||
return false;
|
||||
}
|
||||
return ((r == color.r) && (g == color.g) && (b == color.b));
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_point(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_point(renderer, 10.0f, 20.0f, testred),
|
||||
"plotting one pixel");
|
||||
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 10, 20, testred),
|
||||
"the plotted pixel at 10,20 is not the color it was drawn with");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 11, 20, testblack),
|
||||
"plotting one pixel also changed its neighbour at 11,20");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 10, 21, testblack),
|
||||
"plotting one pixel also changed the pixel below it");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_point(NULL, 0.0f, 0.0f, testred),
|
||||
"plotting through a NULL backend");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_line(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
bool onthe_line = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, clear_target());
|
||||
// A vertical line, so every pixel of it is known without reasoning
|
||||
// about how SDL rasterises a diagonal.
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_line(renderer, 5.0f, 4.0f, 5.0f, 12.0f, testred),
|
||||
"drawing a vertical line");
|
||||
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
for ( i = 4; i <= 12; i++ ) {
|
||||
TEST_ASSERT_FLAG(onthe_line, pixel_is(shot, 5, i, testred));
|
||||
}
|
||||
TEST_ASSERT(errctx, onthe_line == true,
|
||||
"the vertical line from 5,4 to 5,12 has a gap in it");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 5, 3, testblack),
|
||||
"the line ran past its first endpoint");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 5, 13, testblack),
|
||||
"the line ran past its second endpoint");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 6, 8, testblack),
|
||||
"the line is wider than one pixel");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_line(NULL, 0.0f, 0.0f, 1.0f, 1.0f, testred),
|
||||
"drawing a line through a NULL backend");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_rects(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
SDL_FRect box;
|
||||
|
||||
ATTEMPT {
|
||||
box.x = 8.0f;
|
||||
box.y = 8.0f;
|
||||
box.w = 16.0f;
|
||||
box.h = 16.0f;
|
||||
|
||||
// The outline touches the border and leaves the middle alone.
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_rect(renderer, &box, testred), "outlining a rectangle");
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the outline is missing its top left corner");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 23, 23, testred),
|
||||
"the outline is missing its bottom right corner");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 16, 8, testred), "the outline is missing its top edge");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testblack), "the outline filled its interior");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
// The filled form covers the interior as well.
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(renderer, &box, testred), "filling a rectangle");
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 16, 16, testred), "the fill left its interior empty");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred), "the fill missed its top left corner");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 24, 24, testblack),
|
||||
"the fill ran one pixel past its bottom right corner");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(NULL, &box, testred),
|
||||
"outlining through a NULL backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_rect(renderer, NULL, testred),
|
||||
"outlining a NULL rectangle");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(NULL, &box, testred),
|
||||
"filling through a NULL backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_draw_filled_rect(renderer, NULL, testred),
|
||||
"filling a NULL rectangle");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_circle(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
bool symmetric = true;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_circle(renderer, 32.0f, 32.0f, 10.0f, testred),
|
||||
"drawing a circle");
|
||||
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
// The four axis points are exact for any correct midpoint circle.
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testred), "the circle is missing its rightmost pixel");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 22, 32, testred), "the circle is missing its leftmost pixel");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 42, testred), "the circle is missing its bottom pixel");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 22, testred), "the circle is missing its top pixel");
|
||||
// It is an outline, not a disc.
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack), "the circle filled its center");
|
||||
// ...and nothing lands outside the radius.
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 43, 32, testred) == false,
|
||||
"the circle drew a pixel one past its radius");
|
||||
|
||||
// Every plotted pixel has a mirror in the other three quadrants. The
|
||||
// circle is drawn one octant at a time and reflected seven ways, so a
|
||||
// sign error in any single reflection breaks this and nothing else --
|
||||
// the four axis points above stay put either way.
|
||||
for ( y = 22; y <= 42; y++ ) {
|
||||
for ( x = 22; x <= 42; x++ ) {
|
||||
if ( !pixel_is(shot, x, y, testred) ) {
|
||||
continue;
|
||||
}
|
||||
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, y, testred));
|
||||
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, x, 64 - y, testred));
|
||||
TEST_ASSERT_FLAG(symmetric, pixel_is(shot, 64 - x, 64 - y, testred));
|
||||
}
|
||||
}
|
||||
TEST_ASSERT(errctx, symmetric == true,
|
||||
"the circle is not symmetric about its center; an octant is reflected wrong");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
// A zero radius is the degenerate case, not an error: one pixel.
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_circle(renderer, 5.0f, 5.0f, 0.0f, testred),
|
||||
"drawing a circle of radius zero");
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 5, 5, testred),
|
||||
"a circle of radius zero did not plot its center");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_draw_circle(renderer, 5.0f, 5.0f, -1.0f, testred),
|
||||
"drawing a circle of negative radius");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_circle(NULL, 5.0f, 5.0f, 4.0f, testred),
|
||||
"drawing a circle through a NULL backend");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_flood_fill(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
SDL_FRect box;
|
||||
|
||||
ATTEMPT {
|
||||
// A red box outline on black. Filling inside it must stay inside it,
|
||||
// which is the whole contract of PAINT.
|
||||
box.x = 10.0f;
|
||||
box.y = 10.0f;
|
||||
box.w = 20.0f;
|
||||
box.h = 20.0f;
|
||||
CATCH(errctx, clear_target());
|
||||
CATCH(errctx, akgl_draw_rect(renderer, &box, testred));
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 20, 20, testgreen),
|
||||
"flooding the inside of a box");
|
||||
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen), "the seed pixel was not filled");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 11, 11, testgreen),
|
||||
"the fill did not reach the top left of the interior");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 28, 28, testgreen),
|
||||
"the fill did not reach the bottom right of the interior");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred), "the fill overwrote the boundary");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 20, 10, testred), "the fill overwrote the top edge");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 20, 5, testblack), "the fill leaked outside the box");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack),
|
||||
"the fill leaked into the rest of the target");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
// Filling a region that is already the requested color changes nothing
|
||||
// and is not an error.
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 20, 20, testgreen),
|
||||
"flooding a region that is already that color");
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 20, 20, testgreen),
|
||||
"refilling a region disturbed it");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
|
||||
"refilling a region disturbed its boundary");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
// Flooding the outside reaches every pixel that is not the box.
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_flood_fill(renderer, 0, 0, testgreen),
|
||||
"flooding the area around a box");
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testgreen), "the seed pixel was not filled");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, TEST_TARGET_SIZE - 1, TEST_TARGET_SIZE - 1, testgreen),
|
||||
"the fill did not reach the far corner of the target");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 10, 10, testred),
|
||||
"the fill from outside overwrote the boundary");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_draw_flood_fill(renderer, -1, 0, testred),
|
||||
"flooding from a seed left of the target");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_draw_flood_fill(renderer, 0, TEST_TARGET_SIZE, testred),
|
||||
"flooding from a seed below the target");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_flood_fill(NULL, 0, 0, testred),
|
||||
"flooding through a NULL backend");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_copy_and_paste_region(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
SDL_Surface *saved = NULL;
|
||||
SDL_Surface *reused = NULL;
|
||||
SDL_Rect region;
|
||||
SDL_FRect box;
|
||||
|
||||
ATTEMPT {
|
||||
// Put something recognisable in the top left corner and save it.
|
||||
box.x = 0.0f;
|
||||
box.y = 0.0f;
|
||||
box.w = 8.0f;
|
||||
box.h = 8.0f;
|
||||
CATCH(errctx, clear_target());
|
||||
CATCH(errctx, akgl_draw_filled_rect(renderer, &box, testred));
|
||||
|
||||
region.x = 0;
|
||||
region.y = 0;
|
||||
region.w = 8;
|
||||
region.h = 8;
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(renderer, ®ion, &saved),
|
||||
"saving a region of the target");
|
||||
TEST_ASSERT(errctx, saved != NULL, "akgl_draw_copy_region did not allocate a surface");
|
||||
TEST_ASSERT(errctx, saved->w == 8 && saved->h == 8,
|
||||
"the saved surface is %dx%d, expected 8x8", saved->w, saved->h);
|
||||
|
||||
// Wipe the screen and put it back somewhere else.
|
||||
CATCH(errctx, clear_target());
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_paste_region(renderer, saved, 32.0f, 32.0f),
|
||||
"pasting a saved region");
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testred),
|
||||
"the pasted region did not land at its destination");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 39, 39, testred),
|
||||
"the pasted region is smaller than what was saved");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 40, 40, testblack),
|
||||
"the pasted region is larger than what was saved");
|
||||
TEST_ASSERT(errctx, pixel_is(shot, 0, 0, testblack),
|
||||
"pasting also redrew the region at its original position");
|
||||
SDL_DestroySurface(shot);
|
||||
shot = NULL;
|
||||
|
||||
// A surface the caller already owns is reused rather than replaced, so
|
||||
// saving the same region repeatedly does not churn allocations.
|
||||
reused = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32);
|
||||
FAIL_ZERO_BREAK(errctx, reused, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
region.x = 32;
|
||||
region.y = 32;
|
||||
TEST_EXPECT_OK(errctx, akgl_draw_copy_region(renderer, ®ion, &reused),
|
||||
"saving into a caller-owned surface");
|
||||
TEST_ASSERT(errctx, pixel_is(reused, 0, 0, testred),
|
||||
"the caller-owned surface did not receive the region");
|
||||
|
||||
// Wrong-sized destinations and regions off the edge of the target are
|
||||
// refused rather than silently clipped.
|
||||
region.w = 4;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_draw_copy_region(renderer, ®ion, &reused),
|
||||
"saving into a destination of the wrong size");
|
||||
region.x = TEST_TARGET_SIZE - 4;
|
||||
region.y = 0;
|
||||
region.w = 8;
|
||||
region.h = 8;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_draw_copy_region(renderer, ®ion, &reused),
|
||||
"saving a region that runs off the right edge");
|
||||
region.x = 0;
|
||||
region.w = 0;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_draw_copy_region(renderer, ®ion, &reused),
|
||||
"saving a region with no area");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_copy_region(NULL, ®ion, &reused),
|
||||
"saving through a NULL backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_copy_region(renderer, NULL, &reused),
|
||||
"saving a NULL region");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_copy_region(renderer, ®ion, NULL),
|
||||
"saving into a NULL destination");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_paste_region(NULL, saved, 0.0f, 0.0f),
|
||||
"pasting through a NULL backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_paste_region(renderer, NULL, 0.0f, 0.0f),
|
||||
"pasting a NULL surface");
|
||||
} CLEANUP {
|
||||
if ( shot != NULL ) {
|
||||
SDL_DestroySurface(shot);
|
||||
}
|
||||
if ( saved != NULL ) {
|
||||
SDL_DestroySurface(saved);
|
||||
}
|
||||
if ( reused != NULL ) {
|
||||
SDL_DestroySurface(reused);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_preserves_render_draw_color(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_FRect box;
|
||||
uint8_t r = 0;
|
||||
uint8_t g = 0;
|
||||
uint8_t b = 0;
|
||||
uint8_t a = 0;
|
||||
|
||||
ATTEMPT {
|
||||
box.x = 0.0f;
|
||||
box.y = 0.0f;
|
||||
box.w = 4.0f;
|
||||
box.h = 4.0f;
|
||||
|
||||
// Drawing must not leave the renderer's color set to whatever it drew
|
||||
// with, or the host's next SDL_RenderClear() paints the wrong color.
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_SetRenderDrawColor(renderer->sdl_renderer, 0x11, 0x22, 0x33, 0x44),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
CATCH(errctx, akgl_draw_point(renderer, 1.0f, 1.0f, testred));
|
||||
CATCH(errctx, akgl_draw_line(renderer, 0.0f, 0.0f, 3.0f, 3.0f, testred));
|
||||
CATCH(errctx, akgl_draw_rect(renderer, &box, testred));
|
||||
CATCH(errctx, akgl_draw_filled_rect(renderer, &box, testred));
|
||||
CATCH(errctx, akgl_draw_circle(renderer, 20.0f, 20.0f, 4.0f, testred));
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetRenderDrawColor(renderer->sdl_renderer, &r, &g, &b, &a),
|
||||
AKGL_ERR_SDL,
|
||||
"%s",
|
||||
SDL_GetError());
|
||||
TEST_ASSERT(errctx, (r == 0x11) && (g == 0x22) && (b == 0x33) && (a == 0x44),
|
||||
"drawing left the render draw color at %02x%02x%02x%02x, expected 11223344",
|
||||
r, g, b, a);
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_draw_backend_without_a_renderer(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_RenderBackend empty;
|
||||
SDL_Surface *saved = NULL;
|
||||
SDL_Surface *scratch = NULL;
|
||||
SDL_FRect box;
|
||||
SDL_Rect region;
|
||||
|
||||
ATTEMPT {
|
||||
// A backend that exists but was never given an SDL_Renderer. Every
|
||||
// entry point has to say so rather than dereference it -- this is the
|
||||
// state a host is in between allocating a backend and initializing it.
|
||||
memset(&empty, 0x00, sizeof(akgl_RenderBackend));
|
||||
box.x = 0.0f;
|
||||
box.y = 0.0f;
|
||||
box.w = 4.0f;
|
||||
box.h = 4.0f;
|
||||
region.x = 0;
|
||||
region.y = 0;
|
||||
region.w = 4;
|
||||
region.h = 4;
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_point(&empty, 0.0f, 0.0f, testred),
|
||||
"plotting through an uninitialized backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_line(&empty, 0.0f, 0.0f, 1.0f, 1.0f, testred),
|
||||
"drawing a line through an uninitialized backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_rect(&empty, &box, testred),
|
||||
"outlining through an uninitialized backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_filled_rect(&empty, &box, testred),
|
||||
"filling through an uninitialized backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_circle(&empty, 4.0f, 4.0f, 2.0f, testred),
|
||||
"drawing a circle through an uninitialized backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_flood_fill(&empty, 0, 0, testred),
|
||||
"flooding through an uninitialized backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_copy_region(&empty, ®ion, &saved),
|
||||
"saving through an uninitialized backend");
|
||||
TEST_ASSERT(errctx, saved == NULL,
|
||||
"a refused save still wrote something to the destination");
|
||||
|
||||
scratch = SDL_CreateSurface(4, 4, SDL_PIXELFORMAT_RGBA32);
|
||||
FAIL_ZERO_BREAK(errctx, scratch, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_draw_paste_region(&empty, scratch, 0.0f, 0.0f),
|
||||
"pasting through an uninitialized backend");
|
||||
} CLEANUP {
|
||||
if ( saved != NULL ) {
|
||||
SDL_DestroySurface(saved);
|
||||
}
|
||||
if ( scratch != NULL ) {
|
||||
SDL_DestroySurface(scratch);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
renderer = &_akgl_renderer;
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_VIDEO),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_CreateWindowAndRenderer(
|
||||
"net/aklabs/libakgl/test_draw",
|
||||
TEST_TARGET_SIZE,
|
||||
TEST_TARGET_SIZE,
|
||||
0,
|
||||
&window,
|
||||
&renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
|
||||
CATCH(errctx, test_draw_point());
|
||||
CATCH(errctx, test_draw_line());
|
||||
CATCH(errctx, test_draw_rects());
|
||||
CATCH(errctx, test_draw_circle());
|
||||
CATCH(errctx, test_draw_flood_fill());
|
||||
CATCH(errctx, test_draw_copy_and_paste_region());
|
||||
CATCH(errctx, test_draw_preserves_render_draw_color());
|
||||
CATCH(errctx, test_draw_backend_without_a_renderer());
|
||||
} CLEANUP {
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
102
tests/error.c
Normal file
102
tests/error.c
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @file error.c
|
||||
* @brief Unit tests for the libakgl status band: reservation, ownership and names.
|
||||
*
|
||||
* The libakerror registry is process-global, so these tests assert against
|
||||
* whatever akgl_error_init() left behind rather than building their own state.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/**
|
||||
* @brief akgl_error_init() must own the libakgl status band and name every code in it.
|
||||
*
|
||||
* A code whose name never registered degrades to "Unknown Error" in every stack
|
||||
* trace that carries it, and a band we never reserved is one another component
|
||||
* can name out from under us. Both stay silent until something has already gone
|
||||
* wrong, so assert them directly rather than waiting to read a useless trace.
|
||||
*/
|
||||
akerr_ErrorContext *test_error_init_owns_the_status_band(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
static const struct {
|
||||
int status;
|
||||
const char *name;
|
||||
} expected[] = {
|
||||
{ AKGL_ERR_SDL, "SDL Error" },
|
||||
{ AKGL_ERR_REGISTRY, "Registry Error" },
|
||||
{ AKGL_ERR_HEAP, "Heap Error" },
|
||||
{ AKGL_ERR_BEHAVIOR, "Behavior Error" },
|
||||
{ AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt" }
|
||||
};
|
||||
bool named = true;
|
||||
int i = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(e, akgl_error_init());
|
||||
|
||||
TEST_ASSERT(e, (int)(sizeof(expected) / sizeof(expected[0])) == AKGL_ERR_COUNT,
|
||||
"the libakgl status band holds %d codes but %d are named here",
|
||||
AKGL_ERR_COUNT, (int)(sizeof(expected) / sizeof(expected[0])));
|
||||
|
||||
for ( i = 0; i < (int)(sizeof(expected) / sizeof(expected[0])); i++ ) {
|
||||
TEST_ASSERT_FLAG(named,
|
||||
strcmp(akerr_name_for_status(expected[i].status, NULL),
|
||||
expected[i].name) == 0);
|
||||
}
|
||||
TEST_ASSERT(e, named,
|
||||
"akgl_error_init did not register the expected name for every AKGL_ERR_* code");
|
||||
|
||||
// The reservation is what makes those names ours. Without it the
|
||||
// registrations above would still succeed for anyone who asked.
|
||||
TEST_EXPECT_STATUS(e, AKERR_STATUS_NAME_FOREIGN,
|
||||
akerr_register_status_name("not-libakgl", AKGL_ERR_HEAP, "Squatter"),
|
||||
"a foreign owner was allowed to rename a libakgl status");
|
||||
TEST_EXPECT_STATUS(e, AKERR_STATUS_RANGE_OVERLAP,
|
||||
akerr_reserve_status_range(AKGL_ERR_BASE, AKGL_ERR_COUNT, "not-libakgl"),
|
||||
"a foreign owner was allowed to reserve the libakgl status band");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calling akgl_error_init() twice must be a no-op, not a self-collision.
|
||||
*
|
||||
* Nothing in libakgl orders initialization for an embedding program, so a second
|
||||
* call has to be harmless. libakerror only treats an *identical* reservation as
|
||||
* a repeat -- a subset or superset raises -- which makes this a real constraint
|
||||
* on AKGL_ERR_BASE and AKGL_ERR_COUNT, not a triviality.
|
||||
*/
|
||||
akerr_ErrorContext *test_error_init_is_idempotent(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
TEST_EXPECT_OK(e, akgl_error_init(), "the second akgl_error_init failed");
|
||||
TEST_EXPECT_OK(e, akgl_error_init(), "the third akgl_error_init failed");
|
||||
TEST_ASSERT(e, strcmp(akerr_name_for_status(AKGL_ERR_SDL, NULL), "SDL Error") == 0,
|
||||
"re-running akgl_error_init lost the name for AKGL_ERR_SDL");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, test_error_init_owns_the_status_band());
|
||||
CATCH(errctx, test_error_init_is_idempotent());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
@@ -402,6 +402,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
|
||||
|
||||
37
tests/headers.c
Normal file
37
tests/headers.c
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @file headers.c
|
||||
* @brief Checks that a public header can be the first thing a caller includes.
|
||||
*
|
||||
* The assertion here is the compile, not anything main() does. `akgl/controller.h`
|
||||
* declares its binding handlers as taking an `akgl_Actor *` and once did not
|
||||
* declare that type, so every translation unit that reached for it before
|
||||
* `akgl/actor.h` failed with "unknown type name 'akgl_Actor'". `src/controller.c`
|
||||
* never noticed, because it includes `akgl/game.h` first.
|
||||
*
|
||||
* One header per file, deliberately. A second `#include` after the first proves
|
||||
* nothing about the second: by then the first has already dragged its
|
||||
* dependencies in. Covering another header means another file like this one.
|
||||
*/
|
||||
|
||||
#include <akgl/controller.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akgl_Control control;
|
||||
|
||||
// Enough to require the akgl_Actor declaration to be complete rather than
|
||||
// merely present: the handler signature names it, and a control map holds
|
||||
// one by value.
|
||||
memset(&control, 0x00, sizeof(akgl_Control));
|
||||
control.key = SDLK_LEFT;
|
||||
control.handler_on = &akgl_Actor_cmhf_left_on;
|
||||
if ( control.handler_on == NULL ) {
|
||||
return 1;
|
||||
}
|
||||
if ( sizeof(GAME_ControlMaps) != (sizeof(akgl_ControlMap) * AKGL_MAX_CONTROL_MAPS) ) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -361,6 +361,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
|
||||
|
||||
@@ -347,6 +347,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, load_fixture());
|
||||
|
||||
928
tests/perf.c
Normal file
928
tests/perf.c
Normal file
@@ -0,0 +1,928 @@
|
||||
/**
|
||||
* @file perf.c
|
||||
* @brief Stress and timing benchmarks for every subsystem that does not need a renderer.
|
||||
*
|
||||
* These are not correctness tests. Each one drives a hot path -- the pool scans,
|
||||
* the registry, the physics sweep, the per-actor update, the geometry helpers,
|
||||
* the JSON accessors -- hard enough that its per-operation cost is measurable,
|
||||
* and reports what that cost is. The companion suite in `tests/perf_render.c`
|
||||
* covers the paths that need a renderer.
|
||||
*
|
||||
* The pools are the reason this matters. Every acquire is a linear scan of a
|
||||
* fixed array looking for a zero reference count, so cost grows with how full
|
||||
* the pool is, not with how much is being asked for. Several benchmarks below
|
||||
* are therefore run twice: once against an empty pool, which is the number a
|
||||
* casual reading of the code predicts, and once against a nearly full one,
|
||||
* which is the number a game in its fifth minute actually gets.
|
||||
*
|
||||
* Everything here runs headless under the dummy drivers, and every benchmark
|
||||
* rebuilds the state it needs rather than inheriting whatever the previous one
|
||||
* left behind.
|
||||
*
|
||||
* @note SDL's log output is redirected to a sink that discards it. The library
|
||||
* logs on paths this suite hammers -- akgl_actor_initialize logs every
|
||||
* spawn -- and timing a write to a terminal or a CTest capture file
|
||||
* measures the machine's I/O, not libakgl. What the *formatting* costs is
|
||||
* measured deliberately, by the pair of actor-spawn benchmarks.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <jansson.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/util.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
#include <akgl/tilemap.h>
|
||||
|
||||
#include "benchutil.h"
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief How many actors the frame-shaped benchmarks put on the heap. */
|
||||
#define BENCH_ACTOR_COUNT AKGL_MAX_HEAP_ACTOR
|
||||
/** @brief The JSON document the accessor benchmarks read. */
|
||||
#define BENCH_JSON_FIXTURE "assets/snippets/test_json_helpers.json"
|
||||
/** @brief A path that exists relative to the CTest working directory, for akgl_path_relative. */
|
||||
#define BENCH_PATH_FIXTURE "assets/testcharacter.json"
|
||||
|
||||
/**
|
||||
* @brief Print the library's fixed memory footprint.
|
||||
*
|
||||
* Not a benchmark, but it belongs in the same report: libakgl does not call
|
||||
* `malloc`, so every one of these arrays exists from process start whether the
|
||||
* game uses one slot or all of them, and several of the timings above are
|
||||
* explained entirely by their size. The string pool is a megabyte of `char`
|
||||
* arrays, and an akgl_Tilemap is dominated by 16 layers of 512x512 `int` cells
|
||||
* and 16 tilesets of 65536 offset pairs -- which is why zeroing one costs what
|
||||
* it costs, and why the header warns against putting one on the stack.
|
||||
*/
|
||||
static void bench_report_footprint(void)
|
||||
{
|
||||
size_t total = 0;
|
||||
|
||||
total = sizeof(HEAP_ACTOR) + sizeof(HEAP_SPRITE) + sizeof(HEAP_SPRITESHEET) +
|
||||
sizeof(HEAP_CHARACTER) + sizeof(HEAP_STRING);
|
||||
|
||||
printf("\n");
|
||||
printf("static footprint, fixed at compile time:\n");
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_ACTOR", AKGL_MAX_HEAP_ACTOR, sizeof(akgl_Actor), sizeof(HEAP_ACTOR));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_SPRITE", AKGL_MAX_HEAP_SPRITE, sizeof(akgl_Sprite), sizeof(HEAP_SPRITE));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_SPRITESHEET", AKGL_MAX_HEAP_SPRITESHEET, sizeof(akgl_SpriteSheet), sizeof(HEAP_SPRITESHEET));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_CHARACTER", AKGL_MAX_HEAP_CHARACTER, sizeof(akgl_Character), sizeof(HEAP_CHARACTER));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_STRING", AKGL_MAX_HEAP_STRING, sizeof(akgl_String), sizeof(HEAP_STRING));
|
||||
printf(" %-24s %5s %7s %10zu bytes\n", "pools, total", "", "", total);
|
||||
printf(" %-24s %5d x %7s = %10zu bytes\n",
|
||||
"akgl_Tilemap", 1, "", sizeof(akgl_Tilemap));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
" of which layers", AKGL_TILEMAP_MAX_LAYERS, sizeof(akgl_TilemapLayer),
|
||||
(sizeof(akgl_TilemapLayer) * AKGL_TILEMAP_MAX_LAYERS));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
" of which tilesets", AKGL_TILEMAP_MAX_TILESETS, sizeof(akgl_Tileset),
|
||||
(sizeof(akgl_Tileset) * AKGL_TILEMAP_MAX_TILESETS));
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Swallow SDL's log output.
|
||||
*
|
||||
* Installed for the whole run. See the note in the file comment: the library
|
||||
* logs on paths this suite calls hundreds of thousands of times, and the cost of
|
||||
* writing that out is not a property of libakgl.
|
||||
*/
|
||||
static void bench_discard_log(void *userdata, int category, SDL_LogPriority priority, const char *message)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Claim a spritesheet slot without loading an image into it.
|
||||
*
|
||||
* akgl_spritesheet_initialize uploads a texture and so needs a renderer, which
|
||||
* this suite deliberately does not have. Nothing here draws, and the only thing
|
||||
* that reads the sheet is akgl_sprite_initialize storing the pointer, so an
|
||||
* empty claimed slot is enough.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_make_sheet(akgl_SpriteSheet **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||
PASS(errctx, akgl_heap_next_spritesheet(dest));
|
||||
memset(*dest, 0x00, sizeof(akgl_SpriteSheet));
|
||||
(*dest)->refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a sprite on a freshly claimed sheet and publish it in the sprite registry.
|
||||
*
|
||||
* @param dest Receives the sprite. Required.
|
||||
* @param name Registry key. Copied at a fixed #AKGL_SPRITE_MAX_NAME_LENGTH
|
||||
* bytes by akgl_sprite_initialize, so it is staged through a buffer
|
||||
* of exactly that size first.
|
||||
* @param speed Nanoseconds one frame is held. 0 makes every akgl_actor_update
|
||||
* advance the animation, which is the expensive path.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_make_sprite(akgl_Sprite **dest, char *name, uint32_t speed)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_SpriteSheet *sheet = NULL;
|
||||
char namebuf[AKGL_SPRITE_MAX_NAME_LENGTH];
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
|
||||
|
||||
memset(&namebuf, 0x00, sizeof(namebuf));
|
||||
strncpy((char *)&namebuf, name, AKGL_SPRITE_MAX_NAME_LENGTH - 1);
|
||||
|
||||
PASS(errctx, bench_make_sheet(&sheet));
|
||||
PASS(errctx, akgl_heap_next_sprite(dest));
|
||||
PASS(errctx, akgl_sprite_initialize(*dest, (char *)&namebuf, sheet));
|
||||
(*dest)->frames = 3;
|
||||
(*dest)->frameids[0] = 0;
|
||||
(*dest)->frameids[1] = 1;
|
||||
(*dest)->frameids[2] = 2;
|
||||
(*dest)->width = 48;
|
||||
(*dest)->height = 48;
|
||||
(*dest)->speed = speed;
|
||||
(*dest)->loop = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a character with one sprite mapped to the two states these benchmarks use.
|
||||
*
|
||||
* State 0 is what akgl_actor_initialize leaves an actor in and what
|
||||
* akgl_actor_automatic_face leaves it in while it is standing still;
|
||||
* `MOVING_RIGHT | FACE_RIGHT` is what bench_fill_actor_pool puts it in and what
|
||||
* the face logic settles on from there. Mapping both keeps the update path off
|
||||
* the missing-sprite branch, which is measured deliberately elsewhere rather
|
||||
* than by accident here.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_make_character(akgl_Character **dest, char *name, uint32_t spritespeed)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Sprite *sprite = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
|
||||
|
||||
PASS(errctx, akgl_heap_next_character(dest));
|
||||
PASS(errctx, akgl_character_initialize(*dest, name));
|
||||
(*dest)->sx = 120.0;
|
||||
(*dest)->sy = 120.0;
|
||||
(*dest)->sz = 0.0;
|
||||
(*dest)->ax = 40.0;
|
||||
(*dest)->ay = 40.0;
|
||||
(*dest)->speedtime = 16;
|
||||
|
||||
PASS(errctx, bench_make_sprite(&sprite, name, spritespeed));
|
||||
PASS(errctx, (*dest)->sprite_add(*dest, sprite, 0));
|
||||
PASS(errctx,
|
||||
(*dest)->sprite_add(
|
||||
*dest,
|
||||
sprite,
|
||||
(AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT))
|
||||
);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Populate the actor pool with @p count live actors bound to @p basechar.
|
||||
*
|
||||
* The heap and the actor registry are rebuilt first, so this is the state every
|
||||
* frame-shaped benchmark starts from rather than something it inherits.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_fill_actor_pool(akgl_Character *basechar, int count)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Actor *actor = NULL;
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||
int i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "basechar");
|
||||
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
PASS(errctx, akgl_registry_init_actor());
|
||||
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
memset(&name, 0x00, sizeof(name));
|
||||
snprintf((char *)&name, AKGL_ACTOR_MAX_NAME_LENGTH, "benchactor%d", i);
|
||||
PASS(errctx, akgl_heap_next_actor(&actor));
|
||||
PASS(errctx, akgl_actor_initialize(actor, (char *)&name));
|
||||
PASS(errctx, akgl_actor_set_character(actor, basechar->name));
|
||||
actor->visible = true;
|
||||
actor->layer = 0;
|
||||
actor->x = (float32_t)(i * 13);
|
||||
actor->y = (float32_t)(i * 7);
|
||||
actor->sx = 120.0;
|
||||
actor->sy = 120.0;
|
||||
actor->ax = 40.0;
|
||||
actor->ay = 40.0;
|
||||
AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time a claim from an empty actor pool against one from a nearly full pool.
|
||||
*
|
||||
* The pool scan stops at the first slot with a zero reference count, so an empty
|
||||
* pool answers from index 0 and a pool with one slot left answers from index
|
||||
* #AKGL_MAX_HEAP_ACTOR - 1. The ratio between these two numbers *is* the cost of
|
||||
* the linear-scan allocator.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_heap_actor_claim(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
int count = bench_iterations(500000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
bench_start("heap_next_actor, empty pool", "call", 100.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
for ( i = 0; i < (AKGL_MAX_HEAP_ACTOR - 1); i++ ) {
|
||||
HEAP_ACTOR[i].refcount = 1;
|
||||
}
|
||||
bench_start("heap_next_actor, one slot left", "call", 400.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the string pool, which is the one that hurts.
|
||||
*
|
||||
* Every entry is #AKGL_MAX_STRING_LENGTH bytes, so 256 of them is a megabyte and
|
||||
* the scan for a free slot touches one reference count every 4 KiB -- a cache
|
||||
* miss per candidate. Release then wipes the whole 4 KiB whether the string held
|
||||
* a path or a single character. Three numbers come out of this: the claim on an
|
||||
* empty pool, the claim with one slot left, and the release on its own.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_heap_string(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *str = NULL;
|
||||
int count = bench_iterations(100000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
PASS(errctx, akgl_heap_init());
|
||||
|
||||
// Claim and release together: the scratch-buffer idiom the library uses
|
||||
// everywhere, and the only string benchmark whose cost a caller can
|
||||
// actually observe from outside.
|
||||
bench_start("heap string claim + release cycle", "cycle", 500.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_string(&str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_string(str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
// The claim on its own. The reference the claim takes is dropped by hand
|
||||
// rather than through release, so the 4 KiB wipe stays out of this one.
|
||||
bench_start("heap_next_string, empty pool", "call", 100.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_string(&str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
str->refcount = 0;
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
// The same claim with 255 of the 256 slots taken: a megabyte walked, one
|
||||
// reference count read per 4 KiB page.
|
||||
for ( i = 0; i < (AKGL_MAX_HEAP_STRING - 1); i++ ) {
|
||||
HEAP_STRING[i].refcount = 1;
|
||||
}
|
||||
bench_start("heap_next_string, one slot left", "call", 2500.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_string(&str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
str->refcount = 0;
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
PASS(errctx, akgl_heap_init());
|
||||
|
||||
// Release on its own: one 4 KiB memset per call, whatever the string held.
|
||||
PASS(errctx, akgl_heap_next_string(&str));
|
||||
bench_start("heap_release_string, 4 KiB wipe", "call", 500.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
str->refcount = 1;
|
||||
inner = akgl_heap_release_string(str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time a full reset of every pool.
|
||||
*
|
||||
* akgl_heap_init zeroes all five arrays -- better than a megabyte, dominated by
|
||||
* the string pool. A game pays this once at startup and again on every call to
|
||||
* akgl_game_init; a level transition that calls it per level pays it per level.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_heap_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(2000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("heap_init, all five pools", "call", 450000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_heap_init());
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time an actor spawn, with and without the log line the library writes.
|
||||
*
|
||||
* A spawn is a pool claim, a `memset` of the whole actor, a registry insert, and
|
||||
* an `SDL_Log`. The second run raises the log priority so SDL returns before it
|
||||
* formats anything; the gap between the two numbers is what the log line costs
|
||||
* every caller, including the ones that never read it.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_actor_spawn(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||
int count = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
memset(&name, 0x00, sizeof(name));
|
||||
strncpy((char *)&name, "benchspawn", AKGL_ACTOR_MAX_NAME_LENGTH - 1);
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
PASS(errctx, akgl_registry_init_actor());
|
||||
|
||||
bench_start("actor spawn + release, library logging on", "actor", 2100.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_actor(&actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_actor_initialize(actor, (char *)&name);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_actor(actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
SDL_SetLogPriorities(SDL_LOG_PRIORITY_CRITICAL);
|
||||
bench_start("actor spawn + release, logging suppressed", "actor", 1700.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_actor(&actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_actor_initialize(actor, (char *)&name);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_actor(actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
SDL_SetLogPriorities(SDL_LOG_PRIORITY_INFO);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time a registry lookup with the actor registry full.
|
||||
*
|
||||
* akgl_actor_set_character is the library's own name-to-pointer lookup: an SDL
|
||||
* property fetch against a registry holding #AKGL_MAX_HEAP_ACTOR entries, plus
|
||||
* the four field copies that follow it. Every actor built from a tilemap object
|
||||
* layer pays it once.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_registry_lookup(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchlookupchar", 0));
|
||||
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
|
||||
actor = &HEAP_ACTOR[0];
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("actor_set_character, registry lookup", "call", 400.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_actor_set_character(actor, basechar->name));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the property store a game reads its configuration out of.
|
||||
*
|
||||
* akgl_get_property copies a fixed #AKGL_MAX_STRING_LENGTH bytes out of the
|
||||
* property store regardless of how long the value is, so reading `"0.0"` moves
|
||||
* 4 KiB. akgl_physics_init_arcade makes six of these calls.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_properties(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *value = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_registry_init_properties());
|
||||
PASS(errctx, akgl_set_property("bench.property", "a short value"));
|
||||
PASS(errctx, akgl_heap_next_string(&value));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("set_property", "call", 1000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_set_property("bench.property", "a short value"));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("get_property, 4 KiB copy", "call", 900.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_get_property("bench.property", &value, "unset"));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
PASS(errctx, akgl_heap_release_string(value));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the state-to-sprite lookup that runs twice per actor per frame.
|
||||
*
|
||||
* akgl_character_sprite_get renders the state bitmask to decimal with `SDL_itoa`
|
||||
* and looks the result up as a property string. akgl_actor_update calls it, and
|
||||
* so does akgl_actor_render, so a 64-actor frame makes 128 of these calls before
|
||||
* anything is drawn.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_character_sprite_get(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Sprite *sprite = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchspritechar", 0));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("character_sprite_get, state to sprite", "call", 400.0);
|
||||
BENCH_LOOP(inner, i, count, basechar->sprite_get(basechar, 0, &sprite));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time one actor's logic update, with the animation advancing every call.
|
||||
*
|
||||
* The sprite's frame time is 0, so every update takes the branch that changes
|
||||
* frame -- the expensive path, and the one a fast animation actually takes. An
|
||||
* update is a face recalculation, a sprite lookup, a clock read, and the frame
|
||||
* arithmetic.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_actor_update(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchupdatechar", 0));
|
||||
PASS(errctx, bench_fill_actor_pool(basechar, 1));
|
||||
actor = &HEAP_ACTOR[0];
|
||||
AKGL_BITMASK_CLEAR(actor->state);
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("actor_update, animation advancing", "actor", 700.0);
|
||||
BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
// An actor in a state its character has no sprite for is not an error
|
||||
// the library refuses -- akgl_actor_update handles AKERR_KEY and carries
|
||||
// on, and akgl_actor_render logs it and draws nothing. It is a normal
|
||||
// condition on a partly authored character, and it is worth knowing what
|
||||
// one costs, because raising, formatting, and handling a context is far
|
||||
// more work than the update it replaces.
|
||||
AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||||
bench_start("actor_update, no sprite for state", "actor", 6200.0);
|
||||
BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
AKGL_BITMASK_CLEAR(actor->state);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the physics sweep over a full actor pool, and over an empty one.
|
||||
*
|
||||
* akgl_physics_simulate walks all #AKGL_MAX_HEAP_ACTOR slots whether or not
|
||||
* anything is in them, so the empty-pool number is the floor every frame pays.
|
||||
* The full-pool number is one frame of movement for 64 actors: movement logic,
|
||||
* gravity, drag, clamp, and integrate, per actor.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_physics_simulate(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_PhysicsBackend backend;
|
||||
int count = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, akgl_registry_init_properties());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchphysicschar", 0));
|
||||
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
PASS(errctx, akgl_physics_init_arcade(&backend));
|
||||
backend.gravity_y = 9.8;
|
||||
backend.drag_x = 0.1;
|
||||
backend.drag_y = 0.1;
|
||||
|
||||
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("physics_simulate, 64 live actors", "frame", 12500.0);
|
||||
BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
PASS(errctx, akgl_registry_init_actor());
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("physics_simulate, empty pool", "frame", 650.0);
|
||||
BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the logic half of a frame: update every actor, then simulate.
|
||||
*
|
||||
* This is what a host's main loop does before it draws anything, and it is the
|
||||
* number to subtract from a 16.6 ms frame to find out what is left for drawing.
|
||||
* Note that it is *not* what akgl_game_update does: that one runs the update
|
||||
* loop once per tilemap layer, which is measured in the render suite.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_logic_frame(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_PhysicsBackend backend;
|
||||
int count = bench_iterations(5000);
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, akgl_registry_init_properties());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchframechar", 0));
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
PASS(errctx, akgl_physics_init_arcade(&backend));
|
||||
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("logic frame, 64 actors updated + simulated", "frame", 60000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
for ( j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
|
||||
actor = &HEAP_ACTOR[j];
|
||||
if ( actor->refcount == 0 ) {
|
||||
continue;
|
||||
}
|
||||
inner = actor->updatefunc(actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = backend.simulate(&backend, NULL);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the geometry helpers, including the all-pairs sweep a caller has to write.
|
||||
*
|
||||
* akgl_collide_rectangles tests eight corners and returns at the first hit, so
|
||||
* an overlap is cheap and a miss is the full eight. The third benchmark is the
|
||||
* broad phase the library does not provide: 64 actors is 2016 pairs, and a
|
||||
* caller doing collision at all does that every frame.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_geometry(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
RectanglePoints points;
|
||||
SDL_FRect rects[BENCH_ACTOR_COUNT];
|
||||
SDL_FRect overlapping = { .x = 8.0, .y = 8.0, .w = 32.0, .h = 32.0 };
|
||||
SDL_FRect disjoint = { .x = 900.0, .y = 900.0, .w = 32.0, .h = 32.0 };
|
||||
SDL_FRect subject = { .x = 0.0, .y = 0.0, .w = 32.0, .h = 32.0 };
|
||||
bool collide = false;
|
||||
int count = bench_iterations(500000);
|
||||
int sweeps = bench_iterations(2000);
|
||||
int pairs = 0;
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int k = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||||
rects[i].x = (float32_t)(i * 24);
|
||||
rects[i].y = (float32_t)(i * 18);
|
||||
rects[i].w = 32.0;
|
||||
rects[i].h = 32.0;
|
||||
}
|
||||
pairs = (BENCH_ACTOR_COUNT * (BENCH_ACTOR_COUNT - 1)) / 2;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("rectangle_points", "call", 100.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_rectangle_points(&points, &subject));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("collide_rectangles, overlapping", "call", 300.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &overlapping, &collide));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("collide_rectangles, disjoint", "call", 600.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &disjoint, &collide));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("all-pairs collision sweep, 64 actors", "sweep", 1200000.0);
|
||||
for ( k = 0; k < sweeps; k++ ) {
|
||||
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||||
for ( j = i + 1; j < BENCH_ACTOR_COUNT; j++ ) {
|
||||
inner = akgl_collide_rectangles(&rects[i], &rects[j], &collide);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(sweeps);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
printf("all-pairs collision sweep covers %d pairs per sweep\n", pairs);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the pooled-string operations.
|
||||
*
|
||||
* Both of these move #AKGL_MAX_STRING_LENGTH bytes by default -- a copy with a
|
||||
* count of 0 means "all of it" -- so the cost is the same for a filename and for
|
||||
* a single character.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_strings(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *src = NULL;
|
||||
akgl_String *dst = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_heap_next_string(&src));
|
||||
PASS(errctx, akgl_heap_next_string(&dst));
|
||||
PASS(errctx, akgl_string_initialize(src, "assets/testcharacter.json"));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("string_initialize", "call", 350.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_string_initialize(dst, "assets/testcharacter.json"));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("string_copy, full length", "call", 350.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_string_copy(src, dst, 0));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
PASS(errctx, akgl_heap_release_string(src));
|
||||
PASS(errctx, akgl_heap_release_string(dst));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time parsing a document and reading values back out of it.
|
||||
*
|
||||
* The parse is what an asset load pays per file. The accessors are what the
|
||||
* tilemap and character loaders pay per field, and every one of them that
|
||||
* returns a string claims and fills a 4 KiB pooled string.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_json(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *path = NULL;
|
||||
akgl_String *value = NULL;
|
||||
json_t *doc = NULL;
|
||||
json_error_t jsonerr;
|
||||
int number = 0;
|
||||
int loads = bench_iterations(2000);
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_heap_next_string(&path));
|
||||
PASS(errctx, akgl_heap_next_string(&value));
|
||||
snprintf((char *)&path->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), BENCH_JSON_FIXTURE);
|
||||
|
||||
doc = json_load_file((char *)&path->data, 0, &jsonerr);
|
||||
FAIL_ZERO_RETURN(errctx, doc, AKERR_IO, "Unable to load %s: %s", (char *)&path->data, jsonerr.text);
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("json_load_file, small document", "load", 120000.0);
|
||||
for ( i = 0; i < loads; i++ ) {
|
||||
json_t *scratch = json_load_file((char *)&path->data, 0, &jsonerr);
|
||||
if ( scratch == NULL ) {
|
||||
break;
|
||||
}
|
||||
json_decref(scratch);
|
||||
}
|
||||
bench_stop(loads);
|
||||
|
||||
bench_start("get_json_string_value", "call", 450.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_get_json_string_value(doc, "name", &value));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("get_json_integer_value", "call", 150.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_get_json_integer_value(doc, "count", &number));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
json_decref(doc);
|
||||
PASS(errctx, akgl_heap_release_string(path));
|
||||
PASS(errctx, akgl_heap_release_string(value));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time path resolution, which every asset reference goes through.
|
||||
*
|
||||
* akgl_path_relative calls `realpath(3)`, so this is a syscall and a walk of the
|
||||
* filesystem, per reference, at load time. A tilemap naming twenty tilesets and
|
||||
* sprites pays it twenty times.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_path_relative(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *dst = NULL;
|
||||
int count = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_heap_next_string(&dst));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("path_relative, realpath on an existing file", "call", 36000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_path_relative(".", BENCH_PATH_FIXTURE, dst));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
PASS(errctx, akgl_heap_release_string(dst));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
SDL_SetLogOutputFunction(bench_discard_log, NULL);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, akgl_registry_init_properties());
|
||||
|
||||
CATCH(errctx, bench_heap_actor_claim());
|
||||
CATCH(errctx, bench_heap_string());
|
||||
CATCH(errctx, bench_heap_init());
|
||||
CATCH(errctx, bench_actor_spawn());
|
||||
CATCH(errctx, bench_registry_lookup());
|
||||
CATCH(errctx, bench_properties());
|
||||
CATCH(errctx, bench_character_sprite_get());
|
||||
CATCH(errctx, bench_actor_update());
|
||||
CATCH(errctx, bench_physics_simulate());
|
||||
CATCH(errctx, bench_logic_frame());
|
||||
CATCH(errctx, bench_geometry());
|
||||
CATCH(errctx, bench_strings());
|
||||
CATCH(errctx, bench_json());
|
||||
CATCH(errctx, bench_path_relative());
|
||||
|
||||
bench_report_footprint();
|
||||
BENCH_REPORT_BREAK(errctx);
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
837
tests/perf_render.c
Normal file
837
tests/perf_render.c
Normal file
@@ -0,0 +1,837 @@
|
||||
/**
|
||||
* @file perf_render.c
|
||||
* @brief Stress and timing benchmarks for the paths that need a renderer.
|
||||
*
|
||||
* The companion to `tests/perf.c`, which covers everything that does not draw.
|
||||
* Everything here runs against a software renderer under the dummy video driver
|
||||
* at #BENCH_SCREEN_W x #BENCH_SCREEN_H -- no display, no window shown, no GPU.
|
||||
*
|
||||
* A software renderer is the right instrument for this even though no shipped
|
||||
* game uses one. What these benchmarks are measuring is the work libakgl does
|
||||
* *per drawn thing* -- how many blits a screenful of tiles turns into, how many
|
||||
* lookups an actor costs before its texture moves, what a line of HUD text
|
||||
* allocates and throws away every frame. A software renderer keeps that work in
|
||||
* the same process where it can be timed, and its per-blit cost is at least
|
||||
* honest about how much pixel traffic was asked for. Read these numbers as
|
||||
* *counts of work*, not as frame rates a real game would see.
|
||||
*
|
||||
* The tilemap benchmarks build their map by hand rather than loading the 2x2
|
||||
* fixture. A 2x2 map measures nothing: the point of the exercise is a full
|
||||
* screen of tiles -- #BENCH_MAP_W x #BENCH_MAP_H cells at #BENCH_TILE_SIZE
|
||||
* pixels, of which the camera covers 40 x 30 -- and the second variant repeats
|
||||
* it with eight tilesets, because akgl_tilemap_draw scans every tileset for
|
||||
* every tile and never stops at the one that matched.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/text.h>
|
||||
#include <akgl/tilemap.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
#include "benchutil.h"
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Width of the offscreen render target every benchmark draws into. */
|
||||
#define BENCH_SCREEN_W 640
|
||||
/** @brief Height of that target. */
|
||||
#define BENCH_SCREEN_H 480
|
||||
/** @brief Edge of one cell in the synthetic map, in pixels. */
|
||||
#define BENCH_TILE_SIZE 16
|
||||
/** @brief Width of the synthetic map, in tiles. Bigger than the camera on purpose. */
|
||||
#define BENCH_MAP_W 128
|
||||
/** @brief Height of the synthetic map, in tiles. */
|
||||
#define BENCH_MAP_H 128
|
||||
/** @brief Tilesets in the many-tileset variant of the draw benchmark. */
|
||||
#define BENCH_MAP_TILESETS 8
|
||||
/** @brief Actors the scene-wide benchmarks put on the heap. */
|
||||
#define BENCH_ACTOR_COUNT AKGL_MAX_HEAP_ACTOR
|
||||
/** @brief The tileset image the synthetic map cuts its tiles from: 768x576, 48x36 tiles. */
|
||||
#define BENCH_TILESET_IMAGE "assets/World_A1.png"
|
||||
/** @brief The font the text benchmarks rasterize with. */
|
||||
#define BENCH_FONT_PATH "assets/akgl_test_mono.ttf"
|
||||
/** @brief Point size for that font. */
|
||||
#define BENCH_FONT_SIZE 16
|
||||
/** @brief A representative line of HUD text: a score readout. */
|
||||
#define BENCH_TEXT "SCORE 000123456"
|
||||
|
||||
/** @brief The font opened once in main and used by the text benchmarks. */
|
||||
static TTF_Font *benchfont = NULL;
|
||||
/** @brief The tileset texture the synthetic map borrows. Destroyed in main. */
|
||||
static SDL_Texture *benchtiles = NULL;
|
||||
|
||||
/**
|
||||
* @brief Stop the clock, but only after the renderer has done what it was asked.
|
||||
*
|
||||
* SDL batches. A `draw_*` call queues a command and returns, and the queue is
|
||||
* not executed until something forces it: a present, a readback, or the
|
||||
* destruction of a texture it refers to. Without a flush inside the timed
|
||||
* region, every drawing benchmark here measures the cost of *queueing* work and
|
||||
* none of the cost of doing it -- and the bill arrives at teardown, where
|
||||
* SDL_DestroyTexture on the tileset spent nearly two minutes executing blits
|
||||
* that six earlier benchmarks had queued and been given credit for not doing.
|
||||
*
|
||||
* So the flush goes inside the measurement: what these benchmarks report is N
|
||||
* calls plus the drawing those N calls asked for, which is the number a caller
|
||||
* actually pays over a frame.
|
||||
*/
|
||||
#define BENCH_FLUSH_STOP(e, ops) \
|
||||
{ \
|
||||
bool __bench_flushed = SDL_FlushRenderer(renderer->sdl_renderer); \
|
||||
bench_stop(ops); \
|
||||
FAIL_ZERO_RETURN(e, __bench_flushed, AKGL_ERR_SDL, "SDL_FlushRenderer: %s", SDL_GetError()); \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Swallow SDL's log output. See the same function in tests/perf.c.
|
||||
*/
|
||||
static void bench_discard_log(void *userdata, int category, SDL_LogPriority priority, const char *message)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a map by hand: one tile layer, @p tilesets tilesets, every cell filled.
|
||||
*
|
||||
* The cells all draw from the *last* tileset, which is the worst case for
|
||||
* akgl_tilemap_draw's inner scan and the reason the many-tileset variant exists.
|
||||
* Global ids are laid out so that tileset k owns [1 + k*tilecount, ...], which
|
||||
* keeps each cell matching exactly one tileset -- the draw loop has no `break`,
|
||||
* so overlapping ranges would blit the same cell more than once and measure
|
||||
* something that cannot happen with a map Tiled produced.
|
||||
*
|
||||
* @param map The tilemap to fill in. Required. Zeroed first.
|
||||
* @param tilesets How many tilesets to declare, all sharing one texture.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_build_map(akgl_Tilemap *map, int tilesets)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int columns = 0;
|
||||
int rows = 0;
|
||||
int tilecount = 0;
|
||||
int lastgid = 0;
|
||||
int i = 0;
|
||||
int cell = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "map");
|
||||
FAIL_ZERO_RETURN(errctx, benchtiles, AKERR_NULLPOINTER, "benchtiles");
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((tilesets < 1) || (tilesets > AKGL_TILEMAP_MAX_TILESETS)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"%d tilesets is outside 1..%d",
|
||||
tilesets,
|
||||
AKGL_TILEMAP_MAX_TILESETS);
|
||||
|
||||
memset(map, 0x00, sizeof(akgl_Tilemap));
|
||||
columns = benchtiles->w / BENCH_TILE_SIZE;
|
||||
rows = benchtiles->h / BENCH_TILE_SIZE;
|
||||
tilecount = columns * rows;
|
||||
|
||||
map->tilewidth = BENCH_TILE_SIZE;
|
||||
map->tileheight = BENCH_TILE_SIZE;
|
||||
map->width = BENCH_MAP_W;
|
||||
map->height = BENCH_MAP_H;
|
||||
map->numlayers = 1;
|
||||
map->numtilesets = tilesets;
|
||||
|
||||
for ( i = 0; i < tilesets; i++ ) {
|
||||
map->tilesets[i].firstgid = 1 + (i * tilecount);
|
||||
map->tilesets[i].tilecount = tilecount;
|
||||
map->tilesets[i].columns = columns;
|
||||
map->tilesets[i].tilewidth = BENCH_TILE_SIZE;
|
||||
map->tilesets[i].tileheight = BENCH_TILE_SIZE;
|
||||
map->tilesets[i].imagewidth = benchtiles->w;
|
||||
map->tilesets[i].imageheight = benchtiles->h;
|
||||
map->tilesets[i].texture = benchtiles;
|
||||
snprintf((char *)&map->tilesets[i].name, AKGL_TILEMAP_MAX_TILESET_NAME_SIZE, "benchtileset%d", i);
|
||||
PASS(errctx, akgl_tilemap_compute_tileset_offsets(map, i));
|
||||
}
|
||||
|
||||
lastgid = map->tilesets[tilesets - 1].firstgid;
|
||||
map->layers[0].type = AKGL_TILEMAP_LAYER_TYPE_TILES;
|
||||
map->layers[0].width = BENCH_MAP_W;
|
||||
map->layers[0].height = BENCH_MAP_H;
|
||||
map->layers[0].visible = true;
|
||||
map->layers[0].opacity = 1.0;
|
||||
for ( i = 0; i < (BENCH_MAP_W * BENCH_MAP_H); i++ ) {
|
||||
cell = i % tilecount;
|
||||
map->layers[0].data[i] = lastgid + cell;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Load the test sprite and character, then fill the actor pool with actors wearing it.
|
||||
*
|
||||
* These are the real assets, loaded through the real loaders, so the actors have
|
||||
* a texture-backed sprite and akgl_actor_render takes its full path rather than
|
||||
* bailing out at the missing-sprite branch. Facing is pinned by turning
|
||||
* `movement_controls_face` off: the automatic face logic clears every facing bit
|
||||
* for an actor that is not moving, and the fixture character maps its sprite to
|
||||
* `ALIVE | FACE_LEFT`.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_populate_scene(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Actor *actor = NULL;
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||
int i = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||
PASS(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
||||
PASS(errctx, akgl_character_load_json("assets/testcharacter.json"));
|
||||
|
||||
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||||
memset(&name, 0x00, sizeof(name));
|
||||
snprintf((char *)&name, AKGL_ACTOR_MAX_NAME_LENGTH, "benchactor%d", i);
|
||||
PASS(errctx, akgl_heap_next_actor(&actor));
|
||||
PASS(errctx, akgl_actor_initialize(actor, (char *)&name));
|
||||
PASS(errctx, akgl_actor_set_character(actor, "testcharacter"));
|
||||
actor->visible = true;
|
||||
actor->layer = 0;
|
||||
actor->movement_controls_face = false;
|
||||
actor->x = (float32_t)((i * 37) % BENCH_SCREEN_W);
|
||||
actor->y = (float32_t)((i * 23) % BENCH_SCREEN_H);
|
||||
actor->curSpriteFrameId = 0;
|
||||
AKGL_BITMASK_ADD(actor->state, (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the empty frame: clear the target and present it.
|
||||
*
|
||||
* Everything else a frame does is on top of this. It is also the only benchmark
|
||||
* here whose cost is entirely SDL's rather than libakgl's, which makes it the
|
||||
* baseline the rest are worth reading against.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_frame(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(5000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("frame_start + frame_end, 640x480", "frame", 230000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = renderer->frame_start(renderer);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = renderer->frame_end(renderer);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the immediate-mode primitives.
|
||||
*
|
||||
* Each of these saves the renderer's draw colour, sets its own, draws, and puts
|
||||
* the colour back, so the fixed cost per call is three SDL round trips whatever
|
||||
* the shape. The shapes are sized like something a game would actually draw: a
|
||||
* full-diagonal line, a dialogue-box-sized rectangle, a 64 pixel radius circle.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_primitives(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
SDL_Color red = { 0xff, 0x00, 0x00, 0xff };
|
||||
SDL_FRect rect = { .x = 32.0, .y = 32.0, .w = 200.0, .h = 150.0 };
|
||||
int count = bench_iterations(100000);
|
||||
int filled = bench_iterations(2000);
|
||||
int circles = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("draw_point", "call", 1400.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_draw_point(renderer, 320.0, 240.0, red));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_line, screen diagonal", "call", 6200.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_draw_line(renderer, 0.0, 0.0, 639.0, 479.0, red));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_rect, 200x150 outline", "call", 5000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_draw_rect(renderer, &rect, red));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_filled_rect, 200x150", "call", 440000.0);
|
||||
BENCH_LOOP(inner, i, filled, akgl_draw_filled_rect(renderer, &rect, red));
|
||||
BENCH_FLUSH_STOP(errctx, filled);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_circle, radius 64", "call", 25000.0);
|
||||
BENCH_LOOP(inner, i, circles, akgl_draw_circle(renderer, 320.0, 240.0, 64.0, red));
|
||||
BENCH_FLUSH_STOP(errctx, circles);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the framebuffer round trips: save a region, put it back, flood fill.
|
||||
*
|
||||
* These are the expensive ones by construction. A copy reads pixels back off the
|
||||
* render target; a paste uploads a texture, draws it, and destroys it again; and
|
||||
* a flood fill reads back the *entire* target, converts the whole surface to
|
||||
* RGBA32, walks it, uploads a patch, and destroys everything. That is a
|
||||
* megabyte and a half of traffic for one fill at 640x480, whatever the size of
|
||||
* the region that actually changed.
|
||||
*
|
||||
* The fill alternates colours because a fill whose seed pixel already carries
|
||||
* the requested colour returns immediately, and would otherwise measure the
|
||||
* early exit rather than the fill.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_framebuffer(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
SDL_Surface *saved = NULL;
|
||||
SDL_Rect region = { .x = 64, .y = 64, .w = 64, .h = 64 };
|
||||
SDL_Color red = { 0xff, 0x00, 0x00, 0xff };
|
||||
SDL_Color blue = { 0x00, 0x00, 0xff, 0xff };
|
||||
int count = bench_iterations(5000);
|
||||
int fills = bench_iterations(200);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
PASS(errctx, akgl_draw_copy_region(renderer, ®ion, &saved));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("draw_copy_region, 64x64 readback", "call", 10000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
SDL_Surface *scratch = NULL;
|
||||
inner = akgl_draw_copy_region(renderer, ®ion, &scratch);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
SDL_DestroySurface(scratch);
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_paste_region, 64x64 upload", "call", 55000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_draw_paste_region(renderer, saved, 64.0, 64.0));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_flood_fill, full 640x480 target", "call", 20000000.0);
|
||||
for ( i = 0; i < fills; i++ ) {
|
||||
inner = akgl_draw_flood_fill(renderer, 320, 240, ( (i % 2) == 0 ) ? red : blue);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, fills);
|
||||
PASS(errctx, inner);
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
}
|
||||
SDL_DestroySurface(saved);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time measuring and drawing one line of text.
|
||||
*
|
||||
* akgl_text_rendertextat rasterizes the string, uploads it as a texture, blits
|
||||
* it, and destroys the texture again -- every call, with no cache anywhere. A
|
||||
* HUD with six readouts pays this six times a frame for text that changes once a
|
||||
* second.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_text(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
SDL_Color white = { 0xff, 0xff, 0xff, 0xff };
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int count = bench_iterations(20000);
|
||||
int lines = bench_iterations(5000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("text_measure, 15 characters", "call", 400.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_text_measure(benchfont, BENCH_TEXT, &w, &h));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("text_rendertextat, 15 characters", "call", 130000.0);
|
||||
BENCH_LOOP(inner, i, lines, akgl_text_rendertextat(benchfont, BENCH_TEXT, white, 0, 8, 8));
|
||||
BENCH_FLUSH_STOP(errctx, lines);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time loading assets off disk: a sprite, a character, and a whole map.
|
||||
*
|
||||
* This is startup and level-transition cost, not frame cost, and it is measured
|
||||
* because it is the part of a game the player waits through. The sprite load
|
||||
* after the first reuses the already registered spritesheet, so what is measured
|
||||
* is the JSON path rather than the image decode.
|
||||
*
|
||||
* @note Each character load leaks the SDL property set holding its
|
||||
* state-to-sprite map -- akgl_heap_release_character does not walk it.
|
||||
* That is a known defect rather than something this benchmark introduces;
|
||||
* it is repeated here often enough to be worth saying out loud.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_asset_load(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Sprite *sprite = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
int count = bench_iterations(2000);
|
||||
int maps = bench_iterations(50);
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
PASS(errctx, bench_populate_scene());
|
||||
|
||||
bench_start("sprite_load_json, sheet already loaded", "load", 180000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_sprite_load_json("assets/testsprite.json");
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
sprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||
if ( sprite == NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_sprite(sprite);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
PASS(errctx, bench_populate_scene());
|
||||
bench_start("character_load_json, two sprite mappings", "load", 150000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_character_load_json("assets/testcharacter.json");
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
|
||||
if ( basechar == NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_character(basechar);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
PASS(errctx, bench_populate_scene());
|
||||
bench_start("tilemap_load + release, fixture map", "load", 120000000.0);
|
||||
for ( i = 0; i < maps; i++ ) {
|
||||
// The map's object layer spawns actors into the pool, and neither
|
||||
// akgl_tilemap_release nor anything else gives them back, so a loop
|
||||
// that only loaded and released would exhaust the actor pool on its
|
||||
// 32nd iteration. Clearing the pool is part of what a host does
|
||||
// between levels anyway, and it is a pair of memsets against a map
|
||||
// load that decodes a 768x576 PNG.
|
||||
inner = akgl_heap_init_actor();
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_registry_init_actor();
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
// And the string pool has to be reclaimed by hand, because a map
|
||||
// load leaks five pooled strings that release does not give back.
|
||||
// The 52nd load would find the pool empty, and what happens then is
|
||||
// not an AKGL_ERR_HEAP: akgl_get_json_string_value finishes with
|
||||
// pass-up false, swallows the failed claim, and dereferences the
|
||||
// pointer it never set. Both defects are filed in TODO.md; this loop
|
||||
// works around the first so it never reaches the second.
|
||||
for ( j = 0; j < AKGL_MAX_HEAP_STRING; j++ ) {
|
||||
HEAP_STRING[j].refcount = 0;
|
||||
}
|
||||
inner = akgl_tilemap_load("assets/testmap.tmj", gamemap);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_tilemap_release(gamemap);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, maps);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time zeroing an akgl_Tilemap, which is the first thing a map load does.
|
||||
*
|
||||
* akgl_tilemap_load begins with `memset(dest, 0, sizeof(akgl_Tilemap))`, and an
|
||||
* akgl_Tilemap is 16 layers of 512x512 cells plus 16 tilesets of 65536 offset
|
||||
* pairs. The struct is that size whether the map is 2x2 or 512x512, so this is a
|
||||
* fixed toll on every level transition, paid before a single byte of the map
|
||||
* file has been read. Measuring it separately is the only way to tell how much
|
||||
* of the load benchmark above is the map and how much is the container.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_map_zero(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int count = bench_iterations(200);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("zeroing one akgl_Tilemap", "call", 16000000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
memset(gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time building a tileset's offset table.
|
||||
*
|
||||
* akgl_tilemap_compute_tileset_offsets fills one entry per tile in the image --
|
||||
* 1728 of them for the fixture tileset -- and a map pays it once per tileset at
|
||||
* load time. It is also the function whose table costs 512 KB per tileset,
|
||||
* which is the reason an akgl_Tilemap is as large as it is.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_tileset_offsets(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, bench_build_map(gamemap, 1));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("tilemap_compute_tileset_offsets, 1728 tiles", "call", 23000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_tilemap_compute_tileset_offsets(gamemap, 0));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time drawing a screenful of tiles, with one tileset and with eight.
|
||||
*
|
||||
* The camera covers 40 x 30 cells, so each of these is 1200 blits plus whatever
|
||||
* the tileset scan costs on top. The gap between the two numbers is that scan:
|
||||
* akgl_tilemap_draw walks every tileset for every tile and does not stop at the
|
||||
* one that matched, so the cost is linear in tileset count even though at most
|
||||
* one can ever answer.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_tilemap_draw(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(30);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
PASS(errctx, bench_build_map(gamemap, 1));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("tilemap_draw, 40x30 tiles, 1 tileset", "frame", 170000000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(gamemap, camera, 0));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
|
||||
PASS(errctx, bench_build_map(gamemap, BENCH_MAP_TILESETS));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("tilemap_draw, 40x30 tiles, 8 tilesets", "frame", 170000000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(gamemap, camera, 0));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
printf("tilemap_draw covers %d x %d = %d tiles per frame\n",
|
||||
(BENCH_SCREEN_W / BENCH_TILE_SIZE),
|
||||
(BENCH_SCREEN_H / BENCH_TILE_SIZE),
|
||||
((BENCH_SCREEN_W / BENCH_TILE_SIZE) * (BENCH_SCREEN_H / BENCH_TILE_SIZE)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The control: the same 1200 blits, issued straight to SDL.
|
||||
*
|
||||
* Without this, every drawing number in this file is unreadable -- there is no
|
||||
* way to tell how much of a 16 ms tilemap frame is libakgl deciding what to draw
|
||||
* and how much is the software rasterizer moving pixels. This does the same
|
||||
* amount of pixel work with none of the library in the way: same texture, same
|
||||
* source rectangles, same destinations, no bounds logic, no tileset scan, no
|
||||
* error contexts. Subtract it from the tilemap number and what is left is what
|
||||
* libakgl costs.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_raw_blit_control(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_FRect src = { .x = 0.0, .y = 0.0, .w = BENCH_TILE_SIZE, .h = BENCH_TILE_SIZE };
|
||||
SDL_FRect dest = { .x = 0.0, .y = 0.0, .w = BENCH_TILE_SIZE, .h = BENCH_TILE_SIZE };
|
||||
int across = (BENCH_SCREEN_W / BENCH_TILE_SIZE);
|
||||
int down = (BENCH_SCREEN_H / BENCH_TILE_SIZE);
|
||||
int columns = 0;
|
||||
int tilecount = 0;
|
||||
int tile = 0;
|
||||
int count = bench_iterations(30);
|
||||
int i = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int rep = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, benchtiles, AKERR_NULLPOINTER, "benchtiles");
|
||||
columns = benchtiles->w / BENCH_TILE_SIZE;
|
||||
tilecount = columns * (benchtiles->h / BENCH_TILE_SIZE);
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
// One source tile, blitted 1200 times. The floor: 1200 SDL calls and
|
||||
// 1200 x 256 pixels of copying, with the source rectangle staying in
|
||||
// cache the whole way.
|
||||
bench_start("raw SDL blits, one source tile (control)", "frame", 5000000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
src.x = 0.0;
|
||||
src.y = 0.0;
|
||||
for ( y = 0; y < down; y++ ) {
|
||||
for ( x = 0; x < across; x++ ) {
|
||||
dest.x = (float32_t)(x * BENCH_TILE_SIZE);
|
||||
dest.y = (float32_t)(y * BENCH_TILE_SIZE);
|
||||
SDL_RenderTexture(renderer->sdl_renderer, benchtiles, &src, &dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
|
||||
// The same 1200 blits reading the same scattered source tiles the map
|
||||
// asks for: cell (x, y) of a BENCH_MAP_W-wide map, so consecutive screen
|
||||
// rows read parts of the sheet BENCH_MAP_W tiles apart rather than
|
||||
// walking it in order. This is the honest control for
|
||||
// akgl_tilemap_draw -- same call count, same pixels, same access
|
||||
// pattern, none of libakgl in the path -- and the gap between the two is
|
||||
// what the library's own per-tile work costs.
|
||||
bench_start("raw SDL blits, map order (control)", "frame", 170000000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
for ( y = 0; y < down; y++ ) {
|
||||
for ( x = 0; x < across; x++ ) {
|
||||
tile = (x + (y * BENCH_MAP_W)) % tilecount;
|
||||
src.x = (float32_t)((tile % columns) * BENCH_TILE_SIZE);
|
||||
src.y = (float32_t)((tile / columns) * BENCH_TILE_SIZE);
|
||||
dest.x = (float32_t)(x * BENCH_TILE_SIZE);
|
||||
dest.y = (float32_t)(y * BENCH_TILE_SIZE);
|
||||
SDL_RenderTexture(renderer->sdl_renderer, benchtiles, &src, &dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time one actor's render, and then the whole scene through the backend.
|
||||
*
|
||||
* A render is two state-to-sprite lookups -- one for the sprite, one inside the
|
||||
* visibility test -- some frame arithmetic, and one blit. The scene benchmark
|
||||
* puts that together with the tilemap: one layer of tiles plus 64 actors, which
|
||||
* is what akgl_render_2d_draw_world does for a modest 2D game.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_scene(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
int count = bench_iterations(20000);
|
||||
int frames = bench_iterations(30);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, bench_populate_scene());
|
||||
PASS(errctx, bench_build_map(gamemap, 1));
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
actor = &HEAP_ACTOR[0];
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("actor_render, on camera", "actor", 31000.0);
|
||||
BENCH_LOOP(inner, i, count, actor->renderfunc(actor));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_world, 1200 tiles + 64 actors", "frame", 170000000.0);
|
||||
BENCH_LOOP(inner, i, frames, renderer->draw_world(renderer, NULL));
|
||||
BENCH_FLUSH_STOP(errctx, frames);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time a whole frame the way akgl_game_update runs one.
|
||||
*
|
||||
* This is the number a host actually gets, and it is not the sum of the parts.
|
||||
* akgl_game_update's update sweep is nested inside a loop over
|
||||
* #AKGL_TILEMAP_MAX_LAYERS and does not filter by layer, so every live actor is
|
||||
* updated sixteen times per frame rather than once. The gap between this
|
||||
* benchmark and the scene benchmark plus the logic frame in tests/perf.c is that
|
||||
* multiplier, and TODO.md carries it as a defect.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_game_update(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(30);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, bench_populate_scene());
|
||||
PASS(errctx, bench_build_map(gamemap, 1));
|
||||
PASS(errctx, akgl_physics_init_null(physics));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("game_update, full frame", "frame", 170000000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_game_update(NULL));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||
SDL_SetLogOutputFunction(bench_discard_log, NULL);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
renderer = &_akgl_renderer;
|
||||
physics = &_akgl_physics;
|
||||
camera = &_akgl_camera;
|
||||
gamemap = &_akgl_gamemap;
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_VIDEO),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
TTF_Init(),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize the font engine: %s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_CreateWindowAndRenderer(
|
||||
"net/aklabs/libakgl/test_perf_render",
|
||||
BENCH_SCREEN_W,
|
||||
BENCH_SCREEN_H,
|
||||
0,
|
||||
&window,
|
||||
&renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
CATCH(errctx, akgl_render_bind2d(renderer));
|
||||
|
||||
camera->x = 0.0;
|
||||
camera->y = 0.0;
|
||||
camera->w = BENCH_SCREEN_W;
|
||||
camera->h = BENCH_SCREEN_H;
|
||||
|
||||
// akgl_game_update takes this before it does anything else. Normally
|
||||
// akgl_game_init creates it; this suite brings the library up piece by
|
||||
// piece so it can stay headless, so it creates the mutex itself.
|
||||
game.statelock = SDL_CreateMutex();
|
||||
FAIL_ZERO_BREAK(errctx, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
// And this one is not optional either. akgl_game_updateFPS calls
|
||||
// game.lowfpsfunc through the pointer, unguarded, on every frame under
|
||||
// 30 fps -- which includes the first frame, before there is a frame rate
|
||||
// to compare. A host that brings the library up the way renderer.h
|
||||
// documents for an embedder, akgl_render_bind2d over its own window,
|
||||
// segfaults on its first akgl_game_update. Filed in TODO.md; installing
|
||||
// the default by hand is the workaround.
|
||||
game.lowfpsfunc = &akgl_game_lowfps;
|
||||
|
||||
benchfont = TTF_OpenFont(BENCH_FONT_PATH, BENCH_FONT_SIZE);
|
||||
FAIL_ZERO_BREAK(errctx, benchfont, AKGL_ERR_SDL, "Couldn't open %s: %s", BENCH_FONT_PATH, SDL_GetError());
|
||||
benchtiles = IMG_LoadTexture(renderer->sdl_renderer, BENCH_TILESET_IMAGE);
|
||||
FAIL_ZERO_BREAK(errctx, benchtiles, AKGL_ERR_SDL, "Couldn't load %s: %s", BENCH_TILESET_IMAGE, SDL_GetError());
|
||||
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, akgl_registry_init_properties());
|
||||
|
||||
CATCH(errctx, bench_frame());
|
||||
CATCH(errctx, bench_primitives());
|
||||
CATCH(errctx, bench_framebuffer());
|
||||
CATCH(errctx, bench_text());
|
||||
CATCH(errctx, bench_asset_load());
|
||||
CATCH(errctx, bench_map_zero());
|
||||
CATCH(errctx, bench_tileset_offsets());
|
||||
CATCH(errctx, bench_tilemap_draw());
|
||||
CATCH(errctx, bench_raw_blit_control());
|
||||
CATCH(errctx, bench_scene());
|
||||
CATCH(errctx, bench_game_update());
|
||||
|
||||
BENCH_REPORT_BREAK(errctx);
|
||||
} CLEANUP {
|
||||
if ( benchtiles != NULL ) {
|
||||
SDL_DestroyTexture(benchtiles);
|
||||
}
|
||||
if ( benchfont != NULL ) {
|
||||
TTF_CloseFont(benchfont);
|
||||
}
|
||||
TTF_Quit();
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
@@ -722,6 +722,7 @@ int main(void)
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, akgl_registry_init_properties());
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
typedef akerr_ErrorContext *(*RegistryFuncPtr)(void);
|
||||
|
||||
@@ -17,7 +23,7 @@ akerr_ErrorContext *test_akgl_registry_init(RegistryFuncPtr funcptr)
|
||||
SDL_calloc_func calloc_func;
|
||||
SDL_realloc_func realloc_func;
|
||||
SDL_free_func free_func;
|
||||
|
||||
|
||||
SDL_GetMemoryFunctions(
|
||||
&malloc_func,
|
||||
&calloc_func,
|
||||
@@ -54,7 +60,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
printf("Sucess\n");
|
||||
printf("Sucess\n");
|
||||
} FINISH(errctx, true);
|
||||
|
||||
ATTEMPT {
|
||||
@@ -62,7 +68,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
printf("Sucess\n");
|
||||
printf("Sucess\n");
|
||||
} FINISH(errctx, true);
|
||||
|
||||
ATTEMPT {
|
||||
@@ -70,7 +76,7 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
printf("Sucess\n");
|
||||
printf("Sucess\n");
|
||||
} FINISH(errctx, true);
|
||||
|
||||
ATTEMPT {
|
||||
@@ -78,7 +84,76 @@ akerr_ErrorContext *test_akgl_registry_init_creation_failures(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
printf("Sucess\n");
|
||||
printf("Sucess\n");
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief akgl_get_property must copy the value, and not a byte more.
|
||||
*
|
||||
* It used to copy a fixed AKGL_MAX_STRING_LENGTH bytes out of whatever SDL
|
||||
* returned, and what SDL returns is its own strdup of the value -- four bytes
|
||||
* for "0.0". That read up to 4 KiB past the end of somebody else's allocation on
|
||||
* every call, which valgrind reported twelve times over in tests/physics.c
|
||||
* alone.
|
||||
*
|
||||
* The destination is filled with a sentinel first, so the assertion is not
|
||||
* "the value arrived" -- that would pass either way -- but "the bytes past the
|
||||
* terminator were left alone", which is only true if the copy was bounded by
|
||||
* the source.
|
||||
*/
|
||||
akerr_ErrorContext *test_akgl_get_property_copies_only_the_value(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_String *value = NULL;
|
||||
char oversized[AKGL_MAX_STRING_LENGTH + 8];
|
||||
size_t valuelen = 0;
|
||||
size_t i = 0;
|
||||
bool untouched = true;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init_properties());
|
||||
CATCH(errctx, akgl_heap_next_string(&value));
|
||||
|
||||
memset(&value->data, 0x5a, AKGL_MAX_STRING_LENGTH);
|
||||
CATCH(errctx, akgl_set_property("registry.short", "0.0"));
|
||||
CATCH(errctx, akgl_get_property("registry.short", &value, "unset"));
|
||||
|
||||
valuelen = strlen("0.0");
|
||||
TEST_ASSERT(errctx, strcmp(value->data, "0.0") == 0,
|
||||
"akgl_get_property returned \"%s\", expected \"0.0\"", value->data);
|
||||
for ( i = (valuelen + 1); i < AKGL_MAX_STRING_LENGTH; i++ ) {
|
||||
TEST_ASSERT_FLAG(untouched, (value->data[i] == 0x5a));
|
||||
}
|
||||
TEST_ASSERT(errctx, untouched,
|
||||
"akgl_get_property wrote past the end of a %zu byte value", valuelen);
|
||||
|
||||
// The default takes the same path as a stored value.
|
||||
CATCH(errctx, akgl_get_property("registry.absent", &value, "fallback"));
|
||||
TEST_ASSERT(errctx, strcmp(value->data, "fallback") == 0,
|
||||
"akgl_get_property returned \"%s\" for an unset property, expected the default",
|
||||
value->data);
|
||||
|
||||
// A value too long for an akgl_String is refused rather than truncated
|
||||
// into an unterminated buffer.
|
||||
memset(&oversized, 'x', sizeof(oversized));
|
||||
oversized[AKGL_MAX_STRING_LENGTH + 7] = '\0';
|
||||
CATCH(errctx, akgl_set_property("registry.oversized", (char *)&oversized));
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_get_property("registry.oversized", &value, "unset"),
|
||||
"reading a property longer than an akgl_String");
|
||||
|
||||
// And the documented refusal survives: unset, with no default.
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_get_property("registry.absent", &value, NULL),
|
||||
"reading an unset property with no default");
|
||||
} CLEANUP {
|
||||
if ( value != NULL ) {
|
||||
IGNORE(akgl_heap_release_string(value));
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
@@ -87,7 +162,9 @@ int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, test_akgl_registry_init_creation_failures());
|
||||
CATCH(errctx, test_akgl_get_property_copies_only_the_value());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
222
tests/renderer.c
Normal file
222
tests/renderer.c
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* @file renderer.c
|
||||
* @brief Unit tests for the 2D render backend's vtable and its entry points.
|
||||
*
|
||||
* None of this needs a display. akgl_render_bind2d() installs the backend's
|
||||
* methods and nothing else, so it can be checked against a backend that has no
|
||||
* SDL_Renderer at all; the entry points that do draw are checked against a
|
||||
* software renderer under the dummy video driver, the same way tests/draw.c
|
||||
* does it.
|
||||
*
|
||||
* akgl_render_init2d() is not covered here: it creates a window from the
|
||||
* property registry and writes the `camera` global, which is what the offscreen
|
||||
* harness described in TODO.md exists to make testable.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/renderer.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Width and height of the offscreen target the drawing tests use. */
|
||||
#define TEST_TARGET_SIZE 32
|
||||
|
||||
/** @brief A backend bound to a live software renderer, built by main(). */
|
||||
static akgl_RenderBackend bound;
|
||||
|
||||
akerr_ErrorContext *test_render_bind2d(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_RenderBackend backend;
|
||||
|
||||
ATTEMPT {
|
||||
// The state a host is in when it owns its own window: a zeroed backend
|
||||
// with somebody else's SDL_Renderer in it. Binding must fill in the six
|
||||
// methods and leave that renderer alone -- creating a second window is
|
||||
// precisely what an embedded interpreter must not do.
|
||||
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
|
||||
backend.sdl_renderer = (SDL_Renderer *)&bound;
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_render_bind2d(&backend), "binding the 2D backend");
|
||||
|
||||
TEST_ASSERT(errctx, backend.sdl_renderer == (SDL_Renderer *)&bound,
|
||||
"binding replaced the caller's SDL_Renderer");
|
||||
TEST_ASSERT(errctx, backend.shutdown == &akgl_render_2d_shutdown,
|
||||
"binding did not install shutdown");
|
||||
TEST_ASSERT(errctx, backend.frame_start == &akgl_render_2d_frame_start,
|
||||
"binding did not install frame_start");
|
||||
TEST_ASSERT(errctx, backend.frame_end == &akgl_render_2d_frame_end,
|
||||
"binding did not install frame_end");
|
||||
TEST_ASSERT(errctx, backend.draw_texture == &akgl_render_2d_draw_texture,
|
||||
"binding did not install draw_texture");
|
||||
TEST_ASSERT(errctx, backend.draw_mesh == &akgl_render_2d_draw_mesh,
|
||||
"binding did not install draw_mesh");
|
||||
TEST_ASSERT(errctx, backend.draw_world == &akgl_render_2d_draw_world,
|
||||
"binding did not install draw_world");
|
||||
|
||||
// Binding a backend that has no renderer yet is legal -- the two halves
|
||||
// are separable in both directions -- and leaves it NULL rather than
|
||||
// inventing one.
|
||||
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
|
||||
TEST_EXPECT_OK(errctx, akgl_render_bind2d(&backend),
|
||||
"binding a backend with no SDL renderer");
|
||||
TEST_ASSERT(errctx, backend.sdl_renderer == NULL,
|
||||
"binding invented an SDL_Renderer");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_bind2d(NULL),
|
||||
"binding a NULL backend");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_render_backend_without_a_renderer(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_RenderBackend empty;
|
||||
|
||||
ATTEMPT {
|
||||
// Bound but never given an SDL_Renderer. Every entry point that draws
|
||||
// has to report that rather than dereference it.
|
||||
memset(&empty, 0x00, sizeof(akgl_RenderBackend));
|
||||
CATCH(errctx, akgl_render_bind2d(&empty));
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_start(&empty),
|
||||
"starting a frame on a backend with no renderer");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, empty.frame_end(&empty),
|
||||
"ending a frame on a backend with no renderer");
|
||||
|
||||
// Shutdown is the one that has nothing to release yet, so it succeeds.
|
||||
TEST_EXPECT_OK(errctx, empty.shutdown(&empty), "shutting down an unused backend");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_render_frames_and_textures(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *surface = NULL;
|
||||
SDL_Texture *texture = NULL;
|
||||
SDL_FRect dest;
|
||||
SDL_FPoint center;
|
||||
|
||||
ATTEMPT {
|
||||
dest.x = 0.0f;
|
||||
dest.y = 0.0f;
|
||||
dest.w = 8.0f;
|
||||
dest.h = 8.0f;
|
||||
center.x = 4.0f;
|
||||
center.y = 4.0f;
|
||||
|
||||
surface = SDL_CreateSurface(8, 8, SDL_PIXELFORMAT_RGBA32);
|
||||
FAIL_ZERO_BREAK(errctx, surface, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
texture = SDL_CreateTextureFromSurface(bound.sdl_renderer, surface);
|
||||
FAIL_ZERO_BREAK(errctx, texture, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
TEST_EXPECT_OK(errctx, bound.frame_start(&bound), "starting a frame");
|
||||
TEST_EXPECT_OK(errctx,
|
||||
bound.draw_texture(&bound, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE),
|
||||
"blitting a texture unrotated");
|
||||
// A non-zero angle takes the rotated path, which is the one that
|
||||
// consults the pivot and the flip mode.
|
||||
TEST_EXPECT_OK(errctx,
|
||||
bound.draw_texture(&bound, texture, NULL, &dest, 90.0, ¢er, SDL_FLIP_HORIZONTAL),
|
||||
"blitting a texture rotated");
|
||||
TEST_EXPECT_OK(errctx, bound.frame_end(&bound), "ending a frame");
|
||||
|
||||
// A rotation with no pivot is refused: SDL's "NULL means the centre of
|
||||
// dest" convention is not offered here, so a NULL is a caller mistake
|
||||
// rather than a default.
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
bound.draw_texture(&bound, texture, NULL, &dest, 90.0, NULL, SDL_FLIP_NONE),
|
||||
"blitting rotated with no pivot");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_render_2d_draw_texture(NULL, texture, NULL, &dest, 0, NULL, SDL_FLIP_NONE),
|
||||
"blitting through a NULL backend");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
bound.draw_texture(&bound, NULL, NULL, &dest, 0, NULL, SDL_FLIP_NONE),
|
||||
"blitting a NULL texture");
|
||||
} CLEANUP {
|
||||
if ( texture != NULL ) {
|
||||
SDL_DestroyTexture(texture);
|
||||
}
|
||||
if ( surface != NULL ) {
|
||||
SDL_DestroySurface(surface);
|
||||
}
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_render_unimplemented_and_guards(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
// The 3D hook is not a stub that quietly does nothing. It refuses, so a
|
||||
// caller that reaches it finds out on the first frame.
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_API, bound.draw_mesh(&bound),
|
||||
"drawing a mesh through the 2D backend");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_render_2d_draw_world(NULL, NULL),
|
||||
"drawing the world through a NULL backend");
|
||||
|
||||
// Shutting the 2D backend down releases nothing today, and says so.
|
||||
TEST_EXPECT_OK(errctx, bound.shutdown(&bound), "shutting the backend down");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
memset(&bound, 0x00, sizeof(akgl_RenderBackend));
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_VIDEO),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_CreateWindowAndRenderer(
|
||||
"net/aklabs/libakgl/test_renderer",
|
||||
TEST_TARGET_SIZE,
|
||||
TEST_TARGET_SIZE,
|
||||
0,
|
||||
&window,
|
||||
&bound.sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
// The host owns the window; libakgl only ever binds to it. This is the
|
||||
// arrangement akgl_render_bind2d exists for.
|
||||
CATCH(errctx, akgl_render_bind2d(&bound));
|
||||
|
||||
CATCH(errctx, test_render_bind2d());
|
||||
CATCH(errctx, test_render_backend_without_a_renderer());
|
||||
CATCH(errctx, test_render_frames_and_textures());
|
||||
CATCH(errctx, test_render_unimplemented_and_guards());
|
||||
} CLEANUP {
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
229
tests/sprite.c
229
tests/sprite.c
@@ -16,53 +16,53 @@
|
||||
|
||||
akerr_ErrorContext *test_akgl_spritesheet_initialize(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_SpriteSheet *sheet = NULL;
|
||||
SDL_Texture *image = NULL;
|
||||
akgl_String *tmpstr = NULL;
|
||||
// Does the image file get loaded?
|
||||
// Is the image file loaded correctly? (Surface comparison)
|
||||
// Is the spritesheet in the registry?
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_spritesheet(&sheet));
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
CATCH(errctx, akgl_spritesheet_initialize(sheet, 48, 48, "assets/spritesheet.png"));
|
||||
FAIL_ZERO_BREAK(errctx, sheet->texture, AKERR_VALUE, "akgl_spritesheet_initialize failed to load the sprite texture");
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
((sheet->texture->w != 576) || (sheet->texture->h != 384)),
|
||||
AKERR_VALUE,
|
||||
"Loaded texture was not the correct size");
|
||||
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
|
||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_SpriteSheet *sheet = NULL;
|
||||
SDL_Texture *image = NULL;
|
||||
akgl_String *tmpstr = NULL;
|
||||
// Does the image file get loaded?
|
||||
// Is the image file loaded correctly? (Surface comparison)
|
||||
// Is the spritesheet in the registry?
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_spritesheet(&sheet));
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
|
||||
CATCH(
|
||||
errctx,
|
||||
akgl_render_and_compare(
|
||||
sheet->texture,
|
||||
image,
|
||||
0, 0, 576, 384,
|
||||
"test_spritesheet_loaded_image.png")
|
||||
);
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
CATCH(errctx, akgl_spritesheet_initialize(sheet, 48, 48, "assets/spritesheet.png"));
|
||||
FAIL_ZERO_BREAK(errctx, sheet->texture, AKERR_VALUE, "akgl_spritesheet_initialize failed to load the sprite texture");
|
||||
FAIL_NONZERO_BREAK(
|
||||
errctx,
|
||||
((sheet->texture->w != 576) || (sheet->texture->h != 384)),
|
||||
AKERR_VALUE,
|
||||
"Loaded texture was not the correct size");
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITESHEET, "assets/spritesheet.png", NULL),
|
||||
AKERR_KEY,
|
||||
"Spritesheet was not placed in the registry");
|
||||
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(tmpstr));
|
||||
IGNORE(akgl_heap_release_spritesheet(sheet));
|
||||
if ( image != NULL )
|
||||
SDL_DestroyTexture(image);
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
|
||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||
|
||||
CATCH(
|
||||
errctx,
|
||||
akgl_render_and_compare(
|
||||
sheet->texture,
|
||||
image,
|
||||
0, 0, 576, 384,
|
||||
"test_spritesheet_loaded_image.png")
|
||||
);
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITESHEET, "assets/spritesheet.png", NULL),
|
||||
AKERR_KEY,
|
||||
"Spritesheet was not placed in the registry");
|
||||
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(tmpstr));
|
||||
IGNORE(akgl_heap_release_spritesheet(sheet));
|
||||
if ( image != NULL )
|
||||
SDL_DestroyTexture(image);
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_akgl_sprite_initialize(void)
|
||||
@@ -71,28 +71,28 @@ akerr_ErrorContext *test_akgl_sprite_initialize(void)
|
||||
akgl_SpriteSheet *testsheet = NULL;
|
||||
akgl_Sprite *testsprite = NULL;
|
||||
akgl_String *tmpstr = NULL;
|
||||
|
||||
|
||||
// Does the sprite get loaded?
|
||||
// Do all frames of the sprite get loaded?
|
||||
// Are all the frames of the sprite what we expect? (Surface comparison)
|
||||
// Is the sprite added to the registry?
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_spritesheet(&testsheet));
|
||||
CATCH(errctx, akgl_heap_next_sprite(&testsprite));
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
CATCH(errctx, akgl_spritesheet_initialize(testsheet, 48, 48, "assets/spritesheet.png"));
|
||||
FAIL_ZERO_BREAK(errctx, testsheet, AKERR_VALUE, "akgl_spritesheet_initialize failed");
|
||||
CATCH(errctx, akgl_heap_next_spritesheet(&testsheet));
|
||||
CATCH(errctx, akgl_heap_next_sprite(&testsprite));
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
CATCH(errctx, akgl_spritesheet_initialize(testsheet, 48, 48, "assets/spritesheet.png"));
|
||||
FAIL_ZERO_BREAK(errctx, testsheet, AKERR_VALUE, "akgl_spritesheet_initialize failed");
|
||||
|
||||
CATCH(errctx, akgl_sprite_initialize(testsprite, "test", testsheet));
|
||||
FAIL_NONZERO_BREAK(errctx, (testsprite->sheet != testsheet), AKERR_VALUE, "Initialized sprite uses wrong sheet");
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "test", NULL),
|
||||
AKERR_KEY,
|
||||
"Sprite was not placed in the registry");
|
||||
|
||||
CATCH(errctx, akgl_sprite_initialize(testsprite, "test", testsheet));
|
||||
FAIL_NONZERO_BREAK(errctx, (testsprite->sheet != testsheet), AKERR_VALUE, "Initialized sprite uses wrong sheet");
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "test", NULL),
|
||||
AKERR_KEY,
|
||||
"Sprite was not placed in the registry");
|
||||
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_sprite(testsprite));
|
||||
IGNORE(akgl_heap_release_string(tmpstr));
|
||||
@@ -108,65 +108,65 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
|
||||
akgl_Sprite *testsprite2 = NULL;
|
||||
akgl_String *tmpstr = NULL;
|
||||
SDL_Texture *image = NULL;
|
||||
|
||||
|
||||
// Does the sprite get loaded?
|
||||
// Do all frames of the sprite get loaded?
|
||||
// Are all the frames of the sprite what we expect? (Surface comparison)
|
||||
// Is the sprite added to the registry?
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite,
|
||||
AKERR_KEY,
|
||||
"akgl_sprite_load_json succeeds but sprite is not placed in the registry");
|
||||
CATCH(errctx, akgl_heap_next_string(&tmpstr));
|
||||
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->width == 48), AKERR_VALUE, "width incorrect (48 : %d)", testsprite->width);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->height == 48), AKERR_VALUE, "height incorrect (48 : %d)", testsprite->height);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100000000), AKERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), AKERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->loopReverse == true), AKERR_VALUE, "loopReverse incorrect (1 : %d)", testsprite->loopReverse);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->frames == 3), AKERR_VALUE, "frame count incorrect (3 : %d)", testsprite->frames);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[0] == 12), AKERR_VALUE, "frameids[0] incorrect (12 : %d)", testsprite->frameids[0]);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[1] == 13), AKERR_VALUE, "frameids[1] incorrect (13 : %d)", testsprite->frameids[1]);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[2] == 14), AKERR_VALUE, "frameids[2] incorrect (14 : %d)", testsprite->frameids[2]);
|
||||
FAIL_NONZERO_BREAK(errctx, strcmp(testsprite->name, "testsprite"), AKERR_VALUE, "name incorrect (testsprite : %s)", (char *)testsprite->name);
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||
testsprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite,
|
||||
AKERR_KEY,
|
||||
"akgl_sprite_load_json succeeds but sprite is not placed in the registry");
|
||||
|
||||
// Is it using the right spritesheet?
|
||||
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
|
||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->width == 48), AKERR_VALUE, "width incorrect (48 : %d)", testsprite->width);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->height == 48), AKERR_VALUE, "height incorrect (48 : %d)", testsprite->height);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->speed == 100000000), AKERR_VALUE, "speed incorrect (100 : %d)", testsprite->speed);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->loop == true), AKERR_VALUE, "loop incorrect (1 : %d)", testsprite->loop);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->loopReverse == true), AKERR_VALUE, "loopReverse incorrect (1 : %d)", testsprite->loopReverse);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->frames == 3), AKERR_VALUE, "frame count incorrect (3 : %d)", testsprite->frames);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[0] == 12), AKERR_VALUE, "frameids[0] incorrect (12 : %d)", testsprite->frameids[0]);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[1] == 13), AKERR_VALUE, "frameids[1] incorrect (13 : %d)", testsprite->frameids[1]);
|
||||
FAIL_ZERO_BREAK(errctx, (testsprite->frameids[2] == 14), AKERR_VALUE, "frameids[2] incorrect (14 : %d)", testsprite->frameids[2]);
|
||||
FAIL_NONZERO_BREAK(errctx, strcmp(testsprite->name, "testsprite"), AKERR_VALUE, "name incorrect (testsprite : %s)", (char *)testsprite->name);
|
||||
|
||||
CATCH(
|
||||
errctx,
|
||||
akgl_render_and_compare(
|
||||
testsprite->sheet->texture,
|
||||
image,
|
||||
0, 0, 576, 384,
|
||||
"test_sprite_loaded_from_json_sheet.png"
|
||||
)
|
||||
);
|
||||
// Is it using the right spritesheet?
|
||||
|
||||
// If we load a second sprite using the same sheet name, do they use the same sheet in memory?
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testsprite2.json");
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
||||
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite,
|
||||
AKERR_KEY,
|
||||
"akgl_sprite_load_json succeeds but second sprite is not placed in the registry");
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/spritesheet.png");
|
||||
image = IMG_LoadTexture(renderer->sdl_renderer, (char *)&tmpstr->data);
|
||||
FAIL_ZERO_BREAK(errctx, image, AKGL_ERR_SDL, "Failed to load comparison image");
|
||||
|
||||
CATCH(
|
||||
errctx,
|
||||
akgl_render_and_compare(
|
||||
testsprite->sheet->texture,
|
||||
image,
|
||||
0, 0, 576, 384,
|
||||
"test_sprite_loaded_from_json_sheet.png"
|
||||
)
|
||||
);
|
||||
|
||||
// If we load a second sprite using the same sheet name, do they use the same sheet in memory?
|
||||
snprintf((char *)&tmpstr->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), "assets/testsprite2.json");
|
||||
CATCH(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
||||
testsprite2 = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite2", NULL);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testsprite,
|
||||
AKERR_KEY,
|
||||
"akgl_sprite_load_json succeeds but second sprite is not placed in the registry");
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
(testsprite->sheet == testsprite2->sheet),
|
||||
AKERR_VALUE,
|
||||
"Previously loaded spritesheets are not reused");
|
||||
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
(testsprite->sheet == testsprite2->sheet),
|
||||
AKERR_VALUE,
|
||||
"Previously loaded spritesheets are not reused");
|
||||
|
||||
} CLEANUP {
|
||||
if ( testsprite != NULL ) {
|
||||
IGNORE(akgl_heap_release_sprite(testsprite));
|
||||
@@ -185,16 +185,17 @@ akerr_ErrorContext *test_akgl_sprite_load_json(void)
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
renderer = &_akgl_renderer;
|
||||
|
||||
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 640, 480, 0, &window, &renderer->sdl_renderer)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
@@ -8,19 +8,19 @@ void reset_string_heap(void);
|
||||
|
||||
akerr_ErrorContext *test_fresh_heap_gives_strings(void)
|
||||
{
|
||||
akgl_String *ptr = NULL;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_string(&ptr));
|
||||
} CLEANUP {
|
||||
reset_string_heap();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
}
|
||||
akgl_String *ptr = NULL;
|
||||
|
||||
return 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
for ( int i = 0; i < AKGL_MAX_HEAP_STRING - 1; i++ ) {
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_heap_next_string(&ptr));
|
||||
} CLEANUP {
|
||||
reset_string_heap();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_string_heap_error_when_no_strings_left(void)
|
||||
@@ -95,7 +95,7 @@ akerr_ErrorContext *test_strcpy_to_all_strings_no_segfault(void)
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_akgl_string_initialize(void)
|
||||
{
|
||||
@@ -111,7 +111,7 @@ akerr_ErrorContext *test_akgl_string_initialize(void)
|
||||
CATCH(errctx, akgl_string_initialize(ptr, "Test value"));
|
||||
FAIL_NONZERO_BREAK(errctx, strcmp((char *)&ptr->data, "Test value"), AKERR_VALUE, "Expected 'Test value', got %s", (char *)&ptr->data);
|
||||
|
||||
CATCH(errctx, akgl_heap_release_string(NULL));
|
||||
CATCH(errctx, akgl_heap_release_string(NULL));
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Failure to properly handle NULL pointer");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
@@ -130,9 +130,10 @@ void reset_string_heap(void)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
printf("test_fresh_heap_gives_string ....\n");
|
||||
test_fresh_heap_gives_strings();
|
||||
reset_string_heap();
|
||||
@@ -150,6 +151,6 @@ int main(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
#include <akgl/error.h>
|
||||
|
||||
/** @brief Fail the enclosing ATTEMPT block unless @p cond holds. */
|
||||
#define TEST_ASSERT(e, cond, ...) \
|
||||
if ( ! (cond) ) { \
|
||||
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \
|
||||
#define TEST_ASSERT(e, cond, ...) \
|
||||
if ( ! (cond) ) { \
|
||||
FAIL_BREAK(e, AKGL_ERR_BEHAVIOR, __VA_ARGS__); \
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,25 +33,25 @@
|
||||
* paths in a row without draining AKERR_ARRAY_ERROR. Pass 0 for @p expected to
|
||||
* require success.
|
||||
*/
|
||||
#define TEST_EXPECT_STATUS(e, expected, stmt, desc) \
|
||||
{ \
|
||||
akerr_ErrorContext *__tec = (stmt); \
|
||||
int __tst = ( __tec == NULL ) ? 0 : __tec->status; \
|
||||
if ( __tec != NULL ) { \
|
||||
__tec->handled = true; \
|
||||
__tec = akerr_release_error(__tec); \
|
||||
} \
|
||||
if ( __tst != (expected) ) { \
|
||||
FAIL_BREAK( \
|
||||
e, \
|
||||
AKGL_ERR_BEHAVIOR, \
|
||||
"%s: expected status %d (%s), got %d (%s)", \
|
||||
desc, \
|
||||
(int)(expected), \
|
||||
akerr_name_for_status((int)(expected), NULL), \
|
||||
__tst, \
|
||||
akerr_name_for_status(__tst, NULL)); \
|
||||
} \
|
||||
#define TEST_EXPECT_STATUS(e, expected, stmt, desc) \
|
||||
{ \
|
||||
akerr_ErrorContext *__tec = (stmt); \
|
||||
int __tst = ( __tec == NULL ) ? 0 : __tec->status; \
|
||||
if ( __tec != NULL ) { \
|
||||
__tec->handled = true; \
|
||||
__tec = akerr_release_error(__tec); \
|
||||
} \
|
||||
if ( __tst != (expected) ) { \
|
||||
FAIL_BREAK( \
|
||||
e, \
|
||||
AKGL_ERR_BEHAVIOR, \
|
||||
"%s: expected status %d (%s), got %d (%s)", \
|
||||
desc, \
|
||||
(int)(expected), \
|
||||
akerr_name_for_status((int)(expected), NULL), \
|
||||
__tst, \
|
||||
akerr_name_for_status(__tst, NULL)); \
|
||||
} \
|
||||
}
|
||||
|
||||
/** @brief Require that @p stmt succeeds, reporting @p desc if it does not. */
|
||||
|
||||
387
tests/text.c
Normal file
387
tests/text.c
Normal file
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* @file text.c
|
||||
* @brief Unit tests for font loading and text measurement.
|
||||
*
|
||||
* Measurement needs a font but no renderer at all. Drawing needs one, but not a
|
||||
* display: a software renderer over a window under the dummy video driver takes
|
||||
* akgl_text_rendertextat() all the way from rasterizing to blitting, so this
|
||||
* suite covers it without waiting for the offscreen harness. What is still
|
||||
* missing is any assertion about the *pixels* -- that is the harness's job.
|
||||
*
|
||||
* The fixture font is monospaced on purpose: the width of an N-character string
|
||||
* is exactly N times the width of one character, so every assertion below is a
|
||||
* relationship between measurements rather than a hardcoded pixel count that
|
||||
* would break when FreeType changes its rounding.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/text.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief The monospaced ASCII subset described in assets/akgl_test_mono.LICENSE.txt. */
|
||||
#define TEST_FONT_PATH "assets/akgl_test_mono.ttf"
|
||||
/** @brief Point size every test in this file opens the fixture font at. */
|
||||
#define TEST_FONT_SIZE 16
|
||||
/** @brief Width and height of the offscreen target the drawing tests draw into. */
|
||||
#define TEST_TARGET_SIZE 128
|
||||
|
||||
/** @brief The fixture font, opened once by main() and shared by every test. */
|
||||
static TTF_Font *testfont = NULL;
|
||||
/** @brief A 2D backend bound to a software renderer, built by main(). */
|
||||
static akgl_RenderBackend testbackend;
|
||||
|
||||
akerr_ErrorContext *test_text_loadfont(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
TTF_Font *registered = NULL;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_registry_init_font());
|
||||
|
||||
TEST_EXPECT_OK(
|
||||
errctx,
|
||||
akgl_text_loadfont("testfont", TEST_FONT_PATH, TEST_FONT_SIZE),
|
||||
"loading the fixture font");
|
||||
|
||||
registered = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL);
|
||||
TEST_ASSERT(errctx, registered != NULL,
|
||||
"akgl_text_loadfont did not place the font in AKGL_REGISTRY_FONT");
|
||||
TEST_ASSERT(errctx, TTF_GetFontHeight(registered) > 0,
|
||||
"the registered font reports height %d, expected a positive height",
|
||||
TTF_GetFontHeight(registered));
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_loadfont(NULL, TEST_FONT_PATH, TEST_FONT_SIZE),
|
||||
"loading a font under a NULL name");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_loadfont("nullpath", NULL, TEST_FONT_SIZE),
|
||||
"loading a font from a NULL path");
|
||||
TEST_EXPECT_STATUS(errctx, AKGL_ERR_SDL,
|
||||
akgl_text_loadfont("missing", "assets/no_such_font.ttf", TEST_FONT_SIZE),
|
||||
"loading a font that does not exist");
|
||||
|
||||
// Loading over a live name closes the font it displaces. Nothing here
|
||||
// can observe the close directly -- the proof is the memcheck run --
|
||||
// but the replacement itself is observable, and it is the path that
|
||||
// used to abandon ten kilobytes per call.
|
||||
TEST_EXPECT_OK(
|
||||
errctx,
|
||||
akgl_text_loadfont("testfont", TEST_FONT_PATH, TEST_FONT_SIZE),
|
||||
"loading a second font over a registered name");
|
||||
TEST_ASSERT(errctx,
|
||||
SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL) != registered,
|
||||
"reloading a font under a live name left the displaced font registered");
|
||||
|
||||
// And the font goes back. Unloading is what makes the suite's own
|
||||
// bookkeeping honest, so this is not only an API test.
|
||||
TEST_EXPECT_OK(errctx, akgl_text_unloadfont("testfont"), "unloading a registered font");
|
||||
TEST_ASSERT(errctx,
|
||||
SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "testfont", NULL) == NULL,
|
||||
"akgl_text_unloadfont left the font in AKGL_REGISTRY_FONT");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_KEY, akgl_text_unloadfont("testfont"),
|
||||
"unloading a font that is already gone");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_KEY, akgl_text_unloadfont("never_loaded"),
|
||||
"unloading a name that was never registered");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_unloadfont(NULL),
|
||||
"unloading a NULL name");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_text_measure(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int onecharw = 0;
|
||||
int onecharh = 0;
|
||||
int emptyw = 0;
|
||||
int emptyh = 0;
|
||||
|
||||
ATTEMPT {
|
||||
// One cell. This is the measurement a character grid is built from.
|
||||
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "A", &onecharw, &onecharh),
|
||||
"measuring a single character");
|
||||
TEST_ASSERT(errctx, onecharw > 0,
|
||||
"one character measured %d wide, expected a positive width", onecharw);
|
||||
TEST_ASSERT(errctx, onecharh == TTF_GetFontHeight(testfont),
|
||||
"one character measured %d high, expected the font height %d",
|
||||
onecharh, TTF_GetFontHeight(testfont));
|
||||
|
||||
// The font is monospaced, so four cells are exactly four times one.
|
||||
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "AAAA", &w, &h),
|
||||
"measuring four characters");
|
||||
TEST_ASSERT(errctx, w == (onecharw * 4),
|
||||
"four characters measured %d wide, expected %d", w, onecharw * 4);
|
||||
TEST_ASSERT(errctx, h == onecharh,
|
||||
"four characters on one line measured %d high, expected %d", h, onecharh);
|
||||
|
||||
// ...and every character advances by the same amount, which is what
|
||||
// makes a fixed grid legitimate in the first place.
|
||||
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "W", &w, &h), "measuring a wide glyph");
|
||||
TEST_ASSERT(errctx, w == onecharw,
|
||||
"'W' measured %d wide but 'A' measured %d in a monospaced font", w, onecharw);
|
||||
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "i", &w, &h), "measuring a narrow glyph");
|
||||
TEST_ASSERT(errctx, w == onecharw,
|
||||
"'i' measured %d wide but 'A' measured %d in a monospaced font", w, onecharw);
|
||||
|
||||
// The empty string is zero wide and still one line high, so a cursor
|
||||
// sitting on an empty line has somewhere to be.
|
||||
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "", &emptyw, &emptyh),
|
||||
"measuring the empty string");
|
||||
TEST_ASSERT(errctx, emptyw == 0,
|
||||
"the empty string measured %d wide, expected 0", emptyw);
|
||||
TEST_ASSERT(errctx, emptyh == onecharh,
|
||||
"the empty string measured %d high, expected the font height %d",
|
||||
emptyh, onecharh);
|
||||
|
||||
// Measuring does not disturb the destinations it was not asked about.
|
||||
w = -1;
|
||||
h = -1;
|
||||
TEST_EXPECT_OK(errctx, akgl_text_measure(testfont, "hello", &w, &h),
|
||||
"measuring a word");
|
||||
TEST_ASSERT(errctx, w == (onecharw * 5),
|
||||
"\"hello\" measured %d wide, expected %d", w, onecharw * 5);
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(NULL, "A", &w, &h),
|
||||
"measuring with a NULL font");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, NULL, &w, &h),
|
||||
"measuring a NULL string");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, "A", NULL, &h),
|
||||
"measuring into a NULL width");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER, akgl_text_measure(testfont, "A", &w, NULL),
|
||||
"measuring into a NULL height");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_text_measure_wrapped(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int onecharw = 0;
|
||||
int flatw = 0;
|
||||
int flath = 0;
|
||||
int lineskip = 0;
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_text_measure(testfont, "A", &onecharw, &flath));
|
||||
lineskip = TTF_GetFontLineSkip(testfont);
|
||||
TEST_ASSERT(errctx, lineskip > 0, "the font reports a line skip of %d", lineskip);
|
||||
|
||||
// A wrap length wide enough for the whole string measures the same as
|
||||
// the unwrapped call.
|
||||
CATCH(errctx, akgl_text_measure(testfont, "one two", &flatw, &flath));
|
||||
TEST_EXPECT_OK(errctx,
|
||||
akgl_text_measure_wrapped(testfont, "one two", flatw + onecharw, &w, &h),
|
||||
"measuring a string that fits inside the wrap length");
|
||||
TEST_ASSERT(errctx, w == flatw,
|
||||
"an unwrapped measurement gave %d wide, expected %d", w, flatw);
|
||||
TEST_ASSERT(errctx, h == flath,
|
||||
"an unwrapped measurement gave %d high, expected %d", h, flath);
|
||||
|
||||
// Narrow enough to force a break at the space: two lines, and nothing
|
||||
// wider than the wrap length.
|
||||
TEST_EXPECT_OK(errctx,
|
||||
akgl_text_measure_wrapped(testfont, "one two", onecharw * 4, &w, &h),
|
||||
"measuring a string that has to wrap");
|
||||
TEST_ASSERT(errctx, w <= (onecharw * 4),
|
||||
"a wrapped measurement gave %d wide, past the %d wrap length",
|
||||
w, onecharw * 4);
|
||||
TEST_ASSERT(errctx, h >= (lineskip * 2),
|
||||
"a wrapped measurement gave %d high, expected at least two lines (%d)",
|
||||
h, lineskip * 2);
|
||||
|
||||
// Zero wraps on newlines only, so an embedded newline still costs a line
|
||||
// and a long unbroken string does not.
|
||||
TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one\ntwo", 0, &w, &h),
|
||||
"measuring a string with an embedded newline");
|
||||
TEST_ASSERT(errctx, h >= (lineskip * 2),
|
||||
"an embedded newline measured %d high, expected at least two lines (%d)",
|
||||
h, lineskip * 2);
|
||||
TEST_ASSERT(errctx, w == (onecharw * 3),
|
||||
"the longer of two three-character lines measured %d wide, expected %d",
|
||||
w, onecharw * 3);
|
||||
|
||||
TEST_EXPECT_OK(errctx, akgl_text_measure_wrapped(testfont, "one two", 0, &w, &h),
|
||||
"measuring a string with no newline at wrap length zero");
|
||||
TEST_ASSERT(errctx, h == flath,
|
||||
"a string with no newline measured %d high at wrap length 0, expected %d",
|
||||
h, flath);
|
||||
|
||||
// A negative wrap length is refused rather than silently treated as
|
||||
// "never wrap", which is what SDL_ttf does with it.
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||
akgl_text_measure_wrapped(testfont, "one two", -1, &w, &h),
|
||||
"measuring at a negative wrap length");
|
||||
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_measure_wrapped(NULL, "A", 100, &w, &h),
|
||||
"measuring wrapped with a NULL font");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_measure_wrapped(testfont, NULL, 100, &w, &h),
|
||||
"measuring a NULL string wrapped");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_measure_wrapped(testfont, "A", 100, NULL, &h),
|
||||
"measuring wrapped into a NULL width");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_measure_wrapped(testfont, "A", 100, &w, NULL),
|
||||
"measuring wrapped into a NULL height");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_text_render_backend_guards(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_RenderBackend backend;
|
||||
SDL_Color white = { 0xff, 0xff, 0xff, 0xff };
|
||||
|
||||
ATTEMPT {
|
||||
// Nothing initialized the renderer at all, which is where the global
|
||||
// starts and where a host that has not called akgl_game_init leaves it.
|
||||
renderer = NULL;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
|
||||
"drawing text with no renderer backend");
|
||||
|
||||
// A backend that exists but was never given an SDL_Renderer -- the
|
||||
// state a host is in between allocating one and initializing it.
|
||||
memset(&backend, 0x00, sizeof(akgl_RenderBackend));
|
||||
renderer = &backend;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
|
||||
"drawing text through a backend with no SDL renderer");
|
||||
|
||||
// A live SDL_Renderer but no methods: a host that created its own
|
||||
// renderer and has not called akgl_render_bind2d() yet. This is the one
|
||||
// that used to be a segfault rather than an error -- with a real
|
||||
// renderer behind it the call gets all the way to a NULL function
|
||||
// pointer, which is why the check has to be here and not left to SDL.
|
||||
backend.sdl_renderer = testbackend.sdl_renderer;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(testfont, "hello", white, 0, 0, 0),
|
||||
"drawing text through a backend that was never bound");
|
||||
|
||||
renderer = &testbackend;
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(NULL, "hello", white, 0, 0, 0),
|
||||
"drawing text with a NULL font");
|
||||
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||
akgl_text_rendertextat(testfont, NULL, white, 0, 0, 0),
|
||||
"drawing a NULL string");
|
||||
} CLEANUP {
|
||||
renderer = &testbackend;
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_text_rendertextat(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Color white = { 0xff, 0xff, 0xff, 0xff };
|
||||
|
||||
ATTEMPT {
|
||||
// A bound backend over a software renderer is enough to draw through:
|
||||
// no display, no window shown, and the whole path from rasterizing to
|
||||
// blitting runs.
|
||||
renderer = &testbackend;
|
||||
TEST_EXPECT_OK(errctx, renderer->frame_start(renderer), "starting a frame");
|
||||
TEST_EXPECT_OK(errctx, akgl_text_rendertextat(testfont, "hello", white, 0, 4, 4),
|
||||
"drawing a line of text");
|
||||
// Wrapping takes the other rasterizing path.
|
||||
TEST_EXPECT_OK(errctx,
|
||||
akgl_text_rendertextat(testfont, "one two three", white, TEST_TARGET_SIZE / 2, 0, 0),
|
||||
"drawing wrapped text");
|
||||
// Not asserted here: the empty string, which SDL_ttf refuses with "Text
|
||||
// has zero width" on both paths, so drawing an empty line reports a
|
||||
// failure rather than drawing nothing. That is filed rather than pinned
|
||||
// -- see TODO.md, "Known and still open".
|
||||
TEST_EXPECT_OK(errctx, renderer->frame_end(renderer), "ending the frame");
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
memset(&testbackend, 0x00, sizeof(akgl_RenderBackend));
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_Init(SDL_INIT_VIDEO),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL: %s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
TTF_Init(),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't initialize the font engine: %s",
|
||||
SDL_GetError());
|
||||
|
||||
testfont = TTF_OpenFont(TEST_FONT_PATH, TEST_FONT_SIZE);
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
testfont,
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't open %s: %s",
|
||||
TEST_FONT_PATH,
|
||||
SDL_GetError());
|
||||
|
||||
// The host owns the window and libakgl binds to it, which is the
|
||||
// arrangement akgl_render_bind2d exists for and the one an embedded
|
||||
// interpreter drawing text is in.
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_CreateWindowAndRenderer(
|
||||
"net/aklabs/libakgl/test_text",
|
||||
TEST_TARGET_SIZE,
|
||||
TEST_TARGET_SIZE,
|
||||
0,
|
||||
&window,
|
||||
&testbackend.sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
CATCH(errctx, akgl_render_bind2d(&testbackend));
|
||||
|
||||
CATCH(errctx, test_text_loadfont());
|
||||
CATCH(errctx, test_text_measure());
|
||||
CATCH(errctx, test_text_measure_wrapped());
|
||||
CATCH(errctx, test_text_render_backend_guards());
|
||||
CATCH(errctx, test_text_rendertextat());
|
||||
} CLEANUP {
|
||||
if ( testfont != NULL ) {
|
||||
TTF_CloseFont(testfont);
|
||||
}
|
||||
TTF_Quit();
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ akerr_ErrorContext *test_tilemap_akgl_get_json_tilemap_property(void)
|
||||
json_error_t jsonerr;
|
||||
akgl_String *tmpstr = NULL;
|
||||
int propnum;
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
@@ -97,7 +97,7 @@ akerr_ErrorContext *test_akgl_tilemap_compute_tileset_offsets(void)
|
||||
&gamemap->tilesets[0].tile_offsets[3][1], // Tile 3 Y
|
||||
};
|
||||
int i = 0;
|
||||
|
||||
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
gamemap->tilesets[0].tilecount = 4;
|
||||
gamemap->tilesets[0].columns = 2;
|
||||
@@ -194,7 +194,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
|
||||
json_error_t errdata;
|
||||
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
@@ -212,7 +212,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_layer_tile(void)
|
||||
(gamemap->layers[0].data[2] != 3) ||
|
||||
(gamemap->layers[0].data[3] != 4) ) {
|
||||
FAIL_BREAK(errctx, AKERR_VALUE, "Test tilemap layer 0 tiles loaded with incorrect values (check gdb)");
|
||||
}
|
||||
}
|
||||
} CLEANUP {
|
||||
if ( pathstr != NULL ) {
|
||||
IGNORE(akgl_heap_release_string(pathstr));
|
||||
@@ -234,7 +234,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
|
||||
int i = 0;
|
||||
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
@@ -267,13 +267,13 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
|
||||
FAIL_BREAK(errctx, AKERR_VALUE, "Map layer 2 should have 1 loaded object (testactor) and nothing else (see gdb)");
|
||||
}
|
||||
// Layer 1 and 3 should have no objects
|
||||
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
||||
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
||||
if ( gamemap->layers[0].objects[i].id != 0 ) {
|
||||
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
|
||||
goto _test_akgl_tilemap_load_layers_cleanup;
|
||||
}
|
||||
}
|
||||
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
||||
for ( i = 0; i < AKGL_TILEMAP_MAX_OBJECTS_PER_LAYER ; i++ ) {
|
||||
if ( gamemap->layers[2].objects[i].id != 0 ) {
|
||||
FAIL(errctx, AKERR_VALUE, "Map layers 1 and 3 should have no objects loaded but found objects");
|
||||
goto _test_akgl_tilemap_load_layers_cleanup;
|
||||
@@ -291,7 +291,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_layers(void)
|
||||
) {
|
||||
FAIL_BREAK(errctx, AKERR_VALUE, "Map layers 1 and 3 should have tile data but it is incorrect");
|
||||
}
|
||||
_test_akgl_tilemap_load_layers_cleanup:
|
||||
_test_akgl_tilemap_load_layers_cleanup:
|
||||
} CLEANUP {
|
||||
if ( pathstr != NULL ) {
|
||||
IGNORE(akgl_heap_release_string(pathstr));
|
||||
@@ -313,7 +313,7 @@ akerr_ErrorContext *test_akgl_tilemap_load_tilesets(void)
|
||||
SDL_Texture *image = NULL;
|
||||
|
||||
memset((void *)gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
@@ -385,7 +385,7 @@ akerr_ErrorContext *test_akgl_tilemap_load(void)
|
||||
akerr_ErrorContext *test_akgl_tilemap_draw(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
@@ -399,7 +399,7 @@ akerr_ErrorContext *test_akgl_tilemap_draw(void)
|
||||
akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
@@ -412,16 +412,17 @@ akerr_ErrorContext *test_akgl_tilemap_draw_tileset(void)
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
gamemap = &_akgl_gamemap;
|
||||
renderer = &_akgl_renderer;
|
||||
SDL_SetAppMetadata("SDL3-GameTest", "0.1", "net.aklabs.sdl3-gametest");
|
||||
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO )) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't initialize SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer("net/aklabs/libakgl/test_sprite", 768, 576, 0, &window, &renderer->sdl_renderer)) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
}
|
||||
@@ -445,6 +446,5 @@ int main(void)
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
111
tests/util.c
111
tests/util.c
@@ -1,14 +1,42 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/util.h>
|
||||
|
||||
/**
|
||||
* @brief How many entries of AKERR_ARRAY_ERROR are currently held by somebody.
|
||||
*
|
||||
* The error contexts are a fixed pool exactly like the object pools: a context
|
||||
* is in use while its reference count is non-zero, and a function that finishes
|
||||
* without releasing one has leaked a slot out of AKERR_MAX_ARRAY_ERROR.
|
||||
*/
|
||||
static int live_error_contexts(void)
|
||||
{
|
||||
int live = 0;
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) {
|
||||
live += 1;
|
||||
}
|
||||
}
|
||||
return live;
|
||||
}
|
||||
|
||||
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
||||
{
|
||||
RectanglePoints points;
|
||||
SDL_FRect testrect;
|
||||
// Zeroed for the same reason as the fixtures in
|
||||
// test_akgl_collide_point_rectangle_nullpointers: the last case here is a
|
||||
// real call, and feeding it stack garbage is noise under `memcheck`.
|
||||
SDL_FRect testrect = {.x = 0, .y = 0, .w = 0, .h = 0};
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
memset((void *)&points, 0x00, sizeof(RectanglePoints));
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(NULL, NULL));
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
|
||||
@@ -26,7 +54,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||
// noop
|
||||
} FINISH(errctx, true);
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(&points, NULL));
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL RectanglePoints pointer");
|
||||
@@ -50,7 +78,7 @@ akerr_ErrorContext *test_akgl_rectangle_points_math(void)
|
||||
RectanglePoints points;
|
||||
SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32};
|
||||
memset((void *)&points, 0x00, sizeof(RectanglePoints));
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(&points, &testrect));
|
||||
@@ -80,12 +108,18 @@ akerr_ErrorContext *test_akgl_rectangle_points_math(void)
|
||||
|
||||
akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
|
||||
{
|
||||
point testpoint;
|
||||
// Zeroed rather than left as whatever the stack held. The last case in this
|
||||
// function is a real call with real arguments, and reading uninitialised
|
||||
// floats out of it is sixteen findings under `memcheck` for a test that is
|
||||
// not about coordinates at all.
|
||||
point testpoint = { .x = 0, .y = 0 };
|
||||
RectanglePoints testrectpoints;
|
||||
bool testcollide;
|
||||
|
||||
bool testcollide = false;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
memset(&testrectpoints, 0x00, sizeof(RectanglePoints));
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL));
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
|
||||
@@ -138,7 +172,7 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
|
||||
RectanglePoints testrectpoints;
|
||||
bool testcollide = false;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect));
|
||||
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
|
||||
@@ -160,10 +194,10 @@ akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
|
||||
|
||||
akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
||||
{
|
||||
SDL_FRect testrect1;
|
||||
SDL_FRect testrect2;
|
||||
bool testcollide;
|
||||
|
||||
SDL_FRect testrect1 = {.x = 0, .y = 0, .w = 0, .h = 0};
|
||||
SDL_FRect testrect2 = {.x = 0, .y = 0, .w = 0, .h = 0};
|
||||
bool testcollide = false;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
@@ -216,9 +250,9 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
SDL_FRect testrect1 = { .x = 0, .y = 0, .w = 32, .h = 32};
|
||||
SDL_FRect testrect2 = { .x = 30, .y = 30, .w = 40, .h = 40};
|
||||
bool testcollide = false;
|
||||
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
// Collision overlapping on the top left
|
||||
CATCH(errctx, akgl_collide_rectangles(&testrect1, &testrect2, &testcollide));
|
||||
@@ -298,7 +332,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
if ( testcollide == true ) {
|
||||
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
|
||||
}
|
||||
|
||||
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
@@ -306,15 +340,64 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Resolving a path through the root fallback must give its context back.
|
||||
*
|
||||
* akgl_path_relative tries the working directory first and falls back to
|
||||
* resolving against @p root when that reports ENOENT. That fallback used to be
|
||||
* taken by returning from inside the HANDLE block, which skips the
|
||||
* RELEASE_ERROR that FINISH ends with -- so every call down that branch leaked
|
||||
* one entry of AKERR_ARRAY_ERROR, and the 129th call aborted the whole process
|
||||
* with "Unable to pull an error context from the array!". A single map load
|
||||
* resolves several paths this way.
|
||||
*
|
||||
* The loop runs well past AKERR_MAX_ARRAY_ERROR on purpose: at the old
|
||||
* behaviour this test does not fail, it terminates the suite.
|
||||
*/
|
||||
akerr_ErrorContext *test_akgl_path_relative_releases_contexts(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_String *dst = NULL;
|
||||
int before = 0;
|
||||
int after = 0;
|
||||
int i = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_heap_next_string(&dst));
|
||||
before = live_error_contexts();
|
||||
|
||||
for ( i = 0; i < (AKERR_MAX_ARRAY_ERROR * 2); i++ ) {
|
||||
PASS(errctx, akgl_path_relative("assets", "testcharacter.json", dst));
|
||||
}
|
||||
after = live_error_contexts();
|
||||
|
||||
ATTEMPT {
|
||||
if ( after != before ) {
|
||||
FAIL_BREAK(
|
||||
errctx,
|
||||
AKGL_ERR_BEHAVIOR,
|
||||
"akgl_path_relative leaked %d error context(s) over %d root-fallback resolutions",
|
||||
(after - before),
|
||||
(AKERR_MAX_ARRAY_ERROR * 2));
|
||||
}
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_heap_release_string(dst));
|
||||
} PROCESS(errctx) {
|
||||
} FINISH(errctx, true);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, test_akgl_rectangle_points_nullpointers());
|
||||
CATCH(errctx, test_akgl_rectangle_points_math());
|
||||
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
|
||||
CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
|
||||
CATCH(errctx, test_akgl_collide_rectangles_logic());
|
||||
CATCH(errctx, test_akgl_path_relative_releases_contexts());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
108
tests/version.c
Normal file
108
tests/version.c
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @file version.c
|
||||
* @brief Unit tests for the version macros and the linked-library accessor.
|
||||
*
|
||||
* These assert that the several places the version appears cannot drift: the
|
||||
* string, the numeric components, and what the shared library reports at
|
||||
* runtime all come from one project() call, and this is what proves it.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/version.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/**
|
||||
* @brief The version string and the numeric components must describe one version.
|
||||
*
|
||||
* They are separate substitutions in version.h.in, so a mangled template can
|
||||
* leave them disagreeing and nothing else would notice.
|
||||
*/
|
||||
akerr_ErrorContext *test_version_string_matches_components(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
char assembled[64];
|
||||
|
||||
ATTEMPT {
|
||||
snprintf(assembled, sizeof(assembled), "%d.%d.%d",
|
||||
AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH);
|
||||
TEST_ASSERT(e, strcmp(assembled, AKGL_VERSION) == 0,
|
||||
"AKGL_VERSION is \"%s\" but the components assemble to \"%s\"",
|
||||
AKGL_VERSION, assembled);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The linked library must report the version its headers were generated from.
|
||||
*
|
||||
* In this build tree they are the same tree, so this can only fail if the test
|
||||
* picked up an installed libakgl off LD_LIBRARY_PATH instead of the one just
|
||||
* built -- which is precisely the mispairing the accessor exists to catch.
|
||||
*/
|
||||
akerr_ErrorContext *test_version_linked_matches_compiled(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
TEST_ASSERT(e, akgl_version() != NULL, "akgl_version returned NULL");
|
||||
TEST_ASSERT(e, strcmp(akgl_version(), AKGL_VERSION) == 0,
|
||||
"linked libakgl reports \"%s\" but the headers say \"%s\"",
|
||||
akgl_version(), AKGL_VERSION);
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief AKGL_VERSION_AT_LEAST must order versions correctly at the boundaries.
|
||||
*
|
||||
* Checked against the current version rather than fixed literals, so the test
|
||||
* does not have to be rewritten every time the version is bumped.
|
||||
*/
|
||||
akerr_ErrorContext *test_version_at_least_boundaries(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
ATTEMPT {
|
||||
TEST_ASSERT(e,
|
||||
AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH),
|
||||
"AKGL_VERSION_AT_LEAST rejected the current version");
|
||||
TEST_ASSERT(e,
|
||||
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, AKGL_VERSION_PATCH + 1),
|
||||
"AKGL_VERSION_AT_LEAST accepted a later patch");
|
||||
TEST_ASSERT(e,
|
||||
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR + 1, 0),
|
||||
"AKGL_VERSION_AT_LEAST accepted a later minor");
|
||||
TEST_ASSERT(e,
|
||||
!AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR + 1, 0, 0),
|
||||
"AKGL_VERSION_AT_LEAST accepted a later major");
|
||||
TEST_ASSERT(e,
|
||||
AKGL_VERSION_AT_LEAST(AKGL_VERSION_MAJOR, AKGL_VERSION_MINOR, 0),
|
||||
"AKGL_VERSION_AT_LEAST rejected an earlier patch");
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, test_version_string_matches_components());
|
||||
CATCH(errctx, test_version_linked_matches_compiled());
|
||||
CATCH(errctx, test_version_at_least_boundaries());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
char *characterjson = NULL;
|
||||
char pathbuf[4096];
|
||||
char cwdbuf[1024];
|
||||
|
||||
|
||||
if ( argc < 3 ) {
|
||||
SDL_Log("charviewer [CHARACTER_FILE] [SPRITE ...]");
|
||||
return SDL_APP_FAILURE;
|
||||
@@ -39,9 +39,9 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
characterjson = argv[1];
|
||||
memset((char *)&pathbuf, 0x00, 4096);
|
||||
memset((char *)&cwdbuf, 0x00, 1024);
|
||||
|
||||
|
||||
ATTEMPT {
|
||||
|
||||
|
||||
FAIL_ZERO_BREAK(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate pointer");
|
||||
FAIL_ZERO_BREAK(errctx, getcwd((char *)&cwdbuf, 1024), AKERR_NULLPOINTER, "Couldn't get current working directory");
|
||||
|
||||
@@ -79,7 +79,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
memset((char *)&pathbuf, 0x00, 4096);
|
||||
}
|
||||
|
||||
ATTEMPT {
|
||||
ATTEMPT {
|
||||
if ( characterjson[0] != '/' ) {
|
||||
sprintf((char *)&pathbuf, "%s/%s", (char *)&cwdbuf, characterjson);
|
||||
} else {
|
||||
@@ -105,7 +105,7 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
SDL_Log("Opening gamepad %d", gamepadids[0]);
|
||||
FAIL_ZERO_BREAK(errctx, SDL_OpenGamepad(gamepadids[0]), AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
CATCH(errctx, akgl_controller_default(0, "player", 0, gamepadids[0]));
|
||||
|
||||
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
@@ -123,7 +123,7 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
|
||||
ATTEMPT {
|
||||
FAIL_ZERO_BREAK(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate pointer");
|
||||
FAIL_ZERO_BREAK(errctx, event, AKERR_NULLPOINTER, "NULL event pointer");
|
||||
|
||||
|
||||
CATCH(errctx, akgl_controller_handle_event(appstate, event));
|
||||
if (event->type == SDL_EVENT_QUIT) {
|
||||
return SDL_APP_SUCCESS; /* end the program, reporting success to the OS. */
|
||||
@@ -140,7 +140,7 @@ SDL_AppResult SDL_AppIterate(void *appstate)
|
||||
akgl_Iterator opflags;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
|
||||
AKGL_BITMASK_CLEAR(opflags.flags);
|
||||
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE);
|
||||
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RENDER);
|
||||
|
||||
Reference in New Issue
Block a user