Fuse (the Free Unix Spectrum Emulator) is a C codebase targeting multiple Unix
platforms plus macOS and Windows. It emulates ZX Spectrum hardware. The
canonical style guide is in hacking/coding_style.txt.
Fuse uses GNU Autotools. A full build from scratch:
./autogen.sh # Run once after cloning; generates ./configure
./configure # Select UI backend, peripherals, etc.
make # Build the fuse binaryCommon ./configure flags:
| Flag | Effect |
|---|---|
--with-sdl |
SDL UI (easiest on macOS) |
--with-gtk |
GTK+ UI |
--without-gtk --without-x |
Headless/null UI |
--prefix=/usr/local |
Installation prefix |
The macOS Cocoa port now lives in the top-level sibling directory ../fusepb/
and has its own Fuse.xcodeproj; the wrapper repo drives normal builds from
its top-level Makefile, which in turn invokes the Xcode project and
regenerates Perl-derived source as needed.
Several source files are generated by Perl scripts at build time and must not be edited by hand:
settings.c/settings.h/options.h— fromsettings.dat,ui/options.datz80/opcodes_base.c,z80/z80_cb.c,z80/z80_ddfd.c,z80/z80_ddfdcb.c,z80/z80_ed.c— from the corresponding.datfiles viaz80/z80.pl
If you need to change settings or Z80 opcode behaviour, edit the .dat /
.pl source, then re-run make.
make testBuilds z80/coretest, runs it, and diffs output against
z80/tests/tests.expected. This is a standalone binary that does not require
the full emulator to be running.
./fuse --unittestsUnit tests are compiled into the main fuse binary. They cover ULA
contention, floating bus, memory pool, all machine memory paging, and all
peripheral port mappings. There is no separate test binary for these.
There is no built-in test filter. To exercise a specific area:
- Identify the relevant
static int foo_test( void )function inunittests/unittests.c. - Temporarily call only that function from
unittests_run()and rebuild. - Run
./fuse --unittests.
See hacking/peripheral_tests.txt for step-by-step instructions on manually
verifying disk/tape/peripheral behaviour under a running emulator.
Fuse has no CI linter. The formatter of record is Uncrustify:
uncrustify -c hacking/uncrustify.cfg --replace myfile.cGNU Indent is also documented as an alternative:
indent -br -ce -prs -npcs -nsaf -nsai -nsaw -nsob input.c -o output.c
# Fix known indent quirks:
sed -i 's/switch (/switch(/g' output.c
sed -i 's/( )/()/g' output.cDo not reformat existing files wholesale; apply formatting only to new or substantially rewritten code.
All rules come from hacking/coding_style.txt. Key points:
- C, targeting C89 compatibility (no C99/C11 features without a configure
guard). Objective-C is used only in the top-level
fusepb/Cocoa port. - Perl is used for code generation only.
- 2 spaces per indent level; no tabs.
- Space after
(and before)in function calls when there are arguments:foo( a, b )— butfoo()with no arguments has no inner spaces. - Spaces around binary operators; no space for
++/--. - Array subscripts take spaces unless the index is very short:
array[ idx ]butarray[i]is acceptable. - Extra spaces to align related assignments vertically are encouraged.
- Opening brace on the same line as the control construct:
if( condition ) { - Cuddled
else:} else { - Exception: function definition opening brace goes on its own line.
static int
my_function( int arg1, const char *arg2 )
{
/* body */
}Return type on its own line; function name starts at column 1.
| Kind | Convention |
|---|---|
| Variables, functions | all_lowercase_with_underscores |
#define macros, enum constants |
IN_UPPERCASE_WITH_UNDERSCORES |
| Header guards | #ifndef FUSE_<FILENAME>_H |
| Struct typedefs | lower_case_t (e.g. fuse_machine_info) |
- C-style only:
/* ... */. C++//comments are forbidden. - Multi-line comments use the block style:
/* First line continuation */
- Copyright header in every file; GPL v2 boilerplate.
Every .c file must start with:
#include <config.h>Then system headers (<stdio.h>, <stdlib.h>, etc.), then third-party
headers (<libspectrum.h>), then local headers ("fuse.h", "module.h",
etc.). Local headers use relative paths from the source root.
Functions return int: 0 = success, non-zero = error code. The canonical
pattern is:
int error;
error = some_function( arg );
if( error ) return error;Do not use exceptions or abort() for recoverable errors. ui_error() is
used to report errors to the user interface before returning an error code.
Inside unittests/unittests.c, use the TEST_ASSERT macro:
TEST_ASSERT( condition );It prints file, line, and expression on failure and increments the failure
count. Test functions return int (0 = pass, >0 = number of failures).
| Concept | Location | Notes |
|---|---|---|
| Startup/teardown ordering | infrastructure/startup_manager.c |
Topological-sort dependency registration; call startup_manager_register() per module |
| Module hooks (reset, ROM-CS, snapshot) | module.c / module.h |
Broadcast to all registered modules |
| Peripheral I/O | periph.c / periph.h |
Register read/write handlers with address mask+value pairs |
| Event queue | event.c / event.h |
Priority queue of T-state–timestamped future events |
| Z80 registers | z80/z80_macros.h |
Endian-aware union; use macros (A, BC, HL, …) not raw struct members |
| Display pipeline | display.c → uidisplay_area() |
Dirty-rectangle tracking; UI backends implement uidisplay_* |
| Machine abstraction | machines/ |
Each machine sets up memory map, contention table, and spectrum_* callbacks |
The emulator runs on a background thread via -[Emulator connectWithPorts:]
in ../fusepb/models/Emulator.m. All AppKit and OpenGL operations must be
dispatched to the main thread or macOS will hard-crash (this enforcement became
strict on macOS 26+).
/* Singleton lazy init — use dispatch_sync so the caller gets the instance */
+ (MyController *)singleton
{
static MyController *instance = nil;
if( !instance ) {
if( [NSThread isMainThread] ) {
instance = [[MyController alloc] init];
} else {
dispatch_sync( dispatch_get_main_queue(), ^{
instance = [[MyController alloc] init];
});
}
}
return instance;
}
/* ObjC call that must block the emulator thread until done */
[obj performSelectorOnMainThread:@selector(myMethod:)
withObject:arg
waitUntilDone:YES];
/* Fire-and-forget UI update */
dispatch_async( dispatch_get_main_queue(), ^{
/* AppKit call here */
});performSelectorOnMainThread:withObject: takes an id, so raw C pointers must
be boxed:
/* Sender side */
NSValue *val = [NSValue valueWithPointer:myPtr];
[obj performSelectorOnMainThread:@selector(myMethodWithValue:)
withObject:val
waitUntilDone:YES];
/* Receiver side */
- (void)myMethodWithValue:(NSValue *)val
{
MyStruct *ptr = [val pointerValue];
/* use ptr */
}Build command (run from the wrapper repo root):
make fuseFor direct Xcode builds, open ../fusepb/Fuse.xcodeproj. The wrapper repo's
Makefile is the preferred maintainer entry point because it passes the repo
root/dependency paths explicitly and performs the post-build re-sign steps.
config.h and libspectrum.h are not in the fuse root; they are
generated at build time and placed in ../fusepb/:
../fusepb/config.h— autoconf output../fusepb/libspectrum.h— generated by../fusepb/generate.pl
The Xcode project now stages managed dependencies under ../fusepb/deps/ and
builds plugin source directly from ../fusepb/plugins/.
fuse.c / fuse.h Main entry point and global state
machines/ Per-machine emulation (48K, 128K, +2, +3, …)
peripherals/ All peripheral emulations (ULA, AY, disk, IDE, NIC)
z80/ Z80 core + Perl-generated opcode tables
ui/ UI backends (SDL, GTK, Xlib, Cocoa, null, …)
debugger/ Built-in debugger (Lex/Yacc command parser)
sound/ Sound output backends
unittests/ Unit test framework and tests
infrastructure/ Startup manager
hacking/ Developer docs: coding style, formatter config, notes
perl/ Shared Perl modules used by code generators
roms/ ZX Spectrum ROM binaries
Related wrapper-repo directories live one level up:
../fusepb/ Native macOS Cocoa port (Xcode project)
../fusepb/plugins/ In-tree Quick Look + Spotlight plugin source
../fusepb/deps/ Staged managed dependency sources