Wire the sink and the three devices to libakgl
src/sink_akgl.c, src/graphics_akgl.c, src/audio_akgl.c and src/input_akgl.c, in
the akbasic_akgl target, which is the only thing here that links SDL.
-DAKBASIC_WITH_AKGL=ON had never been configured in this repository before, and
it now builds and passes.
The sink is what section 3 has been waiting on. Its character grid comes from
akgl_text_measure(font, "A", &w, &h), the direct equivalent of the reference's
font.SizeUTF8("A") and the call that did not exist until 42b60f7. Wrapping is
done on the character grid rather than by handing SDL_ttf a wraplength, because
the cursor has to land somewhere definite: a program that PRINTs a long string
and then PRINTs again expects the second to start on the row after the first
ended, and only the code that placed the characters knows which row that is.
tests/akgl_backends.c draws into a 128x128 software renderer under the dummy
video driver and reads the pixels back -- the pattern deps/libakgl/tests/draw.c
established, which needs no display and no offscreen harness. It asserts the
seam rather than libakgl's own behaviour: a BASIC line in, a lit pixel of the
right colour out.
Four things in libakgl had to be worked around to get here. All four are
commented at their site with "filed upstream" and recorded in TODO.md section 3:
- An embedded libakgl requires SDL, SDL_image, SDL_mixer, SDL_ttf and jansson to
be *installed*. It builds its own vendored copies only when it is top-level,
and they are sitting right there in deps/libakgl/deps. Every lookup is guarded
with if(NOT TARGET ...), so this adds those five subdirectories before
add_subdirectory(deps/libakgl) -- the same trick and the same ordering
requirement akerror::akerror and akstdlib::akstdlib already need.
- akgl/controller.h does not compile on its own: it declares handlers taking an
akgl_Actor * and includes nothing that declares the type.
- There is no way to attach a 2D backend to a renderer you already have.
akgl_render_init2d() installs the vtable but also creates its own window and
writes the camera global, so it belongs to the akgl_game_init() path -- which
is exactly the path an embedding host is not on. The test assigns the six
pointers by hand.
- akgl_text_rendertextat() segfaults on a backend whose vtable is empty; it
reaches through renderer->draw_texture without checking it. Same class of
defect 42b60f7's own commit added a draw test for.
The sink's readline reports end of input rather than reading: a drawn text layer
is not a source of lines, and INPUT through one wants a line editor built on the
keystroke ring. EOF rather than an error is the contract sink.h states, so INPUT
already handles it. That editor is the next piece of work there.
70/70 core ctest with no SDL on the include path, 71/71 with the akgl suite,
clean under -Wall -Wextra, doxygen clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,26 @@ endfunction()
|
||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
|
||||
if(AKBASIC_WITH_AKGL)
|
||||
# libakgl builds its own vendored SDL, SDL_image, SDL_mixer, SDL_ttf and
|
||||
# jansson only when it is *top-level*. Embedded, it goes down a find_package
|
||||
# path instead and requires all five installed on the system -- which is a
|
||||
# surprise, because they are sitting right there in deps/libakgl/deps as
|
||||
# submodules. Every one of those lookups is guarded with
|
||||
# if(NOT TARGET ...), so declaring the targets here first satisfies them
|
||||
# and the vendored copies get used after all. Same trick, and the same
|
||||
# ordering requirement, that akerror::akerror and akstdlib::akstdlib already
|
||||
# need above.
|
||||
#
|
||||
# Filed upstream: an embedded libakgl should add its own vendored
|
||||
# dependencies rather than requiring them installed.
|
||||
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/libakgl/deps/jansson EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakgl/deps/SDL EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakgl/deps/SDL_image EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakgl/deps/SDL_mixer EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakgl/deps/SDL_ttf EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(deps/libakgl EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
@@ -142,7 +162,12 @@ akbasic_instrument(akbasic)
|
||||
# The libakgl-backed text sink, kept in its own target so the core library and
|
||||
# the whole golden suite build with no SDL present.
|
||||
if(AKBASIC_WITH_AKGL)
|
||||
add_library(akbasic_akgl src/sink_akgl.c)
|
||||
add_library(akbasic_akgl
|
||||
src/audio_akgl.c
|
||||
src/graphics_akgl.c
|
||||
src/input_akgl.c
|
||||
src/sink_akgl.c
|
||||
)
|
||||
target_compile_options(akbasic_akgl PRIVATE -Wall -Wextra)
|
||||
target_link_libraries(akbasic_akgl PUBLIC akbasic akgl)
|
||||
akbasic_instrument(akbasic_akgl)
|
||||
@@ -230,6 +255,30 @@ foreach(_test IN LISTS AKBASIC_TESTS AKBASIC_WILL_FAIL_TESTS AKBASIC_KNOWN_FAILI
|
||||
_add_test(NAME ${_test} COMMAND akbasic_test_${_test})
|
||||
endforeach()
|
||||
|
||||
# The akgl-backed suite. Its own list because it is the only test here that links
|
||||
# SDL, and because it can only exist when AKBASIC_WITH_AKGL is on -- the whole
|
||||
# point of the backend records is that everything else builds without it.
|
||||
#
|
||||
# It runs under the dummy video and audio drivers with a software renderer,
|
||||
# following deps/libakgl/tests/draw.c: a 128x128 target read back with
|
||||
# SDL_RenderReadPixels, so it needs no display and no offscreen harness.
|
||||
if(AKBASIC_WITH_AKGL)
|
||||
add_executable(akbasic_test_akgl_backends tests/akgl_backends.c)
|
||||
target_compile_options(akbasic_test_akgl_backends PRIVATE -Wall -Wextra)
|
||||
target_link_libraries(akbasic_test_akgl_backends PRIVATE akbasic_akgl)
|
||||
target_include_directories(akbasic_test_akgl_backends PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||
# libakgl's monospaced fixture font, borrowed rather than copied: a second copy
|
||||
# of a 10 KB binary is a second thing to keep in step for no benefit.
|
||||
target_compile_definitions(akbasic_test_akgl_backends PRIVATE
|
||||
AKBASIC_TEST_FONT="${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests/assets/akgl_test_mono.ttf")
|
||||
akbasic_instrument(akbasic_test_akgl_backends)
|
||||
_add_test(NAME akgl_backends COMMAND akbasic_test_akgl_backends)
|
||||
_set_tests_properties(akgl_backends PROPERTIES
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
|
||||
TIMEOUT 60
|
||||
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy")
|
||||
endif()
|
||||
|
||||
if(AKBASIC_TESTS OR AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
|
||||
_set_tests_properties(
|
||||
${AKBASIC_TESTS} ${AKBASIC_WILL_FAIL_TESTS} ${AKBASIC_KNOWN_FAILING_TESTS}
|
||||
|
||||
@@ -353,7 +353,11 @@ target_link_libraries(YOUR_GAME PRIVATE akbasic::akbasic)
|
||||
|
||||
`akbasic` links `akerror::akerror` and `akstdlib::akstdlib` publicly, so you inherit both. If your project already declares those targets, declare them *before* adding this one — the same rule that applies to `libakgl`.
|
||||
|
||||
`libakgl` itself is **not** a dependency of the core library. `-DAKBASIC_WITH_AKGL=ON` builds an additional `akbasic_akgl` target carrying the akgl-backed text sink; it is off by default, which is why the interpreter and its whole test suite build on a machine with no SDL.
|
||||
`libakgl` itself is **not** a dependency of the core library. `-DAKBASIC_WITH_AKGL=ON` builds an additional `akbasic_akgl` target carrying the akgl-backed text sink and the graphics, sound and input backends; it is off by default, which is why the interpreter and its whole test suite build on a machine with no SDL.
|
||||
|
||||
The graphics, sound and console verbs reach hardware through records of function pointers on the runtime — `akbasic_GraphicsBackend`, `akbasic_AudioBackend` and `akbasic_InputBackend`, attached with `akbasic_runtime_set_devices()`. All three may be `NULL`, which is what the standalone driver gives them: a verb that needs a device it was not given raises rather than crashing, and a `PRINT`-only program never notices. A host that renders some other way supplies its own records and never links `libakgl` at all.
|
||||
|
||||
Two things about those verbs that differ from a real C128, because both would otherwise surprise you. `PLAY` does not block — it queues its notes and `akbasic_runtime_step()` releases them against whatever time you last passed to `akbasic_runtime_settime()`, so the statement after a `PLAY` runs immediately. And `GETKEY` holds the program without blocking you: the step still returns, it simply does not advance until a key arrives.
|
||||
|
||||
## Limits
|
||||
|
||||
|
||||
115
TODO.md
115
TODO.md
@@ -369,44 +369,72 @@ such ceiling because it called `make()`.
|
||||
---
|
||||
|
||||
|
||||
## 3. Remaining work: the akgl text sink
|
||||
## 3. The akgl-backed sink and devices — **done**
|
||||
|
||||
**Unblocked as of `libakgl` 42b60f7** — the text-measurement call this waited on now exists.
|
||||
`-DAKBASIC_WITH_AKGL=ON` builds and its suite passes. Four adaptors in the `akbasic_akgl`
|
||||
target, which is the only thing here that links SDL:
|
||||
|
||||
**`src/sink_akgl.c`.** Implement the §1.5 vtable against `akgl_text_loadfont`,
|
||||
`akgl_text_rendertextat` and `akgl_text_measure`. Owns the cursor, the wrap, and the scroll —
|
||||
everything in `basicruntime_graphics.go` except `Write`/`Println`, which are now the sink
|
||||
interface itself.
|
||||
| File | Backs |
|
||||
|---|---|
|
||||
| `src/sink_akgl.c` | the §1.5 text sink, over `akgl_text_measure` and `akgl_text_rendertextat` |
|
||||
| `src/graphics_akgl.c` | `akbasic_GraphicsBackend`, over `akgl_draw_*`, and the SSHAPE surface pool |
|
||||
| `src/audio_akgl.c` | `akbasic_AudioBackend`, over `akgl_audio_*` |
|
||||
| `src/input_akgl.c` | `akbasic_InputBackend`, over `akgl_controller_poll_key` |
|
||||
|
||||
The character grid comes from `akgl_text_measure(font, "A", &w, &h)`, which is the direct
|
||||
equivalent of the `font.SizeUTF8("A")` at `basicruntime.go:96` that the reference derives
|
||||
`maxCharsW`/`maxCharsH` from. For wrapping, `akgl_text_measure_wrapped` takes the same
|
||||
`wraplength` argument `akgl_text_rendertextat` already does, so the measurement and the draw
|
||||
cannot disagree about where a line breaks.
|
||||
The character grid comes from `akgl_text_measure(font, "A", &w, &h)` — the direct equivalent of
|
||||
the `font.SizeUTF8("A")` at `basicruntime.go:96`, and the call this was blocked on until
|
||||
42b60f7. Wrapping is done on the character grid rather than by handing SDL_ttf a `wraplength`,
|
||||
because the cursor has to land somewhere definite: a program that `PRINT`s a long string and
|
||||
then `PRINT`s again expects the second to start on the row after the first ended, and only the
|
||||
code that placed the characters knows which row that is.
|
||||
|
||||
**The interpreter does not own the window, the renderer, or the game loop.** The sink draws
|
||||
through whatever renderer the host already initialized. `akbasic_sink_init_akgl()` takes the
|
||||
renderer; it does not create one.
|
||||
**The interpreter owns no window, renderer or event loop.** Every initializer takes something
|
||||
the host already made. Each calls `akgl_error_init()` first: `akgl_game_init()` would have,
|
||||
but we drive subsystems directly and never call it, and a code raised before that registration
|
||||
carries no name into its stack trace. It is idempotent.
|
||||
|
||||
**Call `akgl_error_init()` before anything else in `libakgl`.** New in `libakgl` 0.1.0
|
||||
(`deps/libakgl/src/error.c`): it reserves the 256–260 status band and registers a name for
|
||||
each `AKGL_ERR_*` code. `akgl_game_init()` calls it first, but we drive subsystems directly and
|
||||
never call `akgl_game_init()`, so it is ours to call. Skip it and every `AKGL_ERR_*` that
|
||||
reaches a stack trace prints "Unknown Error" — which is exactly the defect the upstream commit
|
||||
found in `game.c`, where an SDL failure five lines ahead of the old registration site was
|
||||
guaranteed to be unnamed. It is idempotent, so calling it from both `akbasic_sink_init_akgl()`
|
||||
and a host that already did is harmless.
|
||||
`PASS` its result; a range collision is an initialization failure, not a warning.
|
||||
*Acceptance:* `tests/akgl_backends.c`, a 128x128 software renderer under the dummy video
|
||||
driver read back with `SDL_RenderReadPixels` — the pattern `deps/libakgl/tests/draw.c`
|
||||
established, which needs no display and no offscreen harness. Registered as the `akgl_backends`
|
||||
CTest case, and only when `AKBASIC_WITH_AKGL` is on.
|
||||
|
||||
**Still to check before starting:** `-DAKBASIC_WITH_AKGL=ON` has never been configured in this
|
||||
repository, because until now there was nothing to build against. It needs `libakgl`'s own
|
||||
submodules present (`git submodule update --init --recursive` pulls SDL and friends), and the
|
||||
`akbasic_akgl` target in `CMakeLists.txt` has therefore never been compiled. Expect to fix
|
||||
something there on the first attempt.
|
||||
### Four things that had to be worked around to get there
|
||||
|
||||
*Acceptance:* `tests/sink_akgl.c` against the offscreen renderer harness described in
|
||||
`deps/libakgl/TODO.md` under "Remaining work". If that harness still does not exist, a
|
||||
known-failing test plus a `libakgl` TODO entry saying so.
|
||||
All four are `libakgl`'s and all four are filed in `deps/libakgl/TODO.md`. Each workaround is
|
||||
commented at its site with the words "filed upstream" so it can be found and deleted later.
|
||||
|
||||
1. **An embedded `libakgl` demands its dependencies be *installed*.** It builds its vendored
|
||||
SDL, SDL_image, SDL_mixer, SDL_ttf and jansson only when it is top-level; embedded, it goes
|
||||
down a `find_package` path and fails on a machine that has none of them — while the
|
||||
submodules sit right there in `deps/libakgl/deps`. Every lookup is guarded with
|
||||
`if(NOT TARGET ...)`, so our `CMakeLists.txt` adds those five subdirectories *before*
|
||||
`add_subdirectory(deps/libakgl)` and the vendored copies get used after all. Same trick and
|
||||
same ordering requirement `akerror::akerror` and `akstdlib::akstdlib` already need.
|
||||
|
||||
2. **`akgl/controller.h` does not compile on its own.** It declares two handler function
|
||||
pointers taking an `akgl_Actor *` and includes nothing that declares the type.
|
||||
`src/input_akgl.c` includes `akgl/actor.h` ahead of it.
|
||||
|
||||
3. **There is no way to attach a 2D backend to a renderer you already have.**
|
||||
`akgl_render_init2d()` installs the six vtable pointers, but it also creates its own window
|
||||
from the game properties and writes to the `camera` global, so it belongs to the
|
||||
`akgl_game_init()` path — which is exactly the path an embedding host is not on.
|
||||
`tests/akgl_backends.c` assigns the six pointers by hand. What is wanted upstream is the
|
||||
vtable half of `init2d` on its own.
|
||||
|
||||
4. **`akgl_text_rendertextat()` segfaults on a backend whose vtable is empty.** It reaches
|
||||
through `renderer->draw_texture` without checking it, so the first `PRINT` through the sink
|
||||
dereferences NULL. That is the same class of defect 42b60f7's own commit added a draw test
|
||||
for — "a backend that exists but was never given an `SDL_Renderer`" — and the text path
|
||||
still has it.
|
||||
|
||||
### Still missing from the sink
|
||||
|
||||
`readline` reports end of input rather than reading anything: a drawn text layer is not a
|
||||
source of lines, and `INPUT` through a graphics sink wants a line editor built on the keystroke
|
||||
ring. EOF rather than an error is the contract `sink.h` states, so `INPUT` handles it already.
|
||||
That editor is the next piece of work here, and it is what `WINDOW` and `KEY` in group E want
|
||||
as well.
|
||||
|
||||
---
|
||||
|
||||
@@ -781,7 +809,7 @@ Dependency baseline:
|
||||
|---|---|---|
|
||||
| `deps/libakerror` | 1.0.0 | Private ownership-enforced status registry. akbasic reserves 512–767 in `akbasic_error_register()`. Does not namespace its `coverage` target when embedded — worked around in our `CMakeLists.txt`. |
|
||||
| `deps/libakstdlib` | 0.1.0 | soname `libakstdlib.so.0.1`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. Its `aksl_ato*` family is banned here — see §1.9. |
|
||||
| `deps/libakgl` | 0.1.0, **pinned at commit 42b60f7** | Migrated to the 1.0.0 registry and owns 256–260. Not yet linked: `AKBASIC_WITH_AKGL` defaults OFF and `src/sink_akgl.c` does not exist. The version number cannot carry this requirement — see below. |
|
||||
| `deps/libakgl` | 0.1.0, **pinned at commit 42b60f7** | Migrated to the 1.0.0 registry and owns 256–260. Linked and tested under `-DAKBASIC_WITH_AKGL=ON`, which still defaults OFF so the core library and its whole suite build on a machine with no SDL. The version number cannot carry this requirement — see below. |
|
||||
|
||||
**The `libakgl` requirement is pinned by commit, not by version, and that is a defect
|
||||
upstream.** 42b60f7 added 22 public symbols across four headers — `akgl_draw_*` (8),
|
||||
@@ -823,16 +851,21 @@ keeping:
|
||||
|
||||
What remains, in priority order:
|
||||
|
||||
1. **§3 — the akgl text sink.** No longer blocked: `libakgl` 42b60f7 landed
|
||||
`akgl_text_measure`, so this is now ordinary work. Note that `-DAKBASIC_WITH_AKGL=ON` has
|
||||
never been configured here, so the `akbasic_akgl` target is unproven.
|
||||
2. **§4 — the language completion work queue.** Nothing in it is blocked on `libakgl` any more:
|
||||
groups G, I and part of E were, and `libakgl` 42b60f7 closed all three. Multiple statements
|
||||
per line (the `COLON` token exists and nothing consumes it) should still come first, because
|
||||
`DO`/`LOOP` reads badly without it.
|
||||
3. **§6 items 12–17** — the defects the port uncovered. Item 12 is the one that produces a
|
||||
1. **§4 — the language completion work queue.** Groups G and I and the `GET`/`GETKEY`/`SCNCLR`
|
||||
part of E are done. Of what is left, **multiple statements per line** should come first:
|
||||
the `COLON` token exists and nothing consumes it, and `DO`/`LOOP` in group A reads badly
|
||||
without it. Then groups A, B, D, F and J, none of which need anything from `libakgl`.
|
||||
2. **A line editor for the akgl sink.** `readline` reports EOF, so `INPUT` does not work
|
||||
through a drawn text layer. It wants the keystroke ring plus a cursor, and it is also what
|
||||
`WINDOW` and `KEY` in group E need. See §3.
|
||||
3. **CI does not cover `-DAKBASIC_WITH_AKGL=ON`.** Doing so needs `submodules: recursive`,
|
||||
which pulls SDL, SDL_image, SDL_mixer, SDL_ttf and jansson — a long build on a job the other
|
||||
four do not need. Deliberately deferred, not forgotten: the akgl target is built and tested
|
||||
locally with the two commands in §3, and it will rot silently until somebody decides that
|
||||
cost is worth paying.
|
||||
4. **§6 items 12–17** — the defects the port uncovered. Item 12 is the one that produces a
|
||||
*wrong answer* rather than a refused one, and should be fixed first.
|
||||
4. **Mutation survivors in `src/value.c`.** The harness is in place (`scripts/mutation_test.py`,
|
||||
5. **Mutation survivors in `src/value.c`.** The harness is in place (`scripts/mutation_test.py`,
|
||||
the `mutation` CMake target, and a CI job on `src/convert.c`), and a partial run over
|
||||
`src/value.c` turned up test gaps worth closing. These are *not* equivalent mutants; each
|
||||
is a real bug of that shape the suite would not notice:
|
||||
|
||||
167
include/akbasic/akgl.h
Normal file
167
include/akbasic/akgl.h
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @file akgl.h
|
||||
* @brief The libakgl-backed implementations of the sink and the three devices.
|
||||
*
|
||||
* Everything declared here lives in the separate `akbasic_akgl` target, which is
|
||||
* the only part of this project that links SDL. The core library builds and its
|
||||
* whole test suite runs on a machine with no SDL on it; that is why the records
|
||||
* these initializers populate are plain function-pointer structs and why this
|
||||
* header is the only one that includes a libakgl header.
|
||||
*
|
||||
* **The interpreter owns no window, no renderer and no event loop.** Every one
|
||||
* of these takes something the host already created and draws or plays through
|
||||
* it. None of them creates a device, and none of them pumps events.
|
||||
*
|
||||
* Each initializer calls akgl_error_init() first. It reserves libakgl's 256-260
|
||||
* status band and names every AKGL_ERR_* code; akgl_game_init() calls it as its
|
||||
* first statement, but a program driving subsystems directly -- which is exactly
|
||||
* what an embedded interpreter does -- never goes through akgl_game_init() and
|
||||
* has to call it itself. Skip it and every AKGL_ERR_* that reaches a stack trace
|
||||
* prints "Unknown Error". It is idempotent, so a host that already called it
|
||||
* loses nothing.
|
||||
*/
|
||||
|
||||
#ifndef _AKBASIC_AKGL_H_
|
||||
#define _AKBASIC_AKGL_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/renderer.h>
|
||||
|
||||
#include <akbasic/audio.h>
|
||||
#include <akbasic/graphics.h>
|
||||
#include <akbasic/input.h>
|
||||
#include <akbasic/sink.h>
|
||||
|
||||
/** @brief How many saved SSHAPE regions the graphics backend will hold at once. */
|
||||
#define AKBASIC_AKGL_MAX_SHAPES 16
|
||||
|
||||
/**
|
||||
* @brief State for the libakgl-backed text sink.
|
||||
*
|
||||
* The cursor, the wrap and the scroll live here rather than in the interpreter:
|
||||
* everything in the reference's basicruntime_graphics.go except Write and
|
||||
* Println, which are the sink interface itself.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
akgl_RenderBackend *renderer;
|
||||
TTF_Font *font;
|
||||
SDL_Color color;
|
||||
|
||||
int x; /* pixel origin of the text area */
|
||||
int y;
|
||||
int width; /* pixel size of the text area */
|
||||
int height;
|
||||
int cellw; /* one character cell, measured from the font */
|
||||
int cellh;
|
||||
int columns; /* the character grid the cell size works out to */
|
||||
int rows;
|
||||
|
||||
int cursorcol;
|
||||
int cursorrow;
|
||||
|
||||
/*
|
||||
* The scrollback the sink redraws every frame. A fixed grid rather than a
|
||||
* list of lines: the interpreter allocates nothing, and neither does this.
|
||||
*/
|
||||
char text[64][256];
|
||||
} akbasic_AkglSink;
|
||||
|
||||
/**
|
||||
* @brief State for the libakgl-backed graphics backend.
|
||||
*
|
||||
* The shape pool is why this exists at all. SSHAPE hands the BASIC program a
|
||||
* handle rather than the pixels -- see TODO.md section 5 -- and these are the
|
||||
* surfaces those handles refer to.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
akgl_RenderBackend *renderer;
|
||||
SDL_Surface *shapes[AKBASIC_AKGL_MAX_SHAPES];
|
||||
int shapecount;
|
||||
} akbasic_AkglGraphics;
|
||||
|
||||
/**
|
||||
* @brief Point a text sink at a renderer and a font the host already has.
|
||||
*
|
||||
* The character grid is derived by measuring one glyph with akgl_text_measure(),
|
||||
* which is the direct equivalent of the reference's font.SizeUTF8("A") -- and
|
||||
* which did not exist in libakgl until 42b60f7. A font that is not monospaced
|
||||
* still works; the grid is then sized by whatever "A" happens to measure, and
|
||||
* proportional glyphs simply do not line up in columns.
|
||||
*
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param state Storage for the sink's own state; must outlive the sink.
|
||||
* @param renderer The renderer the host already initialized; not created here.
|
||||
* @param font An open font; not opened or closed here.
|
||||
* @param w Width in pixels of the text area.
|
||||
* @param h Height in pixels of the text area.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When any pointer argument is NULL.
|
||||
* @throws AKBASIC_ERR_VALUE When the font measures a zero-width cell, which would divide by zero.
|
||||
* @throws AKBASIC_ERR_BOUNDS When the area is too small for even one character.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h);
|
||||
|
||||
/**
|
||||
* @brief Draw whatever the sink currently holds.
|
||||
*
|
||||
* Separate from the sink's write path because the interpreter does not own the
|
||||
* frame: a host calls this when it is drawing, not when the script happens to
|
||||
* PRINT. A driver that only wants stdout never calls it at all.
|
||||
*
|
||||
* @param obj The sink to draw.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||
* @throws AKGL_ERR_SDL When the renderer refuses the text.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_akgl_render(akbasic_TextSink *obj);
|
||||
|
||||
/**
|
||||
* @brief Point a graphics backend at a renderer the host already has.
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @param state Storage for the shape pool; must outlive the backend.
|
||||
* @param renderer The renderer the host already initialized; not created here.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When any argument is NULL.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer);
|
||||
|
||||
/**
|
||||
* @brief Wire an audio backend to libakgl's tone generator.
|
||||
*
|
||||
* Calls akgl_audio_init(), which opens an SDL audio device. A host that owns its
|
||||
* own audio pipeline can call akgl_audio_init() itself beforehand -- it is
|
||||
* idempotent in the sense that mattered upstream: the voice table works whether
|
||||
* or not a device is open.
|
||||
*
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||
* @throws AKGL_ERR_SDL When no audio device can be opened.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_audio_init_akgl(akbasic_AudioBackend *obj);
|
||||
|
||||
/**
|
||||
* @brief Wire an input backend to libakgl's keystroke ring.
|
||||
*
|
||||
* Reads only. The ring is filled by akgl_controller_handle_event(), which the
|
||||
* *host* calls as it pumps SDL events -- so a script gets keystrokes without the
|
||||
* interpreter owning the event loop.
|
||||
*
|
||||
* Worth knowing before lending this to a script: that ring is process-global and
|
||||
* the host's own control maps read the same events. A script sitting in a GET
|
||||
* loop drains keystrokes the game will then never see. A host that cares either
|
||||
* withholds the input backend or supplies a filtered one of its own.
|
||||
*
|
||||
* @param obj Object to initialize, inspect, or modify.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_input_init_akgl(akbasic_InputBackend *obj);
|
||||
|
||||
#endif // _AKBASIC_AKGL_H_
|
||||
137
src/audio_akgl.c
Normal file
137
src/audio_akgl.c
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @file audio_akgl.c
|
||||
* @brief Wires the audio backend record to libakgl's tone generator.
|
||||
*
|
||||
* Another thin adaptor. The one thing in here worth reading is the waveform
|
||||
* mapping: akbasic_Waveform and akgl_AudioWaveform currently agree on every
|
||||
* value, and the mapping is still written out rather than cast across, because
|
||||
* two enumerations agreeing by accident is not a contract and a future SOUND
|
||||
* waveform number would break the coincidence silently.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/audio.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include <akbasic/akgl.h>
|
||||
#include <akbasic/error.h>
|
||||
|
||||
/**
|
||||
* @brief Map a BASIC waveform onto libakgl's.
|
||||
*
|
||||
* @param waveform The BASIC-side selection.
|
||||
* @param dest Output destination populated by the function.
|
||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||
* @throws AKBASIC_ERR_BOUNDS When the waveform is not one of the four SOUND defines.
|
||||
*/
|
||||
static akerr_ErrorContext *to_akgl_waveform(akbasic_Waveform waveform, akgl_AudioWaveform *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
switch ( waveform ) {
|
||||
case AKBASIC_WAVE_TRIANGLE:
|
||||
*dest = AKGL_AUDIO_WAVE_TRIANGLE;
|
||||
break;
|
||||
case AKBASIC_WAVE_SAWTOOTH:
|
||||
*dest = AKGL_AUDIO_WAVE_SAWTOOTH;
|
||||
break;
|
||||
case AKBASIC_WAVE_SQUARE:
|
||||
*dest = AKGL_AUDIO_WAVE_SQUARE;
|
||||
break;
|
||||
case AKBASIC_WAVE_NOISE:
|
||||
*dest = AKGL_AUDIO_WAVE_NOISE;
|
||||
break;
|
||||
default:
|
||||
FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS, "No such waveform %d", (int)waveform);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_tone(akbasic_AudioBackend *self, int voice, double hz, int ms)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
FAIL_ZERO_RETURN(errctx, (ms >= 0), AKERR_VALUE, "Negative note duration %d", ms);
|
||||
PASS(errctx, akgl_audio_tone(voice, (float32_t)hz, (uint32_t)ms));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_stop(akbasic_AudioBackend *self, int voice)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, akgl_audio_stop(voice));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_waveform(akbasic_AudioBackend *self, int voice, akbasic_Waveform waveform)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_AudioWaveform mapped = AKGL_AUDIO_WAVE_SQUARE;
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, to_akgl_waveform(waveform, &mapped));
|
||||
PASS(errctx, akgl_audio_waveform(voice, mapped));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_envelope(akbasic_AudioBackend *self, int voice, int attack, int decay, double sustain, int release)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
FAIL_ZERO_RETURN(errctx, (attack >= 0 && decay >= 0 && release >= 0), AKERR_VALUE,
|
||||
"Negative envelope stage: %d/%d/%d", attack, decay, release);
|
||||
PASS(errctx, akgl_audio_envelope(voice, (uint32_t)attack, (uint32_t)decay,
|
||||
(float32_t)sustain, (uint32_t)release));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_volume(akbasic_AudioBackend *self, double level)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, akgl_audio_volume((float32_t)level));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *snd_voice_active(akbasic_AudioBackend *self, int voice, bool *active)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, akgl_audio_voice_active(voice, active));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_audio_init_akgl(akbasic_AudioBackend *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
||||
"NULL backend in audio_init_akgl");
|
||||
PASS(errctx, akgl_error_init());
|
||||
|
||||
/*
|
||||
* Opens an SDL audio device. A host that owns its own audio pipeline can
|
||||
* skip this entirely and pull samples with akgl_audio_mix() instead -- the
|
||||
* voice table works either way, which is the design upstream chose and the
|
||||
* reason its own suite can be deterministic.
|
||||
*/
|
||||
PASS(errctx, akgl_audio_init());
|
||||
|
||||
obj->self = NULL; /* the voice table is libakgl's, not ours */
|
||||
obj->tone = snd_tone;
|
||||
obj->stop = snd_stop;
|
||||
obj->waveform = snd_waveform;
|
||||
obj->envelope = snd_envelope;
|
||||
obj->volume = snd_volume;
|
||||
obj->voice_active = snd_voice_active;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
244
src/graphics_akgl.c
Normal file
244
src/graphics_akgl.c
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* @file graphics_akgl.c
|
||||
* @brief Wires the graphics backend record to libakgl's immediate-mode drawing.
|
||||
*
|
||||
* A thin adaptor and deliberately so: the interesting decisions about what a
|
||||
* BASIC graphics verb means live in src/runtime_graphics.c, and everything here
|
||||
* is coordinate and colour conversion plus the shape pool that SSHAPE hands out
|
||||
* handles into.
|
||||
*
|
||||
* The akgl_draw_* family is new in libakgl 42b60f7. Every one of its entry
|
||||
* points takes the akgl_RenderBackend the host already initialized rather than
|
||||
* reaching for a global, which is exactly what goal 3 needs.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include <akbasic/akgl.h>
|
||||
#include <akbasic/error.h>
|
||||
|
||||
/** @brief The colour conversion, which is the whole impedance mismatch. */
|
||||
static SDL_Color to_sdl(akbasic_Color color)
|
||||
{
|
||||
SDL_Color out;
|
||||
|
||||
out.r = color.r;
|
||||
out.g = color.g;
|
||||
out.b = color.b;
|
||||
out.a = color.a;
|
||||
return out;
|
||||
}
|
||||
|
||||
/** @brief Recover the backend's own state, or say that it has none. */
|
||||
static akerr_ErrorContext *state_of(akbasic_GraphicsBackend *self, akbasic_AkglGraphics **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in akgl graphics backend");
|
||||
*dest = (akbasic_AkglGraphics *)self->self;
|
||||
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
|
||||
"akgl graphics backend has no state");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_point(akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
PASS(errctx, akgl_draw_point(state->renderer, (float32_t)x, (float32_t)y, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_line(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
PASS(errctx, akgl_draw_line(state->renderer, (float32_t)x1, (float32_t)y1,
|
||||
(float32_t)x2, (float32_t)y2, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Turn two opposite corners into the rect SDL wants.
|
||||
*
|
||||
* BASIC gives two corners in whatever order the program felt like; SDL_FRect is
|
||||
* an origin and a size and does nothing useful with a negative one.
|
||||
*/
|
||||
static SDL_FRect corners_to_rect(double x1, double y1, double x2, double y2)
|
||||
{
|
||||
SDL_FRect rect;
|
||||
|
||||
rect.x = (float32_t)((x1 < x2) ? x1 : x2);
|
||||
rect.y = (float32_t)((y1 < y2) ? y1 : y2);
|
||||
rect.w = (float32_t)((x1 < x2) ? (x2 - x1) : (x1 - x2));
|
||||
rect.h = (float32_t)((y1 < y2) ? (y2 - y1) : (y1 - y2));
|
||||
return rect;
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
SDL_FRect rect;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
rect = corners_to_rect(x1, y1, x2, y2);
|
||||
PASS(errctx, akgl_draw_rect(state->renderer, &rect, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_filled_rect(akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
SDL_FRect rect;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
rect = corners_to_rect(x1, y1, x2, y2);
|
||||
PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_paint(akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
/*
|
||||
* AKERR_OUTOFBOUNDS out of here means the fill exhausted its span stack and
|
||||
* left the region partly filled. It is passed straight through: PAINT is
|
||||
* where that is turned into something a BASIC program can see, because PAINT
|
||||
* is the only thing that knows a program is watching.
|
||||
*/
|
||||
PASS(errctx, akgl_draw_flood_fill(state->renderer, x, y, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_clear(akbasic_GraphicsBackend *self, akbasic_Color color)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
SDL_FRect rect;
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx,
|
||||
SDL_GetCurrentRenderOutputSize(state->renderer->sdl_renderer, &w, &h),
|
||||
AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
/*
|
||||
* A filled rectangle over the output rather than SDL_RenderClear, because
|
||||
* clearing is the host's prerogative: a game that draws a background and
|
||||
* then hands the renderer to a script does not expect GRAPHIC to wipe it to
|
||||
* a colour of the script's choosing outside the area the script owns. This
|
||||
* covers exactly the render output and nothing beyond it.
|
||||
*/
|
||||
rect.x = 0.0f;
|
||||
rect.y = 0.0f;
|
||||
rect.w = (float32_t)w;
|
||||
rect.h = (float32_t)h;
|
||||
PASS(errctx, akgl_draw_filled_rect(state->renderer, &rect, to_sdl(color)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_save_shape(akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
SDL_Rect region;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx, (handle != NULL), AKERR_NULLPOINTER,
|
||||
"NULL handle in save_shape");
|
||||
FAIL_ZERO_RETURN(errctx, (state->shapecount < AKBASIC_AKGL_MAX_SHAPES),
|
||||
AKBASIC_ERR_DEVICE,
|
||||
"SSHAPE pool is full at %d saved regions; GRAPHIC CLR releases them",
|
||||
AKBASIC_AKGL_MAX_SHAPES);
|
||||
|
||||
region.x = (x1 < x2) ? x1 : x2;
|
||||
region.y = (y1 < y2) ? y1 : y2;
|
||||
region.w = (x1 < x2) ? (x2 - x1) : (x1 - x2);
|
||||
region.h = (y1 < y2) ? (y2 - y1) : (y1 - y2);
|
||||
|
||||
/*
|
||||
* NULL through dest asks akgl_draw_copy_region to allocate the surface. That
|
||||
* is the one allocation in this file and it belongs to the pool below, which
|
||||
* is bounded -- so it is a fixed ceiling on memory, not an open one.
|
||||
*/
|
||||
state->shapes[state->shapecount] = NULL;
|
||||
PASS(errctx, akgl_draw_copy_region(state->renderer, ®ion,
|
||||
&state->shapes[state->shapecount]));
|
||||
*handle = state->shapecount;
|
||||
state->shapecount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_paste_shape(akbasic_GraphicsBackend *self, int handle, double x, double y)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
FAIL_ZERO_RETURN(errctx, (handle >= 0 && handle < state->shapecount),
|
||||
AKBASIC_ERR_BOUNDS, "No saved shape %d", handle);
|
||||
FAIL_ZERO_RETURN(errctx, (state->shapes[handle] != NULL), AKBASIC_ERR_STATE,
|
||||
"Saved shape %d has been released", handle);
|
||||
PASS(errctx, akgl_draw_paste_region(state->renderer, state->shapes[handle],
|
||||
(float32_t)x, (float32_t)y));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *gfx_free_shapes(akbasic_GraphicsBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglGraphics *state = NULL;
|
||||
int i = 0;
|
||||
|
||||
PASS(errctx, state_of(self, &state));
|
||||
for ( i = 0; i < state->shapecount; i++ ) {
|
||||
if ( state->shapes[i] != NULL ) {
|
||||
SDL_DestroySurface(state->shapes[i]);
|
||||
state->shapes[i] = NULL;
|
||||
}
|
||||
}
|
||||
state->shapecount = 0;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akbasic_AkglGraphics *state, akgl_RenderBackend *renderer)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in graphics_init_akgl");
|
||||
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
|
||||
"NULL renderer in graphics_init_akgl: the host creates it, not this");
|
||||
PASS(errctx, akgl_error_init());
|
||||
|
||||
memset(state, 0, sizeof(*state));
|
||||
state->renderer = renderer;
|
||||
|
||||
obj->self = state;
|
||||
obj->point = gfx_point;
|
||||
obj->line = gfx_line;
|
||||
obj->rect = gfx_rect;
|
||||
obj->filled_rect = gfx_filled_rect;
|
||||
obj->paint = gfx_paint;
|
||||
obj->clear = gfx_clear;
|
||||
obj->save_shape = gfx_save_shape;
|
||||
obj->paste_shape = gfx_paste_shape;
|
||||
obj->free_shapes = gfx_free_shapes;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
69
src/input_akgl.c
Normal file
69
src/input_akgl.c
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @file input_akgl.c
|
||||
* @brief Wires the input backend record to libakgl's keystroke ring.
|
||||
*
|
||||
* The thinnest of the four adaptors, and the one whose *shape* carries the
|
||||
* design: akgl_controller_poll_key() reads a ring that
|
||||
* akgl_controller_handle_event() fills as the **host** pumps SDL events. The
|
||||
* interpreter therefore answers "is there a key waiting" without owning an event
|
||||
* loop, which is the whole of goal 3 for input.
|
||||
*
|
||||
* The caveat a host needs, repeated here because this is the file that causes
|
||||
* it: that ring is process-global and the host's own control maps read the same
|
||||
* events. A script sitting in a GET loop drains keystrokes the game will then
|
||||
* never see. Withhold the backend, or supply a filtered one.
|
||||
*/
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
/*
|
||||
* akgl/actor.h before akgl/controller.h, and it is not optional: controller.h
|
||||
* declares two handler function pointers taking an akgl_Actor * and includes
|
||||
* nothing that declares the type, so on its own it does not compile. Not a
|
||||
* workaround anybody should copy -- filed upstream against libakgl, whose own
|
||||
* style rules call for self-contained headers. Delete this note and the include
|
||||
* when controller.h includes what it uses.
|
||||
*/
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
#include <akbasic/akgl.h>
|
||||
#include <akbasic/error.h>
|
||||
|
||||
static akerr_ErrorContext *in_poll_key(akbasic_InputBackend *self, int *keycode, bool *available)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
/*
|
||||
* An empty ring comes back as success with available false, both here and
|
||||
* upstream. Passed through unchanged: GET reports it as the empty string,
|
||||
* which is ordinary BASIC rather than a failure.
|
||||
*/
|
||||
PASS(errctx, akgl_controller_poll_key(keycode, available));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *in_flush_keys(akbasic_InputBackend *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
(void)self;
|
||||
PASS(errctx, akgl_controller_flush_keys());
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_input_init_akgl(akbasic_InputBackend *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
||||
"NULL backend in input_init_akgl");
|
||||
PASS(errctx, akgl_error_init());
|
||||
|
||||
obj->self = NULL; /* the ring is libakgl's, not ours */
|
||||
obj->poll_key = in_poll_key;
|
||||
obj->flush_keys = in_flush_keys;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
240
src/sink_akgl.c
Normal file
240
src/sink_akgl.c
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* @file sink_akgl.c
|
||||
* @brief The libakgl-backed text sink: where PRINT goes when a host is drawing.
|
||||
*
|
||||
* Implements the section 1.5 vtable over akgl_text_measure() and
|
||||
* akgl_text_rendertextat(). Owns the cursor, the wrap and the scroll --
|
||||
* everything in the reference's basicruntime_graphics.go except Write and
|
||||
* Println, which are the sink interface itself.
|
||||
*
|
||||
* The measurement call is new in libakgl 42b60f7 and is the reason this file can
|
||||
* exist at all: the reference derives its character grid from
|
||||
* font.SizeUTF8("A") (basicruntime.go:96) and there was no akgl_text_*
|
||||
* equivalent until then.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/text.h>
|
||||
|
||||
#include <akbasic/akgl.h>
|
||||
#include <akbasic/error.h>
|
||||
|
||||
/** @brief Rows of scrollback the sink keeps. Matches the `text` array in akgl.h. */
|
||||
#define SINK_MAX_ROWS 64
|
||||
/** @brief Bytes per row, including the terminator. */
|
||||
#define SINK_MAX_COLUMNS 256
|
||||
|
||||
/** @brief Scroll the grid up by one row, dropping the top one. */
|
||||
static void scroll(akbasic_AkglSink *state)
|
||||
{
|
||||
int row = 0;
|
||||
|
||||
for ( row = 0; row < SINK_MAX_ROWS - 1; row++ ) {
|
||||
memcpy(state->text[row], state->text[row + 1], SINK_MAX_COLUMNS);
|
||||
}
|
||||
memset(state->text[SINK_MAX_ROWS - 1], 0, SINK_MAX_COLUMNS);
|
||||
if ( state->cursorrow > 0 ) {
|
||||
state->cursorrow -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Move to the start of the next row, scrolling if that runs off the end. */
|
||||
static void newline(akbasic_AkglSink *state)
|
||||
{
|
||||
state->cursorcol = 0;
|
||||
state->cursorrow += 1;
|
||||
if ( state->cursorrow >= state->rows || state->cursorrow >= SINK_MAX_ROWS ) {
|
||||
scroll(state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Put one character at the cursor, wrapping and scrolling as needed.
|
||||
*
|
||||
* Wrapping happens here, on the character grid, rather than being left to
|
||||
* SDL_ttf's own wraplength. The cursor has to end up somewhere definite: a
|
||||
* program that PRINTs a long string and then PRINTs again expects the second one
|
||||
* to start on the row after the first one ended, and only the code that placed
|
||||
* the characters knows which row that is.
|
||||
*/
|
||||
static void putchar_at(akbasic_AkglSink *state, char c)
|
||||
{
|
||||
if ( c == '\n' ) {
|
||||
newline(state);
|
||||
return;
|
||||
}
|
||||
if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
|
||||
newline(state);
|
||||
}
|
||||
state->text[state->cursorrow][state->cursorcol] = c;
|
||||
state->cursorcol += 1;
|
||||
state->text[state->cursorrow][state->cursorcol] = '\0';
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSink *state = NULL;
|
||||
size_t i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in akgl sink write");
|
||||
state = (akbasic_AkglSink *)self->self;
|
||||
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
||||
"akgl sink has no state");
|
||||
for ( i = 0; text[i] != '\0'; i++ ) {
|
||||
putchar_at(state, text[i]);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSink *state = NULL;
|
||||
|
||||
PASS(errctx, sink_write(self, text));
|
||||
state = (akbasic_AkglSink *)self->self;
|
||||
newline(state);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *sink_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in akgl sink readline");
|
||||
/*
|
||||
* A drawn text layer is not a source of lines. INPUT through a graphics sink
|
||||
* wants a line editor built on the keystroke ring, which is its own piece of
|
||||
* work and is not done: see TODO.md section 3. Until it exists this reports
|
||||
* end of input rather than pretending to have read something.
|
||||
*
|
||||
* EOF rather than an error, because that is the contract sink.h states:
|
||||
* running off the end of input is how RUNSTREAM mode finishes normally, and
|
||||
* INPUT already handles it.
|
||||
*/
|
||||
if ( len > 0 ) {
|
||||
dest[0] = '\0';
|
||||
}
|
||||
*eof = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *sink_clear(akbasic_TextSink *self)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSink *state = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER,
|
||||
"NULL sink in akgl sink clear");
|
||||
state = (akbasic_AkglSink *)self->self;
|
||||
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
||||
"akgl sink has no state");
|
||||
memset(state->text, 0, sizeof(state->text));
|
||||
state->cursorcol = 0;
|
||||
state->cursorrow = 0;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int cellw = 0;
|
||||
int cellh = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
|
||||
"NULL argument in sink_init_akgl");
|
||||
FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
|
||||
"NULL renderer in sink_init_akgl: the host creates it, not the sink");
|
||||
FAIL_ZERO_RETURN(errctx, (font != NULL), AKERR_NULLPOINTER,
|
||||
"NULL font in sink_init_akgl");
|
||||
|
||||
/*
|
||||
* Before anything else in libakgl. akgl_game_init() would have done it, but
|
||||
* an embedded interpreter drives subsystems directly and never calls that --
|
||||
* and a code raised before this runs carries no name into its stack trace.
|
||||
* Idempotent, so a host that already called it loses nothing.
|
||||
*/
|
||||
PASS(errctx, akgl_error_init());
|
||||
|
||||
/*
|
||||
* The character grid, measured rather than assumed. Direct equivalent of the
|
||||
* reference's font.SizeUTF8("A"), and it needs no renderer -- which is why
|
||||
* the sink can size itself before a frame has ever been drawn.
|
||||
*/
|
||||
PASS(errctx, akgl_text_measure(font, "A", &cellw, &cellh));
|
||||
FAIL_ZERO_RETURN(errctx, (cellw > 0 && cellh > 0), AKBASIC_ERR_VALUE,
|
||||
"Font measures a %dx%d character cell, which cannot be a grid",
|
||||
cellw, cellh);
|
||||
FAIL_ZERO_RETURN(errctx, (w >= cellw && h >= cellh), AKBASIC_ERR_BOUNDS,
|
||||
"A %dx%d text area has no room for a %dx%d character",
|
||||
w, h, cellw, cellh);
|
||||
|
||||
memset(state, 0, sizeof(*state));
|
||||
state->renderer = renderer;
|
||||
state->font = font;
|
||||
state->color.r = 0xff;
|
||||
state->color.g = 0xff;
|
||||
state->color.b = 0xff;
|
||||
state->color.a = 0xff;
|
||||
state->x = 0;
|
||||
state->y = 0;
|
||||
state->width = w;
|
||||
state->height = h;
|
||||
state->cellw = cellw;
|
||||
state->cellh = cellh;
|
||||
state->columns = w / cellw;
|
||||
state->rows = h / cellh;
|
||||
if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
|
||||
state->columns = SINK_MAX_COLUMNS - 1;
|
||||
}
|
||||
if ( state->rows > SINK_MAX_ROWS ) {
|
||||
state->rows = SINK_MAX_ROWS;
|
||||
}
|
||||
|
||||
obj->self = state;
|
||||
obj->write = sink_write;
|
||||
obj->writeln = sink_writeln;
|
||||
obj->readline = sink_readline;
|
||||
obj->clear = sink_clear;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akbasic_AkglSink *state = NULL;
|
||||
int row = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
|
||||
"NULL sink in sink_akgl_render");
|
||||
state = (akbasic_AkglSink *)obj->self;
|
||||
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
|
||||
"akgl sink has no state");
|
||||
|
||||
/*
|
||||
* A loop, so no CATCH and no _BREAK macros: they expand to a C break, which
|
||||
* would escape the loop with an error still pending. PASS only.
|
||||
*
|
||||
* Wrapping is off at the draw -- a zero wraplength -- because the grid above
|
||||
* already decided where every line ends. Letting SDL_ttf wrap again would
|
||||
* put characters somewhere the cursor does not think they are.
|
||||
*/
|
||||
for ( row = 0; row < state->rows; row++ ) {
|
||||
if ( state->text[row][0] == '\0' ) {
|
||||
continue;
|
||||
}
|
||||
PASS(errctx, akgl_text_rendertextat(state->font, state->text[row],
|
||||
state->color, 0,
|
||||
state->x,
|
||||
state->y + (row * state->cellh)));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
416
tests/akgl_backends.c
Normal file
416
tests/akgl_backends.c
Normal file
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* @file akgl_backends.c
|
||||
* @brief Tests the libakgl-backed sink and device backends against real pixels.
|
||||
*
|
||||
* Everything here draws into a small software renderer under the dummy video
|
||||
* driver and reads the target back, which is the pattern
|
||||
* deps/libakgl/tests/draw.c established -- it needs no display, no offscreen
|
||||
* harness and no audio hardware.
|
||||
*
|
||||
* These are the only tests in this repository that link SDL, and they only build
|
||||
* under -DAKBASIC_WITH_AKGL=ON. Everything they exercise below the adaptor is
|
||||
* libakgl's own and is tested over there; what is asserted here is the seam:
|
||||
* that a BASIC verb reaches the right akgl call with the right geometry and the
|
||||
* right colour, and that the text sink puts characters where its own cursor says
|
||||
* they are.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/controller.h>
|
||||
#include <akgl/error.h>
|
||||
/*
|
||||
* game.h purely for the `renderer` global and its `_akgl_renderer` storage. The
|
||||
* interpreter never calls akgl_game_init() -- it drives subsystems directly, and
|
||||
* owning the game loop is exactly what goal 3 forbids -- but the draw calls read
|
||||
* that global, so a test standing in for a host has to populate it the same way
|
||||
* deps/libakgl/tests/draw.c does.
|
||||
*/
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/text.h>
|
||||
|
||||
#include <akbasic/akgl.h>
|
||||
#include <akbasic/error.h>
|
||||
#include <akbasic/runtime.h>
|
||||
#include <akbasic/sink.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Width and height of the offscreen target. Small enough to read back whole. */
|
||||
#define TARGET_SIZE 128
|
||||
|
||||
/* The interpreter carries every pool it owns, so it does not fit on a stack. */
|
||||
static akbasic_Runtime RUNTIME;
|
||||
static akbasic_TextSink SINK;
|
||||
static akbasic_StdioSink SINKSTATE;
|
||||
static akbasic_AkglSink AKGLSINKSTATE;
|
||||
static akbasic_TextSink AKGLSINK;
|
||||
static akbasic_GraphicsBackend GRAPHICS;
|
||||
static akbasic_AkglGraphics GRAPHICSSTATE;
|
||||
static akbasic_InputBackend INPUT;
|
||||
static TTF_Font *font = NULL;
|
||||
static char OUTPUT[8192];
|
||||
static FILE *OUT = NULL;
|
||||
|
||||
/** @brief Report whether one pixel of @p shot carries @p color. Alpha is not compared. */
|
||||
static bool pixel_is(SDL_Surface *shot, int x, int y, uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
uint8_t gotr = 0;
|
||||
uint8_t gotg = 0;
|
||||
uint8_t gotb = 0;
|
||||
uint8_t gota = 0;
|
||||
|
||||
if ( shot == NULL ) {
|
||||
return false;
|
||||
}
|
||||
if ( !SDL_ReadSurfacePixel(shot, x, y, &gotr, &gotg, &gotb, &gota) ) {
|
||||
return false;
|
||||
}
|
||||
return ((gotr == r) && (gotg == g) && (gotb == b));
|
||||
}
|
||||
|
||||
/** @brief Clear the target to opaque black so a drawn pixel is unambiguous. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *clear_target(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, SDL_SetRenderDrawColor(renderer->sdl_renderer, 0, 0, 0, 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 Bring up a runtime with the akgl graphics backend attached. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *start_runtime(const char *source)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
memset(OUTPUT, 0, sizeof(OUTPUT));
|
||||
OUT = fmemopen(OUTPUT, sizeof(OUTPUT), "w");
|
||||
FAIL_ZERO_RETURN(errctx, (OUT != NULL), AKERR_IO, "could not open the output buffer");
|
||||
setvbuf(OUT, NULL, _IONBF, 0);
|
||||
|
||||
PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, OUT, NULL));
|
||||
PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
|
||||
PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, renderer));
|
||||
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL));
|
||||
PASS(errctx, akbasic_runtime_load(&RUNTIME, source));
|
||||
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
|
||||
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static void stop_runtime(void)
|
||||
{
|
||||
if ( OUT != NULL ) {
|
||||
fclose(OUT);
|
||||
OUT = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A DRAW reaches real pixels, in the colour COLOR bound to the source.
|
||||
*
|
||||
* This is the assertion the whole adaptor exists for: a BASIC line in, a lit
|
||||
* pixel out, with nothing mocked in between.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_draw_reaches_pixels(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
|
||||
PASS(errctx, clear_target());
|
||||
PASS(errctx, start_runtime("10 COLOR 1, 3\n20 DRAW 1, 10, 20\n"));
|
||||
TEST_REQUIRE_STR(OUTPUT, "");
|
||||
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
/* Palette index 3 is red: 0x88, 0x39, 0x32 in src/graphics_tables.c. */
|
||||
TEST_REQUIRE(pixel_is(shot, 10, 20, 0x88, 0x39, 0x32),
|
||||
"DRAW 1,10,20 after COLOR 1,3 should have lit (10,20) red");
|
||||
TEST_REQUIRE(!pixel_is(shot, 11, 20, 0x88, 0x39, 0x32),
|
||||
"DRAW should have lit one pixel, not two");
|
||||
SDL_DestroySurface(shot);
|
||||
stop_runtime();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief A BOX outlines: its corners are lit and its middle is not. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_box_outlines(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
|
||||
PASS(errctx, clear_target());
|
||||
PASS(errctx, start_runtime("10 BOX 1, 10, 10, 40, 40\n"));
|
||||
TEST_REQUIRE_STR(OUTPUT, "");
|
||||
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
TEST_REQUIRE(pixel_is(shot, 10, 10, 0xff, 0xff, 0xff), "the box's corner should be lit");
|
||||
TEST_REQUIRE(pixel_is(shot, 25, 10, 0xff, 0xff, 0xff), "the box's top edge should be lit");
|
||||
TEST_REQUIRE(!pixel_is(shot, 25, 25, 0xff, 0xff, 0xff),
|
||||
"an unrotated BOX outlines rather than fills");
|
||||
SDL_DestroySurface(shot);
|
||||
stop_runtime();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief PAINT fills the region a BOX encloses and stops at its edge.
|
||||
*
|
||||
* Also the one place the akgl flood fill is exercised through a BASIC verb,
|
||||
* which is where its partial-fill error would surface if it ever fired.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_paint_fills_region(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
|
||||
PASS(errctx, clear_target());
|
||||
PASS(errctx, start_runtime("10 BOX 1, 10, 10, 40, 40\n"
|
||||
"20 COLOR 2, 6\n"
|
||||
"30 PAINT 2, 25, 25\n"));
|
||||
TEST_REQUIRE_STR(OUTPUT, "");
|
||||
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
/* Palette index 6 is green: 0x55, 0xa0, 0x49. */
|
||||
TEST_REQUIRE(pixel_is(shot, 25, 25, 0x55, 0xa0, 0x49),
|
||||
"the inside of the box should have been painted green");
|
||||
TEST_REQUIRE(!pixel_is(shot, 50, 50, 0x55, 0xa0, 0x49),
|
||||
"the paint should have stopped at the box's edge");
|
||||
SDL_DestroySurface(shot);
|
||||
stop_runtime();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief SSHAPE and GSHAPE round-trip a region through the handle in a string. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_shape_roundtrip(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
|
||||
PASS(errctx, clear_target());
|
||||
PASS(errctx, start_runtime("10 BOX 1, 4, 4, 12, 12\n"
|
||||
"20 SSHAPE A$, 0, 0, 16, 16\n"
|
||||
"30 GSHAPE A$, 64, 64\n"));
|
||||
TEST_REQUIRE_STR(OUTPUT, "");
|
||||
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
/* The saved corner was at (4,4); pasted at (64,64) it lands at (68,68). */
|
||||
TEST_REQUIRE(pixel_is(shot, 68, 68, 0xff, 0xff, 0xff),
|
||||
"the pasted shape should carry the box's corner");
|
||||
SDL_DestroySurface(shot);
|
||||
stop_runtime();
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The sink measures a character grid and puts text where its cursor says.
|
||||
*
|
||||
* akgl_text_measure() is the call this whole file was blocked on until libakgl
|
||||
* 42b60f7, so the grid it produces is worth asserting directly rather than only
|
||||
* through what gets drawn.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_sink_grid_and_wrap(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int cellw = 0;
|
||||
int cellh = 0;
|
||||
|
||||
PASS(errctx, akgl_text_measure(font, "A", &cellw, &cellh));
|
||||
TEST_REQUIRE(cellw > 0 && cellh > 0, "the fixture font should measure a real cell");
|
||||
|
||||
PASS(errctx, akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, renderer, font,
|
||||
TARGET_SIZE, TARGET_SIZE));
|
||||
TEST_REQUIRE_INT(AKGLSINKSTATE.cellw, cellw);
|
||||
TEST_REQUIRE_INT(AKGLSINKSTATE.columns, TARGET_SIZE / cellw);
|
||||
TEST_REQUIRE_INT(AKGLSINKSTATE.rows, TARGET_SIZE / cellh);
|
||||
|
||||
PASS(errctx, AKGLSINK.writeln(&AKGLSINK, "HELLO"));
|
||||
PASS(errctx, AKGLSINK.write(&AKGLSINK, "WORLD"));
|
||||
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "HELLO");
|
||||
TEST_REQUIRE_STR(AKGLSINKSTATE.text[1], "WORLD");
|
||||
TEST_REQUIRE_INT(AKGLSINKSTATE.cursorrow, 1);
|
||||
TEST_REQUIRE_INT(AKGLSINKSTATE.cursorcol, 5);
|
||||
|
||||
/* A clear empties the grid and takes the cursor home. */
|
||||
PASS(errctx, AKGLSINK.clear(&AKGLSINK));
|
||||
TEST_REQUIRE_STR(AKGLSINKSTATE.text[0], "");
|
||||
TEST_REQUIRE_INT(AKGLSINKSTATE.cursorrow, 0);
|
||||
TEST_REQUIRE_INT(AKGLSINKSTATE.cursorcol, 0);
|
||||
|
||||
/*
|
||||
* A text area too small for one character is refused rather than silently
|
||||
* producing a zero-column grid, which would divide by zero on the first
|
||||
* wrap.
|
||||
*/
|
||||
TEST_REQUIRE_STATUS(akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, renderer, font, 1, 1),
|
||||
AKBASIC_ERR_BOUNDS);
|
||||
TEST_REQUIRE_STATUS(akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, NULL, font,
|
||||
TARGET_SIZE, TARGET_SIZE),
|
||||
AKERR_NULLPOINTER);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/** @brief The sink's text actually reaches the target when the host renders. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_sink_renders(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Surface *shot = NULL;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
bool lit = false;
|
||||
|
||||
PASS(errctx, clear_target());
|
||||
PASS(errctx, akbasic_sink_init_akgl(&AKGLSINK, &AKGLSINKSTATE, renderer, font,
|
||||
TARGET_SIZE, TARGET_SIZE));
|
||||
PASS(errctx, AKGLSINK.write(&AKGLSINK, "X"));
|
||||
PASS(errctx, akbasic_sink_akgl_render(&AKGLSINK));
|
||||
|
||||
/*
|
||||
* Asserted as "something was drawn in the first cell" rather than against
|
||||
* particular pixels: which ones a glyph lights is FreeType's business and it
|
||||
* is free to hint them differently. The seam being tested is that the text
|
||||
* reached the renderer at the cursor's cell at all.
|
||||
*/
|
||||
shot = SDL_RenderReadPixels(renderer->sdl_renderer, NULL);
|
||||
TEST_REQUIRE(shot != NULL, "could not read the render target back");
|
||||
for ( y = 0; y < AKGLSINKSTATE.cellh && !lit; y++ ) {
|
||||
for ( x = 0; x < AKGLSINKSTATE.cellw && !lit; x++ ) {
|
||||
lit = !pixel_is(shot, x, y, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
TEST_REQUIRE(lit, "the sink drew nothing into the first character cell");
|
||||
SDL_DestroySurface(shot);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The input adaptor drains the ring the host's event pump fills.
|
||||
*
|
||||
* Synthetic key events through akgl_controller_handle_event(), which is exactly
|
||||
* how a host feeds it, and then read back through the akbasic backend.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_input_backend(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_Event event;
|
||||
int keycode = 0;
|
||||
bool available = false;
|
||||
/*
|
||||
* handle_event refuses a NULL appstate outright, so it needs something to
|
||||
* point at even though nothing here reads it. deps/libakgl/tests/controller.c
|
||||
* uses a placeholder for the same reason.
|
||||
*/
|
||||
int appstate_placeholder = 0;
|
||||
|
||||
PASS(errctx, akbasic_input_init_akgl(&INPUT));
|
||||
PASS(errctx, INPUT.flush_keys(&INPUT));
|
||||
|
||||
/* An empty ring is success with nothing available, never an error. */
|
||||
PASS(errctx, INPUT.poll_key(&INPUT, &keycode, &available));
|
||||
TEST_REQUIRE(!available, "an empty ring should report nothing available");
|
||||
|
||||
memset(&event, 0, sizeof(event));
|
||||
event.type = SDL_EVENT_KEY_DOWN;
|
||||
event.key.key = SDLK_A;
|
||||
PASS(errctx, akgl_controller_handle_event(&appstate_placeholder, &event));
|
||||
|
||||
PASS(errctx, INPUT.poll_key(&INPUT, &keycode, &available));
|
||||
TEST_REQUIRE(available, "a key the host pumped should reach the interpreter");
|
||||
TEST_REQUIRE_INT(keycode, SDLK_A);
|
||||
|
||||
PASS(errctx, INPUT.poll_key(&INPUT, &keycode, &available));
|
||||
TEST_REQUIRE(!available, "the ring should drain to empty");
|
||||
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, TTF_Init(), AKGL_ERR_SDL,
|
||||
"Couldn't initialize SDL_ttf: %s", SDL_GetError());
|
||||
FAIL_ZERO_BREAK(errctx,
|
||||
SDL_CreateWindowAndRenderer("net/aklabs/akbasic/test_akgl_backends",
|
||||
TARGET_SIZE, TARGET_SIZE, 0,
|
||||
&window, &renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
|
||||
/*
|
||||
* Populate the backend's vtable by hand, which is not where anybody would
|
||||
* expect to have to do it.
|
||||
*
|
||||
* akgl_text_rendertextat() reaches through renderer->draw_texture, and an
|
||||
* SDL_Renderer alone does not fill that in --
|
||||
* deps/libakgl/tests/draw.c gets away without it because the draw
|
||||
* primitives take the SDL_Renderer directly. The obvious call,
|
||||
* akgl_render_init2d(), does install these six pointers, but it also
|
||||
* creates its own window from the game properties and writes to the
|
||||
* `camera` global, so it is part of the akgl_game_init() path rather than
|
||||
* something a caller who already has a renderer can use. A host embedding
|
||||
* this interpreter is in exactly that position. Filed upstream: what is
|
||||
* wanted is the vtable half of init2d on its own.
|
||||
*/
|
||||
renderer->shutdown = &akgl_render_2d_shutdown;
|
||||
renderer->frame_start = &akgl_render_2d_frame_start;
|
||||
renderer->frame_end = &akgl_render_2d_frame_end;
|
||||
renderer->draw_texture = &akgl_render_2d_draw_texture;
|
||||
renderer->draw_mesh = &akgl_render_2d_draw_mesh;
|
||||
renderer->draw_world = &akgl_render_2d_draw_world;
|
||||
|
||||
/*
|
||||
* libakgl's own monospaced fixture, borrowed rather than copied. Being
|
||||
* monospaced is what makes a character grid meaningful to assert on.
|
||||
*/
|
||||
font = TTF_OpenFont(AKBASIC_TEST_FONT, 12);
|
||||
FAIL_ZERO_BREAK(errctx, font, AKGL_ERR_SDL,
|
||||
"Couldn't open %s: %s", AKBASIC_TEST_FONT, SDL_GetError());
|
||||
|
||||
CATCH(errctx, test_draw_reaches_pixels());
|
||||
CATCH(errctx, test_box_outlines());
|
||||
CATCH(errctx, test_paint_fills_region());
|
||||
CATCH(errctx, test_shape_roundtrip());
|
||||
CATCH(errctx, test_sink_grid_and_wrap());
|
||||
CATCH(errctx, test_sink_renders());
|
||||
CATCH(errctx, test_input_backend());
|
||||
} CLEANUP {
|
||||
if ( font != NULL ) {
|
||||
TTF_CloseFont(font);
|
||||
}
|
||||
TTF_Quit();
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "akgl backend test failed");
|
||||
akbasic_test_failures += 1;
|
||||
/*
|
||||
* FINISH_NORETURN rather than FINISH, matching src/main.c: FINISH
|
||||
* expands a `return __err_context` that this int-returning function
|
||||
* cannot compile even when the branch is unreachable. HANDLE_DEFAULT
|
||||
* above has already marked the context handled, so nothing aborts.
|
||||
*/
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
return akbasic_test_failures;
|
||||
}
|
||||
Reference in New Issue
Block a user