Reference: the Project file
A run takes one .ngp file and nothing else. The file says which Modules the
program has, what machine it runs on, and what the tool builds it into. No
.asm or .ngc file carries any of that.
This chapter lists every block and every option. The chapters before it teach the ideas. This one is for looking things up.
The command line
nga main.ngp -o game.xex --map game.map
-o names the file to write, and --map a file for the memory map.
--deny, --allow and --off change a finding's severity, as the
diagnostics block does. nga --help lists the rest.
--explain is for a build that ends in NGA5206, no layout satisfies every
constraint. The tool asks the solver once more and names the fewest pairs of
Sections that cannot be kept apart, one NGA5207 for each pair. Making room for
one Section of each pair is the fix.
How the tool reads the file
Line endings mean nothing. Every construct ends because its own shape is complete. Layout is free.
A block may appear more than once. The occurrences sum. Order does not matter, and a block may name something declared later or in a file included later.
Nothing is a keyword. The tool reads a block's name and every word inside it by position.
Lists are comma-separated: needs a, b and resident { a, b }. The modules
block has no commas, because each entry has a structure of its own.
A value is an expression in the assembler's expression language. Write $4000
here as you write it in source. The Project has no Symbols, so a name in an
expression is an error.
Comments start with ; and run to the end of the line.
Two entries that contradict each other are an error. The last one does not win.
This keeps the result independent of the order of your include lines.
modules
modules {
"src/intro.asm"
"gfx/tiles.asm" as gfxTiles
binary( "gfx/font.bin", align = 1024 )
}
Each entry is a file path or a generator call. The Module takes its name from
the file stem. Use as NAME to give it another name.
Two Modules with the same name are an error. The message proposes as.
A path is relative to the file that holds it. Use / on every platform. A path
that names no file there names one in the library.
A path is a plain string. It may hold non-ASCII characters. It may not carry a character set prefix, because nothing translates it.
A .ngc path is a Module in the C subset. The tool compiles it before it
assembles it.
A Project with no Modules is an error.
A generator call writes a Module instead of reading one. See
Generators. One argument stands without a name and is the
file the generator works from. A name with a value is an option, such as
align = 1024. A name on its own is a flag, such as root.
A name before the brace makes the block a group.
group
modules engine { "engine/ball.asm" "engine/draw.asm" }
modules sfx { "engine/beep.asm" }
group play { engine, sfx }
phase level1 { needs play, one then level2 }
A group is a name for a list of Modules. It stands wherever a list of Modules
stands, which is needs and resident. It means every Module it holds, each
one once.
Two blocks declare a group. A named modules block is a group of the Modules
it declares. A group block lists names, never paths.
A group may name another group to any depth. A group that reaches itself is an error.
One name for both a Module and a group is an error. A group may not stand in a
transform entry, which names one Module.
A group that some list named and that holds no Module is a warning.
phase
phase intro { needs intro, hud then game }
phase game { needs game, hud then boss, intro entry gameStart }
needs lists the Modules the Phase requires in memory. A name may be a group.
then lists the Phases a Transition may lead to.
Both may repeat inside a block and sum. Two phase blocks with one name sum.
A Module's Residency is the set of Phases whose needs name it. Its Sections
inherit that Residency. Nothing else declares Residency.
A Module that no Phase names is never in memory. That is a warning.
A name in needs that is no Module is an error. A name in then that is no
Phase is an error.
entry NAME names the Label the Phase starts at. Write one.start for a Label
in a Namespace. Without entry, the tool looks for a Label named entry. The
name must have exactly one definition among the Modules the Phase needs. It may
be private. It must be a Label.
without NAME takes a Module out of the Phase that resident put in. It works
only for a ROM the driver switches out, such as osrom on the 130XE:
phase deep { needs deep without osrom, mathpack }
While deep runs, the driver has the OS ROM switched out, and the RAM under it
belongs to the Phase. Code in deep cannot call the OS, because the OS is not in
the Phase. The program also has to look after the interrupt vectors at $FFFA,
which are in that RAM now.
The driver says which Modules it can switch out. Any other name after without
is an error, and so is the entry Phase of a .xex doing without anything, since
its DOS loads it with every ROM in.
final marks a Phase the program never leaves: it has no then, and its entry
never returns.
phase game { needs game without osrom, mathpack entry gameStart final }
The tool then keeps out of game what runs only when a program leaves a Phase
or returns: the code that ends the program, and a driver that only switches
ROMs. A game that loops in its last Phase gets that memory back. A final
Phase with then is an error, and so is one whose entry may return.
A Phase that no path from the entry reaches is a warning. An edge that no
.transition takes is also a warning.
resident
resident { hud, music }
Names the Modules present in every Phase. This includes Phases declared later and Phases in files included later.
A name may be a group. A name that is neither a Module nor a group is an error.
There is no way to name a subset of Phases other than listing them. A Phase can
leave out of that only a ROM the driver switches, with without.
load
load { needs osram, dos, fade init fadeStart }
Describes what happens while the DOS loads a .xex, before the program starts.
A Module named in needs that holds only pinned Sections with no bytes says
where the machine's memory is during the load: the OS's variables, the DOS's
code and buffers. Nothing the file writes while it loads goes there: no Section
with bytes, and none of the code the tool runs during the load. A buffer with
no bytes may go there, because the DOS is done with the memory by the time the
program writes the buffer. A Section you pin there with bytes, such as a write
to INITAD, is allowed with a warning.
Any other Module named in needs is code of your own that runs during the
load. init names a Label in it, and the .xex calls that Label through
INITAD before it writes anything else. Several inits are called in order.
The tool places this code in memory that is free during the load and writes the
rest of the file after it, so the rest may go over it once it has run. Such a
Module belongs to no Phase.
The library's atari/800xl.ngp already lists the OS's memory as resident,
which covers the load too. A Project that drops it, to give a Phase without the
OS that memory, names it in load instead.
entry
entry intro
Names the Phase the program starts in. Required when the document declares a
phase. Give it at most once.
A Project with no phase block
The Project has one Phase. It is unnamed and holds every Module. It needs no
entry.
target
The machine. Write this in a machine variant and include the variant, because the block states hardware truth. The tool ships variants in the library:
; An Atari 130XE: five Banks as a unit set — the four of extended memory and,
; last, the machine's own memory under the Window — the Window PORTB brings
; one of them into, every Region of the address space, and the Modules a
; program on this machine needs. It declares no storage. A program that loads
; anything during a run includes `atari/extension.ngp` beside this, which makes
; the Banks its storage. What PORTB takes to show a state is the
; driver's and not here, and the OS is two Modules rather than a Region because
; it occupies memory for a while rather than being a truth about addresses: its
; variables for the whole of a program, its ROM in every Phase that does not
; say `without osrom` — see 0229. The same register switches the ROMs, so the
; driver is what declares them.
target {
cpu "6502"
; DOS loads a `.xex`, and a Project naming any other Container is refused
; here rather than at the writer.
containers xex
; No Window has a base, so the memory under this one is a Bank like the
; others and nothing of the program stands there but a Pane or storage — see
; docs/decisions/0229-no-window-has-a-base.md.
units extension 5
window ext $4000 .. $7FFF views extension
}
target {
region ram $0000 .. $CFFF ram
region $D800 .. $FFFF ram
region stack $0100 .. $01FF reserved
region io $D000 .. $D7FF register
; The RAM the OS ROM covers, named so that a Section may say `in underOsLow`
; and stand there: only in a Phase without `osrom`, or hidden under it where
; nothing needs it — see docs/decisions/0243-a-section-may-name-the-region-it-stands-in.md.
region underOsLow $C000 .. $CFFF ram
region underOsHigh $D800 .. $FFFF ram
; This machine takes a `.xex` and nothing else, and a `.xex` is loaded by a
; DOS, which stays where a DOS of the 2.x family stays. A Project whose DOS
; takes more says so with a `reserved` Region of its own, which narrows this
; one further.
region dos $0700 .. $1FFF reserved
; What the driver writes. A Project that wants more of the hardware by name
; declares it, as it declares the memory its DOS holds.
register PORTB $D301
; The hardware that holds a Section while a program has handed it one, by
; `.root NAME` before the taking of its address, until it hands another or
; `.release NAME`: what it holds stays where it is for that long. See
; docs/decisions/0247-the-hardware-holds-a-section-for-as-long-as-it-is-given-it.md.
holder dlist ; ANTIC's display list: SDLSTL, or DLISTL
holder dli ; the display list interrupt: VDSLST
holder vbi ; the immediate vertical blank: VVBLKI
holder vbd ; the deferred vertical blank: VVBLKD
holder nmi ; $FFFA, where the OS ROM is switched out
holder irq ; $FFFE, where the OS ROM is switched out
holder chbase ; the character set: CHBAS, or CHBASE
holder pmbase ; the player-missile memory: PMBASE
holder dosini ; DOSINI: called once a boot returns, and on every reset
}
; The OS and the driver, listed as a file of the Project's own would be. The
; driver is here and not beside the storage because it does more than read it:
; it shows a Pane's Bank and switches the ROMs, with or without storage. BASIC
; is not listed: a program that wants it lists `atari/basic.asm` and names it
; in a Phase's `needs`, and the driver switches it out everywhere else.
modules {
"atari/osram.asm" "atari/osrom.asm" "atari/mathpack.asm"
"atari/charsets.asm" "atari/portb.asm"
}
resident { osram, osrom, mathpack, charsets, portb }
A Project adds its own target block for what the variant cannot know. A DOS
that takes more low memory than the variant leaves it is one line:
target { region $0700 .. $2FFF reserved }
Blocks sum, so this narrows the variant's Regions rather than replacing them.
The Region has no name, because the variant already names dos and one name
names one Region.
region
region ram $0000 .. $CFFF ram
region stack $0100 .. $01FF reserved
region io $D000 .. $D7FF register
A range with one property. Both ends are inclusive. The name is optional.
| Property | Meaning |
|---|---|
ram |
the solver allocates here |
rom |
the solver allocates here only what no code writes, such as a cartridge's fixed part. See Cartridges. |
reserved |
the solver does not allocate here. A pin here is a warning. |
register |
nothing may stand here. A pin here is an error. |
Where two Regions overlap, the more restrictive property wins. A Project can narrow the variant's memory. A Project cannot turn a register into RAM.
A named Region gives every Module a Symbol of kind Region. Its value is the
start address. A Module that defines the same name is an error.
A Region must run from a lower address to a higher one. One name names one Region.
With no region and no register anywhere, the Target keeps two stand-in ram
Regions, $0080-$008F and $2000-$9FFF. The first Region you declare replaces
the stand-in.
register
register COLBK $D01A
register AUDF1 $D200, 2
A named register Region of one byte, or of the width after the comma. The
width is 1 or 2.
units
units extension 5
Declares a unit set of N Banks. N is between 1 and 65536, because a unit's number is two bytes at most.
The name gives every Module a Symbol whose value is the count.
The block says how many Banks there are and nothing else. What the hardware calls each one belongs to the driver.
units sdram 2047 linear $4000 .. $1FFFFFF
linear BEGIN .. END says where the Banks lie in the memory another master,
such as a blitter, reads as one range. Bank 0 starts at BEGIN, each next Bank
starts a Window's size further on, and the last must end by END. Both ends are
inclusive. This is MariaCEL's, whose blocks are 16 KB.
A Section in a Pane of such a set then has a linear address, read with
linearSectionAddress, and a Section may
stand in the set itself, across its Banks.
window
window ext $4000 .. $7FFF views extension
window cel4000 $4000 .. $7FFF views main, sprlist, textab, derived, stripmap, strip
The first is the 130XE's, and the second MariaCEL's, whose states are the blitter's blocks.
Declares a Window: the ranges the hardware switches together, and the states it can show.
Each name after views is a unit set of the target, or a named state of the
hardware. A unit set contributes one state per Bank. List each name once.
No state is held when nothing has switched: what a Window shows outside a
.with is undefined. So a Window's ranges are in no pool, and only a Pane's
Sections and storage stand there. base, which once named such a state, is an
error (NGA1242).
The tool numbers the states from zero in the order written, and a set's Banks in their own order. The driver receives a state by that number.
A Window names no register. Showing a state is the driver's work, and a driver shows every Window the target declares.
Region names, unit set names and Window names share one name space.
cpu
cpu "65sc02"
The processor the machine has. Write "6502" or "65sc02". A Target that says
nothing is a 6502. You quote the name because it starts with a digit.
"65sc02" adds the (zp) addressing modes, stz, trb, tsb, inc and dec of A,
phx, phy, plx, ply, bit immediate and indexed, bra, and
jmp (abs,x).
It does not add the Rockwell bit instructions, wai or stp. The 65SC02 does
not have them either.
An instruction the Target's processor lacks is an error. The message names the instruction, the processor it needs, and the processor the Target is. See Instructions.
containers
containers xex, atr
What the machine takes. Write this in a variant. A Project that names a Container the Target does not offer is an error.
Where no Target says anything, the Project's word stands.
storage
storage { units extension } ; the Banks of a unit set
storage { units 720 size 128 } ; units by number, such as a disk
What a Transition loads from.
units NAME takes the Banks of the unit set the target declares under that
name. The size of one unit is the size of the Window that shows the set. Do not
write size beside it.
units N declares N units known by their numbers, and needs one size. The
size is between one byte and the address space.
Declare units once. A storage holds at most 65536 units, because a unit's number is at most two bytes.
Storage is one space of units × size bytes. Every Section a Transition loads
has a Payload packed into it. An image may run from one unit into the next. A
Payload with no room left is an error.
With no storage block there is no storage. Then nothing waits anywhere: a
Section with bytes is written when the program loads and stays in memory for as
long as a Phase still to come needs it, and a Transition loads nothing.
So a Phase may read and write the bytes of a Phase it leads to, such as a boot Phase patching the game's code before it enters the game. It may not call that code: the code would run in a Phase where what it uses is not there. A routine the boot Phase calls belongs in a Module the boot Phase needs.
A machine variant declares no storage. A library file beside it does, such as
atari/disk.ngp for an 800XL's diskette or atari/extension.ngp for a
130XE's Banks, and a Project includes it when something waits there.
panes
panes in ext { level, tables[8] }
panes in cel4000 = sprlist { sprlist }
A Pane is a named set of Sections that one switch shows together. See Panes. The block names the Window its Panes are in.
The solver gives each Pane one state of the Window for the whole run. It picks a Bank of the unit set the Window shows.
Write = STATE in the header to pin the Panes to a named state. The solver then
has no say. A Window that shows no unit set holds only pinned Panes.
NAME[N] declares a family: N Panes of one layout on consecutive Banks. N is
between 1 and 255. Every Section in the family stands in each member at one
address. A pinned Pane is one state, so it cannot be a family.
A Pane whose Sections come to more than a Bank holds is an error. So is a family wider than its set.
A Pane's name gives every Module a Symbol of kind Pane. Its value is the index
of the state the solver gave it.
transform
transform rle { music.notes, levels.tiles }
Names the Sections whose Payload the tool decodes on the way in. The tool
copies a Section that appears in no transform block.
The unit is the Section, not the Module. A Section has no name of its own, so
name it by a Label of it: module.label, or module.space.label for a Label in
a Namespace. Any Label of the Section names it.
The tool finds the Label whether you exported it or not.
The transform must be a format the tool encodes, and some Module must declare a
decoder for it with .transform. That Module must have a Label of that name.
A group may not stand here. A Section that no Transition loads has nothing to transform, which is a warning.
container
container xex
What the tool builds the program into.
| Name | Result |
|---|---|
raw |
the raw image. This is the default. |
xex |
an Atari DOS binary |
atr |
a bootable double-density Atari diskette |
car |
an Atari cartridge image. See Cartridges. |
bs93 |
a Lynx program an emulator copies into memory and enters |
lnx |
a Lynx cartridge, with the header a reader shows |
lyx |
a Lynx cartridge without the header |
prg |
a Commodore PET program, started by a line of BASIC |
d64 |
a prg on a 1541 or 4040 diskette |
d80 |
a prg on an 8050 diskette |
d82 |
a prg on an 8250 diskette |
Give it at most once. A Container constrains Place, so it belongs here. The -o
option names a path and chooses nothing.
A cartridge names its board after the Container, quoted:
container car "xegs128"
container lnx "64k"
car takes 8k, 16k, or one of xegs32, xegs64 and so on up to
xegs1024. lnx and lyx take 64k, 128k, 256k or 512k, the board's
size, which sets its page size. A board is required there, and every other
Container takes none. The tool checks the board against the machine's Regions
and Windows.
A .xex can be compressed:
container xex compress zx0
Each segment of the file that gets smaller is stored in that format. While the
file loads, the tool's decoder unpacks it at the addresses it belongs at. A
segment that does not get smaller is stored as it is. Once the file has loaded,
memory holds what it would without compression, so the program needs no
changes. The format may be zx0 or rle. Only a .xex takes compress.
To keep some Modules uncompressed, list them, or their groups, after except:
container xex compress zx0 except { loadingPic }
They are stored as they are and arrive when they would without compression. A loading screen is the usual case, since it has to appear at once.
The file is smaller, but the loader has to wait for the decoder. On a slow drive the load is faster overall, and on a fast device it can be slower.
cartridge
cartridge {
name "A Lynx of Two Masters"
manufacturer "NGA"
rotation none
}
What a reader shows about a .lnx: the three fields of its header. name holds
at most 31 bytes and manufacturer at most 15. A longer one is an error and is
not cut. rotation is how the machine is held: none, left or right.
Give each field at most once. Without the block both strings are empty and the
rotation is none. The block with any other Container is an error, lyx
included, because that image has no header.
frames
frames held
Makes the Transition routine read an edge's list of what to load in one pass, before the first image arrives. Without it, the routine goes back to the Frame between one image and the next, and the driver opens it again each time.
It costs some 130 bytes of the routine and two bytes of zero page, in every Phase. It pays where opening is slow, such as a Lynx's cartridge or a diskette, and saves nothing where opening is a Window being switched. On a Lynx, an edge of forty images took 54 frames without it and 15 with it.
The list is kept in the memory of the edge's largest image, which then loads last, so it costs no memory of its own. Write it at most once. In a Project with an edge and no storage there is no Frame, and the line is an error.
optimize
optimize speed
What to prefer where one choice costs speed against size. Write speed, size
or fit. A document that says nothing gets fit.
fit takes as much speed as the tool can promise to give back. It leaves any
choice it cannot undo.
An Intent fills a silence and overrides nothing. The tool wraps a function
written inline under every Intent.
constants
constants {
CART = 1
LEVELS = 8
}
A name the Project gives a value. Every Module sees it. The assembler reads one
in .if, a macro argument takes one, and C reads one through an extern const.
The value is a literal. It is not an expression. Do arithmetic in the source, where the tool reads
the value. A - written right after the = is the literal's sign, so SHIFT = -1 and
DEPTH = -$10 are values. C reads a negative one through an extern const i8.
Giving one name a value twice is an error. A Module that declares a name the Project declared is an error.
choose
choose { "vbxe-d640.ngp" "vbxe-d740.ngp" }
include "game.ngp"
Builds one .xex that can be any of the documents it names, and the program
picks one while the file loads. Use it when one piece of hardware can sit at
more than one address: a VBXE at $D640 or at $D740, for example.
Each document holds values and nothing else: register lines in a target
block, and a constants block. Every document declares the same names. The
tool builds the program as the first document says and places it once. It then
builds it again with the values of each other document and notes every byte
that comes out different.
An init of the load block finds out which document the machine
needs and writes its number into ngaChoice: 0 for the first, 1 for the
second. While the rest of the file loads, the tool's own code changes every
noted byte to the value of that document, before anything runs it. Code that
the first init runs is not changed, so it reads the places it checks by their
numbers. Code of a later init is changed before it runs.
A document may change bytes only. A .if that takes branches of different
lengths in two documents is an error. So is a register of a later document
standing where the program could place a section. Only a .xex without
storage can be changed this way.
diagnostics
diagnostics {
deny NGA4501
allow NGA4606
off NGA0113
}
deny raises a finding to an error. allow lowers it to a warning. off
suppresses it.
An identifier that names no diagnostic is an error.
Setting one identifier twice is an error. The message names both places.
The block takes effect after the tool has read the Project, with every file it
includes and every Module a generator wrote. It cannot change how the tool
reports a finding raised while reading them, and a generator's warning is one of
those. Where the block stands in the file makes no difference. It never
overrides the command line: --deny, --allow and --off reach every finding.
include
include "targets/atari-assets.ngp"
Names another Project file. Its content becomes part of this Project.
The tool resolves the path as it resolves a Module path. See the library.
The tool reads each file once. A second include of a file it has already read
adds nothing, so two files can both include a third that they share. The tool
knows a file by the path it resolves to, not by how the include spelled it.
This is safe because a file means the same wherever it is included: blocks sum
and their order does not matter, and the paths inside a file are resolved
against that file. A cycle is still an error.
Inclusion takes a whole document. An included .ngp is a .ngp that could
stand alone. Blocks sum, so an included file that carries its own
modules { ... } composes with the file that included it.
There is no glob.
The library
The tool ships some files as source instead of compiling them in. They are a
storage driver per mechanism, a decoder per format, a machine's OS, a machine
variant that names them, and the storage that goes beside a variant. They live in lib/.
Name a library file in modules or include as you name a file of your own.
The tool looks beside the document first and in the library second. A file of
your own under the same name therefore wins.
The tool finds the library in three ways, in this order:
- The directory
--libnames. lib/beside the executable, which is how a release archive carries it.- The source tree the tool came from.