joeylib2/docs/ApiSurface.adoc

317 lines
10 KiB
Text

== Surfaces and Presenting
Declared in `<joey/surface.h>` and `<joey/present.h>`.
A surface is a 320x200 16-colour image with its own 200-entry SCB table and its
own 16x16 palette set. Storage is target-native -- chunky 4bpp on the IIgs,
DOS, DVX, and the hosted ports, planar on the Amiga, ST, and X68000 -- but every
entry point speaks in colour indices, so your code never sees the difference.
A new surface starts with the 16-colour EGA palette in all 16 of its palettes.
=== Constants
[cols="3,1,4", options="header"]
|===
|Name |Value |Meaning
|`JL_SURFACE_WIDTH` |320 |Surface width in pixels.
|`JL_SURFACE_HEIGHT` |200 |Surface height in pixels.
|`JL_SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form.
|`JL_SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|`JL_SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|`JL_SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|`JL_SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface.
|===
=== jlSurfaceT
[source,c]
----
typedef struct jlSurfaceT jlSurfaceT;
----
Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet`, or a loader.
=== jlSurfaceCreate
[source,c]
----
jlSurfaceT *jlSurfaceCreate(void);
----
Allocates a new offscreen surface. Each one costs about 32 KB. On the Amiga
that comes from chip RAM, where the blitter can work on its planes; once chip
RAM is full, a surface comes from Fast RAM instead, and its fills and copies
run on the CPU, more slowly, so make the surfaces drawn on most first.
*Parameters:* none.
*Returns:* the surface, or `NULL` on failure (`jlLastError()` explains why).
=== jlSurfaceDestroy
[source,c]
----
void jlSurfaceDestroy(jlSurfaceT *s);
----
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`s` |`jlSurfaceT *` |Surface to release. `NULL` is a no-op, and passing the
stage is also a no-op -- the library owns it.
|===
*Returns:* nothing.
=== jlStageGet
[source,c]
----
jlSurfaceT *jlStageGet(void);
----
Returns the library-owned stage: the back buffer that `jlStagePresent` flips to
the display. Always valid between `jlInit` and `jlShutdown`.
On the IIgs the stage's pixel buffer is pinned to bank `$01` Super Hi-Res space
with shadowing inhibited, so writes run at full speed and stay isolated from
the displayed framebuffer until the next present.
*Parameters:* none. *Returns:* the stage surface.
=== jlSurfaceCopy
[source,c]
----
void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src);
----
Copies pixels, SCBs, and palettes from `src` into `dst`.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination. Must be valid.
|`src` |`const jlSurfaceT *` |Source. Must be valid.
|===
*Returns:* nothing.
=== jlSurfaceCopyRect
[source,c]
----
void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src,
int16_t x, int16_t y, uint16_t w, uint16_t h);
----
Copies a pixel rectangle from `src` to `dst` *at the same position* -- there is
no repositioning, so the planar ports never have to shift bits. This is the
erase half of the clean-buffer pattern: keep a pristine copy of the scene, and
restore rectangles from it before redrawing sprites.
Copies pixels only. SCBs and palettes are untouched.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`src` |`const jlSurfaceT *` |Source surface.
|`x`, `y` |`int16_t` |Top-left of the rectangle, clipped to the surface.
|`w`, `h` |`uint16_t` |Size in pixels.
|===
*Returns:* nothing.
[WARNING]
====
The rectangle is clipped to the surface and then *widened outward to the
16-pixel group grid on every port*: `x` rounds down and `x + w` rounds up to
multiples of 16. Two consequences you own:
* up to 15 extra pixels each side are copied, so a snapped erase can clobber
on-screen content outside the rectangle you asked for;
* it silently invalidates any live `jlSpriteBackupT` whose recorded region it
touches.
Erase *all* regions, then draw *all* sprites. Never mix this with per-sprite
save and restore in the same region.
====
=== jlSurfacePasteIndexed
[source,c]
----
void jlSurfacePasteIndexed(jlSurfaceT *dst, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *src, uint16_t srcStride, bool doubleWide);
----
Writes a rectangle of colour indices from your memory onto a surface: one byte
a pixel, whatever the port stores pixels as. Row `r` of the source starts at
`src + r * srcStride`. Each byte's low nibble is the colour index; the high
nibble is ignored, so a byte array of 0..15 with flags in the top bits pastes
as it is.
With `doubleWide` each byte fills two pixels side by side, so `w` bytes cover
`2 * w` pixels: a 160-wide picture fills the 320-wide surface, the way Sierra's
AGI draws.
The rectangle is clipped to the surface pixel by pixel (a doubled byte cut by
an edge shows its one pixel there). Only the pasted pixels change and are
marked for the next `jlStagePresent`.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`x`, `y` |`int16_t` |Top-left pixel on the surface.
|`w`, `h` |`uint16_t` |Bytes a row and rows.
|`src` |`const uint8_t *` |The colour indices, row by row.
|`srcStride` |`uint16_t` |Bytes from one source row to the next.
|`doubleWide` |`bool` |Each byte fills two pixels.
|===
*Returns:* nothing.
[source,c]
----
// A 160x168 AGI picture, doubled to 320 wide, below an 8-row status line.
jlSurfacePasteIndexed(jlStageGet(), 0, 8, 160, 168, picture, 160, true);
----
Every port has a fast path: asm on the IIgs (two pixel pairs a 16-bit
operation) and on the planar ports (table-driven chunky-to-planar, eight
pixels a step), four bytes a step in C on the chunky ports.
=== jlSurfaceSnapIndexed
[source,c]
----
void jlSurfaceSnapIndexed(const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dst, uint16_t dstStride, bool doubleWide);
----
The reverse of `jlSurfacePasteIndexed`: reads a rectangle of a surface into your
memory as colour indices, one byte a pixel (0..15), row `r` at
`dst + r * dstStride`. With `doubleWide` the rectangle is `2 * w` pixels wide
and each byte takes the left pixel of its pair. A byte whose pixel is off the
surface reads 0, as `jlSamplePixel` does. Bytes between `w` and `dstStride` are
left alone.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`src` |`const jlSurfaceT *` |Source surface.
|`x`, `y` |`int16_t` |Top-left pixel on the surface.
|`w`, `h` |`uint16_t` |Bytes a row and rows.
|`dst` |`uint8_t *` |Where the colour indices go, row by row.
|`dstStride` |`uint16_t` |Bytes from one destination row to the next.
|`doubleWide` |`bool` |Read every other pixel.
|===
*Returns:* nothing.
=== jlSurfaceSaveFile
[source,c]
----
bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path);
----
Writes a surface to disk in target-native form: 32000 pixel bytes, then 200 SCB
bytes, then 512 bytes of `$0RGB` palette data.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`src` |`const jlSurfaceT *` |Surface to write.
|`path` |`const char *` |Destination path.
|===
*Returns:* `true` on success; `false` if the file could not be opened or the
write failed.
[IMPORTANT]
====
The format is *not portable*. No endianness or layout conversion is performed,
so the same platform that wrote a file must be the one that reads it. Use this
for runtime caches, not for shipped assets.
====
=== jlSurfaceLoadFile
[source,c]
----
bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path);
----
Reads a surface from disk, overwriting `dst`. The surface keeps its identity --
nothing is reallocated.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Surface to overwrite.
|`path` |`const char *` |Source path.
|===
*Returns:* `true` on success; `false` if the file is missing, the wrong size,
has an SCB byte above 15, or unreadable. A read that fails part way can leave
the pixels partly overwritten; the SCBs and palettes are then untouched. Colour
0 of every palette loaded is set to black, as `jlPaletteSet` does. Neither file
call sets `jlLastError`.
=== jlSurfaceHash
[source,c]
----
uint32_t jlSurfaceHash(const jlSurfaceT *s);
----
Computes a 32-bit content hash over the surface: the packed chunky pixel byte
stream, then the 200 SCB bytes, then the 256 palette entries.
The important property is that *the same logical pixels hash the same on every
port*, whatever the internal storage format. That makes it the tool for proving
two platforms render identically -- which is exactly how the library's own
cross-port validation works.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`s` |`const jlSurfaceT *` |Surface to hash.
|===
*Returns:* the hash value.
=== jlStagePresent
[source,c]
----
void jlStagePresent(void);
----
Flips the dirty regions of the stage to the display and clears the dirty state.
Where the stage already has the display's layout this is a copy (the IIgs's
4bpp pixels; the planes of the Amiga, ST, and X68000); DOS, DVX, and the hosted
ports expand each 4-bit pixel to their display's 8-bit or RGB pixels. Palette
and SCB changes reach the display here too.
Drawing primitives mark per-row dirty ranges as a side effect, so you only need
to call this at the end of a frame and everything you drew appears. Rows
nothing touched since the last present are skipped.
The Amiga, the ST, DOS, and the X68000 draw into a second screen page and flip
to it at the next vertical blank, so the picture never tears. A present that comes before
that blank, right after another one, waits for it: the page it would draw into
is still on the screen. Present once a frame, after `jlWaitVBL`, and it never
waits. On DOS, a present that changes colours also waits: VGA loads them in
the blank that shows the new page, and the `jlWaitVBL` after it returns at
once, so the loop keeps its pace.
*Parameters:* none. *Returns:* nothing.
[NOTE]
====
Every call that changes the stage's pixels marks the rows it touched, and the
API has no other way to write them, so there is nothing to mark by hand.
====