FastForward Plugin Developer’s Guide
Authoring LoadSeg plugins for the FastForward pipeline
This guide is for authors of FastForward plugins — LoadSeg modules that implement sources, demuxers, codecs, filters, sinks, and muxers. Application developers integrating movies and gadgets should read the Developer’s Guide instead.
Companion references: Source/include/libraries/ffplugin.h, Docs/ABI_v0.md, Docs/PLUGIN_GUIDE.md.
Table of contents
1. Plugin authoring
2. Buffer memory classes
3. Chunky-to-planar helper
4. Memory-safe counted buffers
5. Plugin development guide (full)
1. Plugin authoring
Source/include/libraries/ffplugin.h is the plugin contract. A plugin is a LoadSeg binary that begins with FFPluginHead (security word 0x70FF4E75, id 'FFPI') and exposes one or more FFPlugin records. The implemented plugin sources live flat in Source/plugins/; binary names use the plugin kind as the extension, e.g. pcm.demux, pcm.codec, file.source, ahi.sink.
1.1 Plugin kinds
| Kind |
Number |
Mandatory entry points |
Optional |
| FFK_SOURCE |
1 |
Open, Pull, Close |
Seek, QueryDuration |
| FFK_DEMUX |
2 |
Open, Push, Pull, Close |
Seek |
| FFK_CODEC |
3 |
Open, Push, Pull, Close |
|
| FFK_FILTER |
4 |
Open, Push, Pull, Close |
|
| FFK_SINK |
5 |
Open, Push, Close |
|
| FFK_MUX |
6 |
Open, Push, Pull, Close |
|
| FFK_CLOCK |
7 |
Defined by clock plugin contract |
|
1.2 Capability flags
Plugins advertise the FFC_* capability bitmask in p_Caps:
| Flag |
Meaning |
| FFC_AUDIO |
Plugin handles audio data |
| FFC_VIDEO |
Plugin handles video data |
| FFC_REENTRANT |
Multiple instances safe; properties may change at runtime |
| FFC_ENCODER |
Codec encodes as well as decodes |
| FFC_SEEK |
Supports random-access seek |
| FFC_CLOCK_SLAVE |
Plugin can drive the master clock |
| FFC_DATA |
Generic data (subtitles, MIDI) |
| FFC_LIVE |
Live / non-seekable stream |
| FFC_IMAGE |
Still or animated image |
| FFC_SUBTITLE |
Timed text overlay |
1.3 Host services
Plugins receive an FFHost struct with allocator, logger, library opener, current clock pointer, event poster, pooled buffer allocator and the chunky-to-planar helpers. State lives in FFInstance.fi_Private; reentrancy is automatic because every Open allocates a fresh instance.
1.4 Optional plugins
Additional codec and format plugins ship as separate LoadSeg binaries that the engine discovers at runtime. They are never linked into fastforward.library. All plugin binaries install into the same LIBS:FastForward/Plugins/ directory; applications load whatever is present when the library opens.
2. Buffer memory classes
Every FFBuffer carries an fb_MemClass field telling the engine which memory pool fb_Data was drawn from. Plugins that allocate buffers via FFHost.fh_AllocBufferClass(host, size, memclass) get the right pool automatically:
| FFBM_* |
Selects |
Use |
| FFBM_FAST |
`MEMF_FAST \ |
MEMF_PUBLIC` |
Decoder / filter / demuxer buffers — CPU work area |
| FFBM_CHIP |
`MEMF_CHIP \ |
MEMF_PUBLIC` |
DMA targets: AHI sample buffers (Paula), blitter sources, copperlists |
| FFBM_LOCAL |
MEMF_LOCAL (24-bit then Chip on 1 MB systems) |
24-bit-addressable buffers for legacy DMA-capable hardware |
| FFBM_ANY |
MEMF_ANY |
Engine picks whatever is available |
| FFBM_VRAM |
RTG card-local memory (Picasso96 / CGX) when available, falls back to FFBM_FAST today |
Future video sinks: decode straight into the framebuffer with no Zorro round-trip. Reserved as a stable hint so plugins written today keep working when the RTG-aware allocator lands. |
The cardinal rule: only the final sink stage requests Chip RAM. Decoders, filters, and codecs all stay in Fast RAM, where the 68k is fastest. The AHI sink converts at the last possible moment into a Chip-RAM ring buffer that Paula DMAs from with zero CPU cost; native AGA / OCS / ECS video sinks decode into Fast-RAM chunky buffers and call RenderFFC2P() to scatter the result into the Chip-RAM playfield planes; RTG sinks skip C2P entirely and write chunky straight into FFBM_VRAM. The graph never copies bytes between Fast and Chip more than once per stream.
15.1 Refcount and fan-out
FFBuffer carries a fb_RefCount (initialised to 1 on acquire). When you fan a buffer out to two consumers (demux → audio + video, source → tee → two filters), each consumer that wants to keep the buffer alive past return calls FFHost.fh_RetainBuffer(buf) and later FFHost.fh_ReleaseBuffer(host, buf). The buffer recycles to its free port (or, for non-pooled buffers, calls FFBufferFree) only when the count reaches zero. Plugins that consume in lock-step with their input do not touch the refcount at all.
3. Chunky-to-planar helper (AGA / OCS / ECS sinks)
Native chipset video sinks need to turn 8-bit chunky frames (decoded in Fast RAM) into planar Chip-RAM bitmaps the copper / blitter can display. FastForward ships a hyper-optimised helper for this hot path so every video plugin does not reinvent it. Include <fastforward/ff_c2p.h> for the request struct, tags, and flags.
Typical sink setup:
struct FFC2PContext *c2p;
struct FFC2PRequest req;
c2p = CreateFFC2P(
FFC2PT_BitMap, playfield,
FFC2PT_AssumeAligned, TRUE, /* full-frame blits */
TAG_DONE);
req.cr_Chunky = buf->fb_Data; /* FFBM_FAST decode buffer */
req.cr_StartX = 0;
req.cr_StartY = 0;
req.cr_Width = width;
req.cr_Height = height;
req.cr_SrcModulo = width; /* tight rows; omit when
cr_Width == stride */
req.cr_Flags = FFC2PF_NO_TRAILING_RMW; /* full-width overwrite */
RenderFFC2P(c2p, &req); |
Plugins receive the same three functions through FFHost.fh_C2PCreate / fh_C2PRender / fh_C2PDispose so they never need to link against a separate C2P library. CreateFFC2P selects the backend once from SysBase->AttnFlags: 68020+ uses the register-pack path for the full-byte body, while 68000 / 68010 uses the bitspread fallback. The engine still builds the 2 KiB bitspread lookup table once per library open because both backends use it for leading / trailing partial-byte edges.
| API |
Purpose |
| CreateFFC2PA(tags) |
Bind to a struct BitMap , cache plane pointers. Returns opaque FFC2PContext . |
| RenderFFC2P(ctx, &req) |
Blit one chunky rectangle into the bound bitmap. Returns FFC2P_OK or negative FFC2P_*. |
| DeleteFFC2P(ctx) |
Free the context; shared table stays until library expunge. |
Per-call flags (cr_Flags):
| Flag |
Effect |
| FFC2PF_NO_TRAILING_RMW |
Skip read-modify-write on the right edge (full-width overwrite, the common case for a full-frame video sink). |
| FFC2PF_NO_LEADING_RMW |
Same for the left edge. |
| FFC2PF_REENTRANT |
Caller promises this context is only ever touched by the calling task; engine skips the Forbid() it would otherwise wrap around the cached plane-pointer array. |
RTG note: Picasso96 / CGX framebuffers are chunky — those sinks use FFBM_VRAM and memcpy, not this helper.
The engine does not manage double-buffer flipping itself: that is a compositor concern best done with a copperlist (cheap) or AllocSpriteData / SetRGB32 / etc. The C2P helper produces correct planar data inside the bitmap the caller hands it; the caller swaps front / back planes via plane-pointer rewrites in a copperlist when it wants a clean tear-free flip.
4. Memory-safe counted buffers (FFString, FFData)
Plugin code touches untrusted bytes by definition - a malformed container should never overrun a C buffer in the host. The public ABI therefore exposes two counted-buffer types and a set of inline helpers in <fastforward/ff_string.h>:
struct FFString {
STRPTR fs_Data;
ULONG fs_Length; /* used bytes, not counting any trailing NUL */
ULONG fs_Capacity; /* allocated bytes; 0 == borrowed view */
UWORD fs_Encoding; /* FFSE_LATIN1 / FFSE_UTF8 / FFSE_LOCAL / ...*/
UWORD fs_Flags; /* FFSF_OWNED / FFSF_NUL_TERMINATED / */
/* FFSF_READONLY / FFSF_TRUNCATED */
};
struct FFData {
APTR fd_Data;
ULONG fd_Length;
ULONG fd_Capacity;
ULONG fd_Flags;
}; |
Two modes of use:
Borrowed (zero-copy view) — wrap a pointer you already trust:
struct FFString title;
FFBorrowCString(&title, "Song Title", FFSE_LATIN1);
/* title now points at the literal; FFDisposeStringAlloc is a no-op */ |
Owned (allocated, bounded) — give the library a buffer of fixed capacity it can fill safely:
struct FFString *name = FFNewStringAlloc(64, FFSE_LATIN1);
FFCopyIntoString(name, src_bytes, src_length);
if (name->fs_Flags & FFSF_TRUNCATED) {
/* tag came in longer than 63 bytes - we still have a valid
NUL-terminated prefix in name->fs_Data and the truncation
was deliberate */
}
FFDisposeStringAlloc(name); |
FFCopyIntoString never writes past fs_Capacity-1, always NUL-terminates, and sets FFSF_TRUNCATED when the source did not fit. FFCopyIntoData is the binary equivalent for codec private data, demux header bytes, packet payloads etc.
Other inline helpers worth knowing:
| Helper |
Purpose |
| FFBorrowData(d, ptr, len) |
Wrap counted bytes you already own |
| FFBorrowString(s, ptr, len, enc) |
Wrap counted text with a known encoding |
| FFStringIsEmpty(s) |
TRUE if NULL or zero-length |
| FFStringEqual(a, b) |
Byte-exact equality on length-prefix |
| FFNewDataAlloc(cap) / FFDisposeDataAlloc(d) |
Bounded byte buffer |
Rules of thumb for plugin authors:
1. Never call FFStrCopy() on data that came out of a media file.
Use FFCopyIntoString() into an owned FFString, or FFCopyIntoData() into an owned FFData.
2. Treat container header fields, codec private blobs, and metadata
tag strings as counted from byte one - they have an explicit length on disk; respect it.
3. When returning a value to the host, prefer FFBorrow* for
memory you own and FFNew*Alloc for memory you allocate fresh.
4. FFSF_OWNED is the single bit that tells FFDispose*Alloc
whether to call FreeMem, so mixing owned and borrowed views inside the same code path is safe.
STRPTR is still used at the boundary where AmigaOS expects it (filenames, assigns, tag values for OpenLibrary-style calls) and for fixed-size identifier fields inside structs like FFErrorInfo.ei_Component. Anywhere the library accepts or returns text derived from media content, the counted form is the safe path.
5. Plugin development guide (full)
# FastForward plugin development guide
This document is the practical companion to ABI_v0.md (the wire format) and API.md (the public engine API). It collects every rule, idiom and pitfall the FastForward team has actually paid for while bringing up the MOV / Cinepak / SVQ1 / IMA-ADPCM stack, distilled into a checklist a new plugin author can follow without re-discovering each crash.
The guiding principle is two-tier:
1. Cold paths must be obviously memory-safe. Open, Configure,
Probe, Close, table-driven format dispatch, parser bring-up. Use the project's FF… helpers, NULL-check every allocation, guard every atom walk. Anything that runs once per stream should look boring.
2. Hot paths must be hyper-optimised. Per-sample decode loops,
per-frame YUV→RGB conversion, per-block bit-planar fan-out. Use raw UBYTE * arithmetic, hand-unroll, exploit alignment when you can prove it.
The two tiers meet at the Push / Pull boundary. Everything above that boundary (parameter validation, state machine transitions, plugin plumbing) is cold; everything below it (per-sample decode loops, per-frame conversion, per-block bit operations) is hot.
1. Build environment
1.1 The SCOPTIONS contract
Every plugin compiles under FastForward/Source/plugins/SCOPTIONS:
CPU=68000
STRUCTUREEQUIVALENCE
STRINGMERGE
NOSTACKCHECK
OPTIMIZE
OPTGLOBAL
OPTTIME
OPTPEEP
OPTSCHED
NOICONS
IGNORE=315
IGNORE=316
INCLUDEDIR=/include
MATH=STANDARD
UTILLIB
NOCHECKABORT
DATA=FARONLY |
Two of those switches are load-bearing for ABI safety:
UTILLIB — every 32-bit multiply, divide and modulo SAS/C
generates is lowered to a utility.library pragma stub that reads a global _UtilityBase. If your plugin contains any ULONG / ULONG or LONG * LONG and you do not declare and populate UtilityBase, slink reports undefined symbol _UtilityBase and the plugin will not link. See section 4.3.
DATA=FARONLY — your plugin's data segment is not addressed
through a4 small-data. Function pointers the engine passes you (host->fh_Alloc, etc.) are pointers into the library's own segment, which does use a4. Section 4.1 explains why this matters.
1.2 Strict C89
The whole engine targets ANSI C / C89. The most-cited consequences for plugin authors:
Declare every local at the top of its block. No mid-block
declarations. SAS/C will tolerate for (LONG i = 0; ...) in some modes but the project SCOPTIONS does not enable it.
No // comments. Use /* … */.
No designated initialisers. Initialise struct FFPlugin field by
position; the order matches <libraries/ffplugin.h>.
No variadic macros, inline, restrict, _Bool. Use
static __inline (SAS/C extension), BOOL, and macro fan-outs instead.
1.3 No libc
LoadSeg plugins do not link the C runtime. That has knock-on effects you will trip over the first time:
| You wrote |
What SAS/C emits |
Consequence |
| dst = src; (struct of >~16 bytes) |
call to private __CXAMEMCPY in sc.lib |
unresolved symbol at link time |
| memcpy(dst, src, n); |
call to _memcpy from sc.lib |
unresolved symbol |
| memset(dst, c, n); |
call to _memset from sc.lib |
unresolved symbol |
| strcpy, strlen, strcmp |
same |
unresolved symbols |
| (long long)x arithmetic |
__cln, __divll etc. from sc.lib |
unresolved symbol |
| 1.0 / x floating point |
math library stubs |
unresolved symbol (see §4.3.5) |
| exp(), log10() etc. |
sc.lib transcendentals |
__XCEXIT unresolved + near refs (§4.3.5) |
Use the equivalents from section 3 (helpers) and section 4 (exec substitutes) instead. If you find yourself reaching for <string.h>, stop and use <fastforward/ff_string.h>.
2. Anatomy of a plugin
A plugin file has one job: expose a FFPluginHead followed by one or more FFPlugin records. Everything else is implementation detail.
2.1 File skeleton
#include <exec/types.h>
#include <exec/memory.h>
#include <proto/exec.h>
#include <libraries/ffplugin.h>
#include <libraries/fastforward.h>
#include <fastforward/ff_string.h>
/*
* UTILLIB pragmas reference _UtilityBase. Declare and refcount it so
* the first Open() lazy-opens utility.library and the last Close()
* releases it. Without this, slink complains _UtilityBase is unresolved.
*/
struct Library *UtilityBase = NULL;
static UWORD MyPluginUtilityRefCount = 0;
struct MyCodecPriv {
UBYTE myc_HaveFormat;
UBYTE myc_Pad[3];
struct FFVideoFormat myc_In;
/* ... per-instance state, including any decoder context ... */
};
static CONST_STRPTR MyCodecTypes[] = { "MYCC", NULL };
const UBYTE version[] = "$VER: mycodec.codec 0.1 (FastForward)";
static LONG FF_PLUGIN_ENTRY
MyProbe(struct FFHost *host, struct FFProbe *probe);
static struct FFInstance * FF_PLUGIN_ENTRY
MyOpen(struct FFHost *host);
static LONG FF_PLUGIN_ENTRY
MyConfigure(struct FFInstance *inst, struct TagItem *tags);
static LONG FF_PLUGIN_ENTRY
MyPush(struct FFInstance *inst, struct FFBuffer *buf);
static LONG FF_PLUGIN_ENTRY
MyPull(struct FFInstance *inst, struct FFBuffer *buf);
static void FF_PLUGIN_ENTRY
MyClose(struct FFInstance *inst);
struct FFPlugin MyCodecPlugin = {
NULL, /* p_Next - engine fills in */
FFPLUGIN_VERSION,
FFENGINE_VERSION,
0, /* p_Reserved */
1, /* p_API */
0x4001, /* p_Identifier - allocate per-plugin */
FFK_CODEC,
FFC_VIDEO | FFC_REENTRANT,
"My codec description",
MyCodecTypes,
MyProbe,
MyOpen,
MyConfigure,
MyPush,
MyPull,
MyClose,
NULL, NULL, NULL, NULL
}; |
2.2 The FF_PLUGIN_ENTRY macro
This is not decoration. It expands to the calling convention attribute SAS/C needs so the engine can call your callbacks safely from any task context. Every plugin export must use it. Functions you call only from your own plugin (static helpers) must not.
2.3 Open / Close lifecycle
Open runs once per instance. The engine guarantees you a non-NULL host and exclusive ownership of the returned FFInstance. The canonical pattern:
static struct FFInstance * FF_PLUGIN_ENTRY
MyOpen(struct FFHost *host)
{
struct FFInstance *in;
struct MyCodecPriv *priv;
if (host == NULL) {
return NULL;
}
/* Lazy-open utility.library before any 32-bit divmul runs. */
if (MyPluginUtilityRefCount == 0) {
UtilityBase = (struct Library *)host->fh_OpenLibrary(
(CONST_STRPTR)"utility.library", 37);
if (UtilityBase == NULL) {
return NULL;
}
}
priv = (struct MyCodecPriv *)host->fh_Alloc(
sizeof(struct MyCodecPriv), MEMF_CLEAR | MEMF_ANY);
if (priv == NULL) {
if (MyPluginUtilityRefCount == 0 && UtilityBase != NULL) {
host->fh_CloseLibrary((APTR)UtilityBase);
UtilityBase = NULL;
}
return NULL;
}
in = (struct FFInstance *)host->fh_Alloc(
sizeof(struct FFInstance), MEMF_CLEAR | MEMF_ANY);
if (in == NULL) {
host->fh_Free(priv, sizeof(struct MyCodecPriv));
if (MyPluginUtilityRefCount == 0 && UtilityBase != NULL) {
host->fh_CloseLibrary((APTR)UtilityBase);
UtilityBase = NULL;
}
return NULL;
}
in->fi_Host = host;
in->fi_Plugin = &MyCodecPlugin;
in->fi_Private = priv;
MyPluginUtilityRefCount++;
return in;
} |
Close is the mirror image, in reverse order. Three rules that have been violated in practice and produced crashes:
1. **Every fh_Alloc must be paired with fh_Free and the size you
passed to fh_Alloc.** fh_Free(ptr, 0) corrupts the heap.
2. **fh_Free every sub-allocation,** including format-dependent
buffers your decoder grew during Push. Walk your MyCodecPriv field by field in Close.
3. **Decrement the utility refcount last, and only release UtilityBase
when the refcount returns to zero.** Until the last instance exits, another instance may still be running a 32-bit multiply that needs it.
3. Memory-safe cold paths
The FastForward/Source/include/fastforward/ff_string.h header is a standalone library of no-libc, no-AllocMem-on-the-hot-path helpers. Use them first; reach for raw CopyMem / pointer arithmetic only when the helpers do not express what you need.
3.1 Strings
| Use this |
Instead of |
| FFStrLen(s) |
strlen |
| FFStrEqual(a, b) |
strcmp(a,b) == 0 |
| FFStrCopy(dst, src) |
strcpy(dst, src) |
| FFStrCopyN(dst, src, n) |
strncpy / handwritten loop |
FFStrCopyN always NUL-terminates and never overruns. FFStrCopy assumes the destination already has FFStrLen(src) + 1 bytes available.
Do not use FFStrEqualNoCase from inside a plugin. It is only declared under FF_USE_UTILITY, which the engine defines but plugins must not — see section 4.3. If you need case-insensitive comparison, roll a small ASCII-only loop or call Stricmp after you have lazy- opened utility.library and saved its base in a plugin-local global.
3.2 Counted buffers (FFData, FFString)
These are the safe long-form alternative to bare (ptr, len) pairs flowing through your plugin API. They carry capacity, length, encoding and an owned/borrowed flag, and they never trust the caller's pointer arithmetic.
struct FFString *msg;
msg = FFNewStringAlloc(256, FFENC_ASCII);
if (msg == NULL) {
return -1;
}
FFCopyIntoString(msg, "Hello", 5); /* never overruns */
/* ... use msg->fs_Data, msg->fs_Length ... */
FFDisposeStringAlloc(msg); |
The pair allocates the header and the data buffer in one AllocMem block, so disposal is one FreeMem. The truncation flag FFSF_TRUNCATED lets callers detect data loss without a separate API.
For non-NUL data, use the FFData siblings: FFNewDataAlloc, FFDisposeDataAlloc, FFCopyIntoData.
When you only need a view into someone else's bytes (engine-provided metadata, a parsed atom, etc.), use the borrow variants: FFBorrowData, FFBorrowString, FFBorrowCString. They set FFSF_READONLY and FFDisposeStringAlloc/FFDisposeDataAlloc are no-ops on borrowed views, so you can pass borrowed and owned values through the same code path without conditionals.
3.3 Bytes
FFCopyBytes(src, dst, n) is the no-libc memcpy you want. Internally it's CopyMem with NULL/zero-length guards. FFFillBytes(dst, n, v) is the byte-level fill (use it for non-zero fills; for zero use MEMF_CLEAR on AllocMem and avoid the fill entirely).
Never use memcpy, memset, bcopy, bzero. The SAS/C linker will try to resolve them out of sc.lib, which plugins do not link. The build either fails or, worse, silently links a stub that crashes.
3.4 Allocation
| Cold-path allocation |
Notes |
| `host->fh_Alloc(n, MEMF_CLEAR\ |
MEMF_ANY)` |
Preferred. Routed through the engine's pool allocator. |
| `AllocMem(n, MEMF_CLEAR\ |
MEMF_ANY)` |
Direct exec.library. Use if host is not available (e.g. decoder-core file with no FFHost). |
| AllocVec(n, flags) |
Only when the consumer is going to FreeVec without knowing the size (rare in this codebase). |
| host->fh_AllocBuffer(host, n, flags) |
Hot path. Round-robin pool, no global lock. |
Six rules every allocation in the engine has been audited against:
1. Always check for NULL. AllocMem(MEMF_ANY) can and does fail on
busy systems. The crashes you don't see are the ones where every allocation site has a NULL check; the crashes you do see are always the missing one.
2. **MEMF_CLEAR is not optional for buffers you read before fully
writing.* Cinepak's U/V planes are a textbook case: the decoder only writes the strip positions present in the bitstream, then YUV→RGB reads every byte. Without MEMF_CLEAR the unwritten bytes are whatever stale heap content AllocMem handed back — sometimes zero, sometimes trap-handler code, sometimes the bytes of the struct Library you allocated last frame.
3. Free with the same size you allocated. Per exec.doc/FreeMem,
"If you pass the wrong pointer, you will probably see AN_MemCorrupt $01000005." That crash is delayed — usually one to a few hundred ms after the bad free — so it shows up as "random" crashes in unrelated code.
4. Don't allocate in Push / Pull if you can avoid it. Section 5
covers the hot-path discipline. The short version: allocate once in Open or Configure, re-use, free once in Close.
5. Track owned vs. borrowed pointers. If the engine handed you a
pointer (buf->fb_Data, a tag value, a borrowed string), you do not own it and you must not free it. If your struct field exists, you allocated it, and you must free it. The FFData / FFString helpers (3.2) encode this in FFSF_OWNED so you never have to guess.
6. No MEMF_FAST unless you can prove the target has fast RAM. Per
exec.doc/AllocMem: "If MEMF_FAST is set, AllocMem() will fail on machines that only have chip memory!" Use MEMF_ANY and let the OS pick.
3.5 File I/O
There is one correct way to determine an opened file's size:
struct FileInfoBlock *fib;
ULONG filesize = 0;
fib = (struct FileInfoBlock *)AllocDosObject(DOS_FIB, NULL);
if (fib != NULL) {
if (ExamineFH(fh, fib)) {
filesize = (ULONG)fib->fib_Size;
}
FreeDosObject(DOS_FIB, fib);
} |
If you absolutely cannot use ExamineFH (e.g. you're on a filesystem that lies about fib_Size), the fallback is the seek-pair trick:
Seek(fh, 0, OFFSET_END);
seekrc = Seek(fh, 0, OFFSET_BEGINNING);
if (seekrc >= 0) {
filesize = (ULONG)seekrc;
} |
Seek returns the previous position, not the new one — that is the bug that made our first mov.demux reject every file with filesize=0 until we noticed.
Always check Read's return:
> 0: that many bytes were read; you may have got fewer than asked.
= 0: EOF.
< 0: error, IoErr() has the code.
n = Read(fh, buf, (LONG)want);
if (n < 0 || (ULONG)n != want) {
/* error or short read - bail */
} |
3.6 Tag lists
Always treat tag values as ULONG and cast to your real type at use site. GetTagData(tag, default, taglist) is the only safe way to fish values out:
priv->vs_Window = (struct Window *)GetTagData(FFMT_Window, 0UL, tags);
priv->vs_RPort = (struct RastPort *)GetTagData(FFMT_RastPort, 0UL, tags);
priv->vs_BitMap = (struct BitMap *)GetTagData(FFMT_BitMap, 0UL, tags); |
Never iterate tags by hand; the engine may insert TAG_IGNORE, TAG_MORE, TAG_SKIP you don't expect.
4. The Amiga-side traps that have actually bitten us
Every entry in this section is a bug class we have shipped and fixed. The headers tell you the symptom; the body tells you why and how.
4.1 DATA=FARONLY and the a4 register
Symptom: A plugin calls a host->fh_… function pointer and the engine immediately crashes with "invalid instruction" inside what looks like the very first library global access.
Cause: DATA=FARONLY means the plugin does not manage a4. The engine's library code does — its small-data lives at (a4). When the engine takes a function-pointer call from a plugin, a4 still holds whatever the plugin left in it. The library's first access to SysBase, DOSBase, RealTimeBase, etc. through (a4) chases random memory and the CPU traps.
Fix: Every callback the library exposes to plugins must be tagged __SAVE_DS__ so SAS/C emits a prologue that loads the library's a4 from a static GOT-like cell before running any code. ff_host.c defines FF_HOST_ENTRY as __SAVE_DS__ for exactly this reason. If you write a new host service, decorate it the same way.
Plugin author rule: you never have to think about a4 yourself, but you must respect the contract: do not poke at geta4(), do not use __saveds on your own static helpers, and do not store the engine's function pointers in places (interrupt servers, raw exec lists) where they could be invoked through a path that bypasses the engine's prologue.
4.2 __CXAMEMCPY (large struct assignment)
Symptom: slink: undefined symbol __CXAMEMCPY File 'foo.o'.
Cause: SAS/C lowers dst = src; where src is a struct larger than ~16 bytes (the threshold varies by optimisation flags) to a call into its private __CXAMEMCPY helper in sc.lib. Plugins don't link sc.lib.
Fix: Replace every struct assignment over big-ish structs with CopyMem. There are three syntactic forms that all hit __CXAMEMCPY and that all need to be rewritten:
/* Don't (field-of-struct <- struct): */
buf->fb_Video = priv->myc_In;
/* Don't (array-element <- local struct - the cdxl.demux miss): */
priv->frames[count] = rec;
/* Don't (struct return from function being captured): */
priv->cc_In = MyMakeFormat();
/* Do (works for all three): */
CopyMem((APTR)&priv->myc_In, (APTR)&buf->fb_Video,
sizeof(struct FFVideoFormat));
CopyMem((APTR)&rec, (APTR)&priv->frames[count],
sizeof(struct MyFrameRec)); |
struct FFVideoFormat, struct FFAudioFormat, struct FFProbe, struct FFBuffer are all over the threshold. Anything containing nested structs almost certainly is.
The array[i] = local_struct; variant is the trickiest to spot because it doesn't look like a "copy" - it looks like a write into the array. Search for \] = after writing any new plugin to catch this one before the linker does.
4.2.1 _SetMem missing at link time
Symptom: slink: undefined symbol _SetMem File 'foo.o', often followed by __XCEXIT from sc.lib if the linker tries to satisfy the helper through the C runtime.
Cause: SAS/C does not make SetMem() a safe Exec-library primitive for these LoadSeg modules. In this build it resolves as a C helper symbol, and pulling C helper code into a no-startup module brings the same startup/exit problems as other sc.lib helpers.
Fix: Do not call SetMem() from plugins or library code. Clear small structs explicitly field-by-field, or allocate new private structs with MEMF_CLEAR when ownership/lifetime makes that natural. For the common codec setup case:
setup->cs_CodecFourCC = 0;
setup->cs_CodecTag = 0;
setup->cs_BlockAlign = 0;
setup->cs_ExtraData = NULL;
setup->cs_ExtraDataLen = 0;
setup->cs_Flags = 0; |
4.2.2 Codec setup, block metadata and raw codec IDs
Block-based and header-driven codecs need container metadata in addition to sample rate / channel count. Do not overload FFAudioFormat.af_Pad for this. The API surface for compressed-stream setup is struct FFCodecSetup:
Demuxers return it from Configure() through FFT_TrackCodecSetup.
Demuxers copy it into FFBuffer.fb_CodecSetup on compressed packets.
Codecs read fb_CodecSetup in Push() and copy any extradata they
need after the call returns.
Use FFCodecSetup.cs_BlockAlign for compressed block geometry (nBlockAlign in WAVE/AVI). Use cs_ExtraData / cs_ExtraDataLen for codec private headers such as WAVEFORMATEX extension bytes, QuickTime sample-description payloads, or RealAudio setup blocks. Leave FFAudioFormat.af_BlockAlign zero once a codec emits PCM; it exists only as audio-format metadata / compatibility fallback. af_Pad remains a format-specific spare field; for example fpadpcm.demux still uses it to carry the FP_ADPCM compressed sample width.
Some container-native codec IDs are not printable fourccs. WAVE tags carried by avi.demux use the raw form 'W' 'A' tag_hi tag_lo, so MS ADPCM (wFormatTag == 0x0002) is 0x57410002. A C string cannot hold those embedded NUL/control bytes as a normal fourcc, so plugins advertise exact raw IDs with an 8-digit handled-type string:
static CONST_STRPTR MsAdpcmCodecTypes[] = { "0x57410002", NULL }; |
FFRegistryFindCodecByFourCC() matches these 0xXXXXXXXX entries before falling back to ordinary printable fourcc strings. This avoids the old lossy behaviour where non-printable bytes were folded to ? and unrelated WAVE tags like 0x0002 and 0x0011 both looked like "WA??".
For codecs whose identity is not known until the demuxer has parsed container headers (RealAudio is the current example), fill cs_CodecFourCC as soon as the header parser learns it. Path-backed demuxers may preview their own file in Configure() to return this setup before pipeline construction; packet-backed demuxers must also put the discovered setup in fb_CodecSetup on the first emitted packet.
4.3 _UtilityBase missing at link time
Symptom: slink: undefined symbol _UtilityBase File 'foo.o'.
Cause: UTILLIB in SCOPTIONS makes SAS/C emit pragma calls into utility.library for every 32-bit multiply, divide and modulo. Those stubs read a plugin-global _UtilityBase. Without it declared, the link fails.
Fix: Declare it, refcount it, lazy-open it, and release on last close. The pattern in every plugin file:
struct Library *UtilityBase = NULL;
static UWORD MyPluginUtilityRefCount = 0;
/* In Open(): */
if (MyPluginUtilityRefCount == 0) {
UtilityBase = (struct Library *)host->fh_OpenLibrary(
(CONST_STRPTR)"utility.library", 37);
if (UtilityBase == NULL) {
return NULL;
}
}
/* ...AllocMem priv / inst... */
MyPluginUtilityRefCount++;
/* In Close(): */
if (host != NULL && MyPluginUtilityRefCount > 0) {
MyPluginUtilityRefCount--;
if (MyPluginUtilityRefCount == 0 && UtilityBase != NULL) {
host->fh_CloseLibrary((APTR)UtilityBase);
UtilityBase = NULL;
}
} |
Note that the engine's own code already opens utility.library — but the engine's UtilityBase lives in a different segment. Each plugin needs its own copy of the symbol so its own UTILLIB pragmas resolve.
4.3.1 _GetTagData / _FindTagItem / _CallHook missing at link time
Symptom: slink: undefined symbol _GetTagData File 'foo.o'.
Cause: SAS/C only inlines the jsr to a system-library entry point when the corresponding proto/xxx.h is included in the translation unit. Without #include <proto/utility.h>, a call to GetTagData() becomes a plain external function reference, which slink cannot resolve out of SC SD ND (no LIB: is on the path).
This is NOT the same as the UtilityBase / UTILLIB problem in §4.3. UTILLIB lowers compiler-generated 32-bit math (UMult32, UDivMod32, ...) and only needs UtilityBase declared. Functions you call by name in C source need the proto include too, or SAS/C emits a regular external call.
Fix: Always include the matching proto header for every system library function you call:
#include <proto/exec.h> /* AllocMem, FreeMem, CopyMem, ... */
#include <proto/dos.h> /* Open, Read, Seek, Close, FPrintf */
#include <proto/utility.h> /* GetTagData, FindTagItem, CallHook */
#include <proto/intuition.h> /* OpenScreen, OpenWindow, ... */
#include <proto/graphics.h> /* BltBitMap, InitBitMap, ... */ |
The same rule applies to any other library a plugin opens explicitly (e.g. cybergraphics.library, ahi.device, ...) — without the proto header, you'll get the same _FuncName unresolved-symbol error.
4.3.2 _FFStrEqualNoCase (and any other FF_USE_UTILITY helper) missing at link time
Symptom: slink: undefined symbol _FFStrEqualNoCase File 'foo.o'.
Cause: <fastforward/ff_string.h> only declares FFStrEqualNoCase (and any other case-folding / Stricmp-backed helper) when the translation unit defines FF_USE_UTILITY before including the header. The engine's own internal header (ff_internal.h) defines FF_USE_UTILITY so library code can call those helpers freely.
Plugins are forbidden from defining FF_USE_UTILITY. A plugin does not own its own UtilityBase (the engine does, via the host vtable), so a plugin that drags in Stricmp via FFStrEqualNoCase will link against a UtilityBase that does not exist in the plugin's segment and produce the unresolved-symbol error above. Even adding the lazy-open dance from §4.3 will not fix it, because Stricmp reads _UtilityBase directly via UTILLIB pragmas — the same dual-segment trap as the engine's own utility globals.
Fix: Plugins use only the library-base-free string helpers from <fastforward/ff_string.h>:
| Allowed in plugins |
Forbidden in plugins |
| FFStrLen |
FFStrEqualNoCase |
| FFStrEqual (case-sensitive) |
anything that hides a Stricmp call |
| FFStrCopy, FFStrCopyN |
|
| FFCopyBytes, FFFillBytes |
|
| FFBorrowString, FFCopyIntoString |
|
When a plugin really needs a case-insensitive compare (e.g. matching a file extension against a fixed literal in p_Probe), write a tiny inline ASCII fold in the plugin itself. For a single short literal this is just a handful of byte loads:
/* Match a single fixed 4-byte extension without utility.library. */
static LONG
PathHasFooExt(CONST_STRPTR path)
{
CONST_STRPTR p;
CONST_STRPTR dot;
UBYTE c0;
UBYTE c1;
UBYTE c2;
if (path == NULL) {
return 0;
}
dot = NULL;
p = path;
while (*p != '\0') {
if (*p == '.') {
dot = p;
} else if (*p == '/' || *p == ':') {
dot = NULL;
}
p++;
}
if (dot == NULL || dot[1] == 0 || dot[2] == 0 ||
dot[3] == 0 || dot[4] != 0) {
return 0;
}
c0 = (UBYTE)dot[1];
c1 = (UBYTE)dot[2];
c2 = (UBYTE)dot[3];
if (c0 >= 'A' && c0 <= 'Z') c0 = (UBYTE)(c0 + 32);
if (c1 >= 'A' && c1 <= 'Z') c1 = (UBYTE)(c1 + 32);
if (c2 >= 'A' && c2 <= 'Z') c2 = (UBYTE)(c2 + 32);
return (c0 == 'f' && c1 == 'o' && c2 == 'o') ? 1 : 0;
} |
Keep all locals at the top of the function — cdxl_demux.c's CdxlPathHasCdxlExt is the reference implementation, copy its structure when you need this in a new plugin.
The author has paid this tax twice already on the CDXL demuxer. Don't make it three.
4.3.3 utility.library not yet open during p_Probe
Symptom: a brand-new demuxer is dropped into Plugins/, and the very next time the engine tries to recognise a file that isn't already this plugin's format, the whole system locks up - no Alert, no log line, the parent task just stops responding.
Cause: p_Probe runs before p_Open. Most plugins lazy-open utility.library inside their MyOpen(), so during the probe walk the plugin's file-scope UtilityBase is still NULL. Any function that JSRs through that base - a direct UMult32(a, b), an explicit Stricmp(...), or a SAS/C UTILLIB-generated UMult32 for a * b where a and b are ULONG - will jump through address zero and take the machine down.
This bites hardest when FFRegistryFindDemux is allowed to ask every demuxer "is this yours?" - which is the architecture this engine uses (no central sniffer, see ff_sniff.c and the DataTypes-style contract in §2.3). Your plugin's probe will be called on every CDXL, AVI, MOV, ILBM, JPEG... bitmap-bearing file the user opens, not just on files whose extension already matches your format. The 0xFF byte you carefully guarded against at offset 0 is in the body of those bitmaps too.
Fix: probes are math-free and library-free. If a probe needs the same parser the demuxer uses internally, split the parser:
MpaValidateHeader(p) -> 0/-1, bit-tests + table lookups only,
no UMult32 / UDivMod32, no Stricmp, no OpenLibrary.
MpaParseHeader(p, &frameSize, ...) -> 0/-1, calls
MpaValidateHeader first then does the frame-size math. Only the demuxer's Pull path - which runs after Open - is allowed to call this one.
The MP3 demuxer (mpegaudio_demux.c) shows the pattern: every probe loop calls MpaValidateHeader, MpaPull and MpaFindSync call MpaParseHeader. Mirror it in any new audio/video demuxer whose sync verification needs more than a fixed magic string.
A second safe option is to do the same trick this engine does for its ff_string.h helpers: declare a FF_PROBE_ONLY variant of the parser that does only the parts you can guarantee at probe time. Whichever route you pick, the rule is the same:
> A probe must produce its LONG return value with zero library > JSRs and zero SAS/C-generated UTILLIB math on user-supplied > bytes. If your probe currently violates this, your plugin will > deadlock the very first time someone drops a CDXL movie on a > machine that has your demuxer installed.
4.3.4 _DOSBase missing at link time
Symptom: slink: undefined symbol _DOSBase File 'foo.o'.
Cause: the plugin calls a dos.library function — Open, Read, Write, Seek, Close, FPrintf, Flush, ... The proto/dos.h pragmas JSR through a plugin-global _DOSBase. As with UtilityBase (§4.3), a DATA=FARONLY LoadSeg plugin has no C startup to set that global up, so the link fails.
Fix: do not OpenLibrary("dos.library") yourself. The engine already has dos.library open and exposes its base through the host vtable (fh_DOSBase, alongside fh_SysBase). Declare the plugin-global and point it at the host's base in Open() — before any DOS call:
#include <proto/dos.h>
struct DosLibrary *DOSBase = NULL; /* file scope */
/* In Open(), first thing after the host NULL-check: */
if (DOSBase == NULL) {
DOSBase = (struct DosLibrary *)host->fh_DOSBase;
} |
Open() always runs before Configure()/Push()/Pull(), so a base assigned here is valid for any later file I/O (e.g. a demuxer that sniffs FFT_Path in Configure). p_Probe, which runs before Open(), must therefore do no DOS calls — same rule as §4.3.3. mkv_demux.c, flac_demux.c and rm_demux.c are reference users.
4.3.5 Floating point: exp/log10, the math-library bases, and the sc.lib startup hooks
A codec that needs real floating point (e.g. a CELP/RealAudio decoder calling exp() / log10()) trips three separate traps in turn. Solve all three or the plugin will not link cleanly.
(a) Don't pull transcendentals out of sc.lib. Linking sc.lib for exp/log10 drags the C-startup exit handler __XCEXIT into the plugin and leaves near (a4-relative) references in a DATA=FARONLY segment (slink warning 627). Route the transcendentals through mathieeedoubtrans.library instead — opened via the host like every other base — by macro-redirecting them around the vendored decoder core (the same #define trick used for malloc/memcpy):
#include <proto/mathieeedoubbas.h>
#include <proto/mathieeedoubtrans.h>
#define exp(x) IEEEDPExp((double)(x))
#define log10(x) IEEEDPLog10((double)(x))
#include "decode288.c" /* vendored core that calls exp()/log10() */
#undef log10
#undef exp |
(b) Declare the math bases with the right type. proto/mathieeedoubbas.h and proto/mathieeedoubtrans.h already declare their bases as extern struct MathIEEEBase (from <libraries/mathlibrary.h>), not struct Library . Defining them as struct Library * gives Error 72: conflict with previous declaration. Use the proto type, then lazy-open and refcount exactly like UtilityBase (§4.3):
struct MathIEEEBase *MathIeeeDoubBasBase = NULL;
struct MathIEEEBase *MathIeeeDoubTransBase = NULL;
/* In Open(), gated on the same refcount as UtilityBase: */
MathIeeeDoubBasBase = (struct MathIEEEBase *)host->fh_OpenLibrary(
(CONST_STRPTR)"mathieeedoubbas.library", 37);
MathIeeeDoubTransBase = (struct MathIEEEBase *)host->fh_OpenLibrary(
(CONST_STRPTR)"mathieeedoubtrans.library", 37);
/* ...NULL-check, close-on-failure, CloseLibrary on last Close()... */ |
(c) Stub the sc.lib startup hooks — DEFINE=@__dummy does not work here. You still link scm.lib (and sc.lib) for the ordinary software-double arithmetic the decoder uses, and sc.lib references the C-startup hooks _XCEXIT, __chkabort and _CXBRK. A LoadSeg plugin has no startup to define them. The slink redirect you will see in old Amiga makefiles —
DEFINE @__XCEXIT=@__dummy DEFINE @__chkabort=@__dummy |
— cannot resolve in an sc.lib/scm.lib-only link, because __dummy is itself only defined when amiga.lib (etc.) is linked. slink finds no symbol to alias to and prompts for __XCEXIT anyway. Instead, define empty stubs in the plugin source, exactly as FastForward's own LibInit.c and the datatype classheader.c files do. SAS/C maps the C identifier _XCEXIT to the linker symbol __XCEXIT:
#ifdef __SASC
int _XCEXIT(void) { return 0; } /* never called */
void __regargs __chkabort(void) { }
void __regargs _CXBRK(void) { }
#endif |
These are reference-only: nothing on the decode path ever calls them. This also silences slink warning 625 ("wrong math library"). With (a) the sc.lib transcendental code is never referenced, with (b) the library bases resolve, and with (c) the startup hooks resolve — no slink DEFINE redirects needed. ra288_codec.c is the reference user.
4.4 Unaligned 32-bit loads on 68000/68010
Symptom: Hard crash (address error, Suspect call to a function just called by ROM) on the first interesting frame of a codec.
Cause: Bitstream data is byte-packed. Reading 32 bits with a cast load on a 68000/68010 from an address that is not a multiple of 4 is a bus error.
/* CRASHES on odd addresses on 68000/68010: */
flag = *(ULONG *)from; |
Fix: Read big-endian bytewise with a macro:
#define get32(p) \
(((ULONG)(p)[0] << 24) | \
((ULONG)(p)[1] << 16) | \
((ULONG)(p)[2] << 8) | \
(ULONG)(p)[3]) |
This is what cvid_dec.c does for every packet header read. It costs roughly two extra cycles per long on the inner loop, which is negligible next to the cost of an inner-loop crash.
The same applies to 16-bit reads: use a get16 helper rather than (UWORD ).
Exception: data your own code wrote into an aligned buffer (AllocMem returns 8-byte-aligned memory) is safe to cast-load. The issue is only with cursor pointers into externally-supplied byte streams.
4.5 Plugin static globals are shared across Processes
Symptom: Two demux instances (one filtered to audio, one to video) appear to share an internal counter; a static initialised once stays initialised across "fresh" Open/Close cycles.
Cause: LoadSeg loads each plugin file once. Its data segment exists exactly once in memory and every Open/Close pair on the same plugin shares it. When the engine spawns a child Process to run the video chain in parallel, both the parent's and child's calls into the plugin see the same statics.
Implications:
Decoder-core state held in file-scope globals (e.g. cvid_dec.c's
yuv[3], cvidData) breaks if two streams of the same codec ever run concurrently. Today this is fine because we play one stream at a time, but a video sink that wants to run two windows would have to move the state into the priv struct.
Static counters used for "log only the first N events" do count
across all instances, which is usually what you want for debugging but occasionally surprising.
Fix when you need it: move per-instance state into your MyCodecPriv struct, not into file-scope globals. The ABI gives you fi_Private precisely so you have somewhere to put per-instance state.
4.6 Shared buffered DOS file handles
Symptom: Log lines appear duplicated; some lines are missing; the log truncates mid-line right before the process dies.
Cause: Per dos.doc/VFPrintf: "This routine is buffered." The buffer is held inside struct FileHandle. When two Processes both call FPrintf + Flush on the same BPTR, neither call locks the FH and the internal pointer / count get corrupted: one process's Flush re-emits the partial buffer the other process just installed, and eventually a write through a stale pointer faults.
Fix: serialise every write through the engine's log semaphore. The host vtable exposes two thunks for this:
void (*fh_LogLock)(struct FFHost *host);
void (*fh_LogUnlock)(struct FFHost *host); |
Both call into fastforward.library, which holds a dedicated SignalSemaphore fb_LogLock (separate from fb_Lock, so log I/O does not contend with plugin / movie list maintenance). The semaphore is recursive (Exec ObtainSemaphore), so the same task may nest Lock/Unlock pairs safely.
Wrap every FPrintf + Flush pair, or, when a single message emits several lines, take the lock once and release after the final Flush:
/* single-line message */
if (host != NULL && host->fh_LogLock != NULL) {
host->fh_LogLock(host);
}
FPrintf((BPTR)host->fh_LogOutput, "DEBUG mycodec: pkt=%lx\n", pkt);
Flush((BPTR)host->fh_LogOutput);
if (host != NULL && host->fh_LogUnlock != NULL) {
host->fh_LogUnlock(host);
} |
/* multi-line atomic burst */
host->fh_LogLock(host);
FPrintf(out, "DEBUG mycodec: header\n"); Flush(out);
FPrintf(out, "DEBUG mycodec: body line %ld\n", n); Flush(out);
host->fh_LogUnlock(host); |
Both thunks tolerate a NULL host and a NULL engine pointer, so a plugin may use the same call shape from any path (probe / open / configure / hot path) without extra guards. The in-tree plugins (mov_demux.c, video_sink.c, audio_sink.c, pcm_codec.c) package the pattern in a per-plugin *_TRACE_LOCK / *_TRACE_UNLOCK macro pair so all the existing _TRACE macros transparently lock, print, flush, unlock.
Do not call FPrintf(host->fh_LogOutput, …) without taking the lock — even from a single Process, omitting the lock leaves the next emitter free to interleave with you.
4.7 Signal-based IPC
Symptom: System deadlock. The parent task blocks forever; the child Process appears to have exited.
Cause: Wait((ULONG)task) is not a thing. Wait takes a signal mask — a 1 << n for each signal bit you want to wake on. Passing a task pointer makes the wait succeed only on the coincidence that the pointer bits match an unrelated incoming signal.
Fix: Use AllocSignal(-1) in the parent to reserve a dedicated bit; pass the resulting (1UL << bit) to the child as the "done" mask; the child calls Signal(parent, mask) just before exit; the parent calls Wait(mask) and then FreeSignal(bit).
LONG doneSig;
ULONG doneMask;
struct Task *parent;
doneSig = AllocSignal(-1);
if (doneSig < 0) {
return /* fallback */;
}
doneMask = 1UL << (ULONG)doneSig;
parent = FindTask(NULL);
/* ... pass parent + doneMask to child via tc_UserData or your ctx ... */
/* ... start child ... */
/* parent waits: */
Wait(doneMask);
FreeSignal(doneSig); |
A FreeSignal(-1) is a no-op from V37+ so it is safe to defer the cleanup to a generic teardown path.
4.8 Atom / chunk walkers and overflow
Symptom: A malformed input file hangs the player instead of returning an error. The trace shows the demuxer cycling through atoms forever.
Cause: pos += atomsize; where atomsize came from the file and the file is malicious / corrupted. If pos + atomsize overflows ULONG, pos becomes a small number again and the bounds check pos + 8 <= filesize keeps the loop alive.
Fix: before every increment, check both that atomsize is at least the header size and that adding atomsize to pos does not overflow the buffer:
if (atomsize < 8 || atomsize > limit - current_pos) {
break;
}
current_pos += atomsize; |
Subtracting current_pos from limit first avoids ever computing the overflowing sum.
4.9 BPTR vs APTR confusion
A BPTR is a BCPL pointer — the address shifted right by 2. AmigaDOS functions return BPTRs (Open, Output, Input). exec functions deal in APTRs.
Rules:
Treat a BPTR as opaque. Do not arithmetic on it.
0 is the BPTR equivalent of NULL — that's how Open signals
failure.
The conversion macros (BADDR(bptr), MKBADDR(aptr)) exist if you
really need to fish into the underlying struct, but day-to-day plugin code should not need them.
When you cache a BPTR in your priv struct, type it as BPTR. When you pass it through an APTR-typed field (like base->fb_LogOutput), cast on use with (BPTR).
4.10 Test for the things the user will actually do
The crashes in our log were not the spec-compliant test files in testfiles/. They were arbitrary MOVs from the internet that did things the spec lets you do but in obscure orderings:
interview.mov: a wide atom with no payload, then moov before
mdat.
B5_battle2.mov: an stsd sample description whose width /
height fields are at an offset that varies by codec FourCC.
EpisodioII.mov: a perfectly valid rpza codec we hadn't
implemented.
A demuxer that only handles "easy" files is a demuxer that crashes on real media. Test with whatever you can find.
5. Hyper-optimised hot paths
This is the part of your plugin that runs once per sample, per block, or per pixel. Memory-safety helpers are not on the table here. The rules flip:
5.1 Allocate nothing in the inner loop
AllocMem takes a global Forbid in exec. One bad-luck allocation sprint in a packed Push / Pull loop can starve every other task on the machine. The acceptable strategies, in order of preference:
1. Allocate everything in Configure once the format is known.
Width × height × 3 bytes for an RGB frame, ring of ringSize for an audio output buffer, etc.
2. Re-use one buffer. Reuse the same scratch across frames as long
as the dimensions don't change. Only fh_Free + fh_Alloc when the dimensions actually change.
3. Use fh_AllocBuffer / fh_FreeBuffer. These come from a
per-host pool that does not contend with global exec.
What you must not do: call fh_Alloc once per output sample. The allocation cost will dominate the decode cost.
5.2 Hand-write the inner loop
Once you are inside the per-pixel / per-sample loop, every cycle counts.
Unroll by the natural block size. Cinepak is 2×2-pixel; SVQ1 is
16×16; YUV→RGB is 1×1 but a 2×2 group lets you share U/V loads.
Cache the row stride in a register-typed local. SAS/C will keep
it in a register if you don't address-of it.
Avoid signed division and modulo. >> 1 instead of / 2,
& (n - 1) instead of % n when n is a power of two.
Bytewise-read packed bitstreams (see 4.4). Cast-loads are not
worth the cycles you save.
Don't shave NULL checks you have already done in Configure. If
the buffer was allocated in Configure and is set to NULL in Close, the inner loop can assume it's non-NULL between those two.
5.3 Use the C2P helper for AGA / OCS
If your sink converts chunky pixels to planar bitmap for display on AGA or OCS, do not roll your own C2P loop. The engine ships a shared, highly tuned C2P helper:
ctx = host->fh_C2PCreate(host, ctags);
host->fh_C2PRender(host, ctx, &req);
host->fh_C2PDispose(host, ctx); |
It keeps a single 2 KiB bitspread table in Fast RAM that is shared across every caller, caches per-context plane pointers, and amortises inserts so the inner loop runs at 32 pixels per iteration. Rolling your own will be slower and will fight with anyone else doing C2P at the same time for the same table.
5.4 Skip frames the audio clock says are late
The video sink is responsible for keeping itself in sync with the audio clock, not the other way round. The pattern from video_sink.c:
now = host->fh_GetTime(inst->fi_Clock);
deadline = buf->fb_Timestamp + priv->vs_FramePeriod;
if (now > deadline) {
host->fh_BumpDropped(host, 1);
return 0; /* drop, don't render */
} |
Dropping a late frame is always cheaper than rendering it; the user will notice the audio drift before they notice the lost frame.
5.5 Handle FFBF_EOF first
Every Push / Pull callback must handle EOF before format validation:
static LONG FF_PLUGIN_ENTRY
MyPush(struct FFInstance *inst, struct FFBuffer *buf)
{
if (inst == NULL || inst->fi_Private == NULL || buf == NULL) {
return -1;
}
if ((buf->fb_Flags & FFBF_EOF) != 0) {
/* flush remaining state and propagate the EOF tag */
return 0;
}
/* ... normal decode path ... */
} |
Forgetting this is what makes a pipeline hang on the last frame: the codec keeps refusing the EOF marker, the engine keeps re-pushing it, nobody makes progress.
6. Diagnostics & debug logging
Plugin-side traces all go through the same path:
if (debug && host->fh_LogOutput != 0) {
/* ... lock the log (section 4.6) ... */
FPrintf((BPTR)host->fh_LogOutput,
"DEBUG mycodec: Push w=%lu h=%lu sz=%lu\n",
(ULONG)w, (ULONG)h, buf->fb_Size);
Flush((BPTR)host->fh_LogOutput);
/* ... unlock the log ... */
} |
Rules:
1. Always Flush after FPrintf. A crashing pipeline must not eat
the line that says what crashed it.
2. Always check host->fh_LogOutput != 0. It's zero when the host
has not raised the log level.
3. Cast to (BPTR) at the call site. The host stores it as APTR
so the public header doesn't need DOS types.
4. Log first the things you might be wrong about. The format
parameters the engine handed you, not the values your math computed.
5. Rate-limit hot-path traces. MovPull emits the first three
"Pull ok" lines and then one every 1024 calls. A trace per sample will both flood the log and dominate the run time.
For Probe, never log "unknown MIME". Probe is called dozens of times per file as the engine narrows down the right plugin; logging at INFO level will swamp the user's shell. Use FFLOG_TRACE or stay silent.
7. Versioning and the ABI
<libraries/ffplugin.h> defines FFPLUGIN_VERSION and FFENGINE_VERSION. Both must be the values of the header you compiled against. Rules:
1. Never edit the values manually. Recompile the plugin if the
header changes.
2. **Engine refuses to load plugins whose p_EngineVersion is
greater than its own.** Plugins built against a newer ABI can't call into an older engine.
3. Additive-only. New FFHost fields are appended; old ones never
move. A plugin compiled against v3 still works on a v6 engine because the layout up to v3 is preserved.
If you add fields to MyCodecPriv between releases, that's fine: the priv struct is engine-opaque. If you add fields the engine reads (e.g. you write a new buffer type), they have to be added to the shared header and the version bumped.
8. Quick reference
Allocators
ptr = host->fh_Alloc(size, MEMF_CLEAR | MEMF_ANY);
host->fh_Free(ptr, size); /* same size you allocated */
ptr = AllocMem(size, MEMF_CLEAR | MEMF_ANY); /* same size on free */
FreeMem(ptr, size);
vec = AllocVec(size, flags);
FreeVec(vec); |
File I/O
fh = Open(path, MODE_OLDFILE); /* 0 on failure, IoErr() has reason */
n = Read(fh, buf, len); /* >0 ok, 0 EOF, -1 error */
n = Seek(fh, pos, mode); /* returns previous position */
Close(fh);
fib = AllocDosObject(DOS_FIB, NULL);
ok = ExamineFH(fh, fib); /* fib->fib_Size after */
FreeDosObject(DOS_FIB, fib); |
Tasks & signals
sig = AllocSignal(-1); /* -1 = any, returns -1 on fail */
mask = 1UL << (ULONG)sig;
self = FindTask(NULL);
Signal(target, mask); /* from any context */
got = Wait(mask); /* blocks current task */
FreeSignal(sig); /* -1 harmless in V37+ */ |
Memory copies / fills
CopyMem(src, dst, n); /* src, dst, size (A0/A1/D0) */
FFCopyBytes(src, dst, n); /* NULL-guarded wrapper */
FFFillBytes(dst, n, value); /* byte fill */
/* Use MEMF_CLEAR instead of zero-filling after AllocMem. */ |
Strings
len = FFStrLen(s);
isEq = FFStrEqual(a, b);
FFStrCopy(dst, src); /* dst large enough for src + NUL */
FFStrCopyN(dst, src, dstCap); /* always NUL-terminates */ |
Counted buffers
s = FFNewStringAlloc(cap, FFENC_ASCII);
FFCopyIntoString(s, src, srclen);
FFDisposeStringAlloc(s);
d = FFNewDataAlloc(cap);
FFCopyIntoData(d, src, srclen);
FFDisposeDataAlloc(d); |
Libraries
base = host->fh_OpenLibrary("foo.library", 37);
host->fh_CloseLibrary((APTR)base); |
Logging (until v7 host hook lands)
if (host->fh_LogOutput != 0) {
/* obtain lock */
FPrintf((BPTR)host->fh_LogOutput, "DEBUG ...: ...\n");
Flush((BPTR)host->fh_LogOutput);
/* release lock */
} |
9. Checklist before you commit
[ ] Every host->fh_Alloc / AllocMem is NULL-checked.
[ ] Every allocation has a matching free with the same size.
[ ] MEMF_CLEAR is set on every buffer the decoder reads before
fully writing.
[ ] No memcpy, memset, strcpy, strlen, strcmp anywhere.
[ ] No struct-of-struct assignment (a = b; for big b).
[ ] No FFStrEqualNoCase (or other FF_USE_UTILITY-gated helper)
called from a plugin TU — write an inline ASCII fold (§4.3.2).
[ ] No #define FF_USE_UTILITY in a plugin TU.
[ ] _UtilityBase is declared, refcounted and lazy-opened.
[ ] If the plugin calls dos.library, DOSBase is set from
host->fh_DOSBase in Open() — not opened by hand (§4.3.4).
[ ] Any proto/xxx.h system call has its proto header included, or
slink reports _FuncName unresolved (§4.3.1).
[ ] Floating-point codecs route exp/log10 through
mathieeedoubtrans.library, declare the math bases as struct MathIEEEBase (not struct Library ), and stub _XCEXIT / __chkabort / _CXBRK in source (§4.3.5).
[ ] p_Probe makes ZERO library calls (no UMult32 / UDivMod32
/ Stricmp / OpenLibrary / anything) — UtilityBase is still NULL at probe time (§4.3.3).
[ ] Every callback exported to the engine has FF_PLUGIN_ENTRY.
[ ] Bitstream cursors use get16 / get32 macros, not casts.
[ ] Atom / chunk walkers check atomsize > limit - pos before
pos += atomsize.
[ ] Push / Pull handle FFBF_EOF before format validation.
[ ] Inner loops do not allocate.
[ ] No file-scope statics carry per-instance state.
[ ] Log calls cast to (BPTR) and Flush after every FPrintf.
[ ] Plugin builds cleanly with the project SCOPTIONS — no warnings.
If you can answer "yes" to every box, the plugin you wrote will not reproduce any of the categories of crash we shipped to master and then chased down across the MOV bring-up.
See the Developer’s Guide for the application Movie API. Return to the FastForward Overview.
|
Back to top
|
|