XEX Filter - A toolkit to analyze and manipulate Atari binary files

Copyright © 2020-2022 by Víctor Parada

Current version: 1.7 (2022-02-02)

A bit of history

Every time I needed to review the file structure of an Atari 8-bit binary file (EXE, COM or OBJ, all of them known today as XEX), I had to write and/or customize a special and personalized tool to display the required information in a simple way (other than to use a sector editor or an hex editor). More tools were needed to modify them or to manipulate to create new binary files in the way I wanted.

In the 80's I did it in Atari BASIC (with some USR routines). I created a whole toolkit of utilities to perform very different tasks, and I had to run each of the required ones when I needed to do many operations in a sequence to get a final EXE binary file.

During the recent years, I got fun time rewriting some of those old tools in perl language, in order to work over XEX files directly on my modern computer instead of having to setup an ATR file and run my original tools in an emulator, which takes too much extra time. These new tools became more flexible than their respective original ones in BASIC, and they allowed me to add more features and to include many steps in a single run.

This year (2020), as most of the tools were similar in its internals (some scripts were modifications of previous ones), I decided to merge them in a single bigger tool, in order to allow me to perfom many different tasks at the same time. Then, this XEX Filter toolkit was born.

Atari 8-bit binary file structure

A8 binary files contains programs and/or data in a special file structure: a sequence of data blocks. These blocks specify where in memory to load, and it might be an initialization and/or a run vector.

Each block has the following structure:

Position Content Description
1-2 Binary header Contains the value $FFFF. This must be present in the first segment, and it is optional for the next ones.
3-4 Initial load address Pointer (A) to the memory address where the data of this block will be loaded.
5-6 Last load address Pointer (B) to the memory address for the last byte of this block.
7-x Data Sequence of (B-A+1) bytes to be loaded into memory.

There are two special memory locations that perform different actions when data is loaded there:

INITAD ($02E2-$02E3)
Every time a block is loaded, the loader must check if this vector was changed by the loaded data. If the loader finds a new value, it transfers the control to the subroutine pointed by that vector and waits for it to continue with the next block of the file. Splash screens are initialized this way. Many blocks in the same file might load vectors in this address, and the loader will transfer the control each time.
RUNAD ($02E0-$02E1)
When all the blocks in the file have been loaded into memory, the loader checks if this vector has been loaded and transfers the control to the code at that memory location.

If none of these memory adresses are loaded, it depends on the loader or DOS if the control is transfered to the loaded data, usually the start address of the first or the last block.

Toolkit overview

XEX Filter is a tool that reads one or more XEX files, analyses and shows their binary structure, and optionally creates a new XEX file applying one or more filters to manipulate the file structure and it contents. With these filters it is possible to extract a binary block with its address headers from the file, remove unwanted blocks from it (like a splash screen that is displayed during the load), split or join blocks in a file, split or join files, change the loading order of the blocks, extract portions of the file as raw data (images or charsets to be edited), assign a new load address to a given block, etc.

Help screen is as follows:

  
xex-filter version 1.7 (2022-02-02)
Copyright (c) 2020-2022 by Victor Parada
<https://www.vitoco.cl/atari/xex-filter/>

Usage: xex-filter.pl [-option]... [--] FILE1.XEX [FILE2.XEX]...

Options:
  -i n1,n2-n3,...  List of indexes for blocks selection
  -x n1,n2-n3,...  List of indexes for blocks exclusion
  -s a1,a2-a3,...  List of memory addresses where to split blocks
  -e a1,a2-a3,...  List of memory addresses for data extraction
  -a a1,a2-a3,...  New address list to assign to output blocks
  -v a1=v1,v2,...  Append blocks with byte value at $0000 or address
  -w a1=v1,v2,...  Append blocks with word value at RUNAD or address
  -z max           Max number of bytes to fill between blocks
  -r               Removes zeros if there are more than 4 in a row
  -d               Writes data without address pointers or header
  -b               Reads input as blocks up to 64K of data at $0000
  -f               Fix corrupt files by filling or discarding data
  -m               Makes a memory map with the selected blocks
  -o NEW.XEX       Output file (mandatory for -i -x -s -e -a -z -r -d and -m)

Addresses between 0 and 65535 or in $hhhh format. Use "-" for a range.

The structure of each input file is displayed during the load of them, and a sequence number is assigned to each of the identified blocks. Then, a new file is created applying one or more filters to the selected blocks.

The output file will always have a leading $FFFF word, and any other instance of it in the middle of the input files will be ignored and removed.

Both decimal and hexadecimal numbers are used to display input and output file structures. If a block start address has a label assigned to it, that label will be displayed in the block description.

Some filter options requires a list of values, and values may be specified using decimal or hexadecimal notation, or using one of the defined labels.

Currently defined labels are:

RUNAD=$2E0     SDLSTL=560     CH1=754
INITAD=$2E2    COLOR0=708     CHBAS=756
PORTB=$D301    COLOR1=709     CH=764
BOOT=9         COLOR2=710     BASICF=1016
DOSVEC=10      COLOR3=711     BANK=$4000
SOUNDR=65      COLOR4=712     CARTB=$8000
RAMTOP=106     MEMTOP=$2E5    CARTA=$A000
COLDST=580     MEMLO=$2E7     ATRACT=77
SDMCTL=559

This tool will abort with an error message if an undefined label is found in an option.

Filter options

Some options change the behavior on how input XEX files are read, and some options modifies the way the blocks are written to the new XEX file. Some options might be used in conjunction and some of them cannot be used at the same time, having to run the tool in steps if required.

Notes about options:

(no options)

When no options are specified, the file structure is displayed for all the input files.

Examples: 1 10

-o | Output filename

Specifies a filename for a new XEX file to be created. This option is required for many other options. Warning: If the file already exists, it will be overwritten!

Examples: 2 3 4 5 6 7 8 9 10 11

-i | Indexes for block selection

Allows the selection of the required blocks from the input file(s) to be sent to the output file, probably altered by any other option. If this option is not specified, all the blocks from the input files will be included in the same original order. The list of selected blocks must be specified as integer decimal numbers separated by comma, and the specified order is relevant because that will be the order to be written in the output file. Ranges are allowed by using a hyphen between two numbers. Decreasing order is allowed for any of the specified ranges.

Examples: 3 7 8 9 11

-x | Indexes for block exclusion

You can specify the blocks from the input file(s) that you don't want to be in the output file. This is the complement to the -i option and both option cannot be used in the same command line. The blocks order is not relevant, as the blocks not excluded will be written in the same order they are read from the input file. The list of excluded blocks must be specified as integer decimal numbers separated by comma. Ranges are allowed by using a hyphen between two numbers. Decreasing order is allowed for any of the specified ranges, but it is not relevant in the result.

Examples: 3

-s | Split at address

List of addresses where blocks had to be splitted. With this option, if a selected block contains a byte located at any address from the list, the block will be split there, with that byte being the first one of the next block. If a range of address is specified, two splits should be done, resulting in a isolated block with those address in the header (if both bytes were in the same block from the input file) between the reminders of the originally selected block.

Examples:4 8

-a | Change load address

Specifies a list of new loading memory addresses, one for each block selected for output, which must be in the same order, and addresses for all blocks must be provided. If a range of addresses is specified for a block, an implicit split will happen if the block was larger than the range, and a fill with zeros will happen to complete the range if the block was shorter.

Examples: 8 9

-e | Extract data for a range of memory

Simulates the load into memory of the selected blocks from the input files and creates new blocks from the specified ranges. A single memory address in the list identifies a starting byte for a block, which will end at the beginning of the next block in the list, or at $FFFF if there are no more blocks in the list.

Examples: 7

-m | Memory map

Dumps a 64K memory map block which will be the result of the "load" of the selected blocks from the input files. The block will be initialized with zeros. The output is the same than "-e0".

Examples: 8

-w | Add a word block

Includes a block that contains a word value (two bytes) to be loaded at the specified address, in low-high order. It can be used to load a vector into any system pointer. The list can contain many values separated by comma, and the load address for each of them may be included in the form "addr=val". If no address is specified, it continues with the next address from the previous value of the list. The default address for the first value is RUNAD ($02E0 or 736).

Examples: 9

-v | Add a byte block

Includes a block that contains a byte value to be loaded at the specified address. It can be used to modify any system register during a load. The list can contain many values separated by comma, and the load address for each of them may be included in the form "addr=val". If no address is specified for a value, it continues with the next address from the previous value of the list. The default address for the first value is 0.

Examples: 7 9

-z | Fill with zeros

Merges consecutive blocks, filling with up to the specified number of bytes with the value of zero (null byte). If more bytes are required to merge two blocks, neither the merge nor the fill will be done there. Specify "-z0" to merge only immediately consecutive blocks, where no filling is required.

Examples: 5 7

-r | Remove zeros

Removes sequences of null bytes longer than 4 bytes from the blocks, splitting them if required. This is used to reduce the size of the XEX file under the asumption that the memory locations for those omitted bytes were clean before the load. If a block only has null bytes, the whole block will be removed. Null bytes at the beginning or the end of a block will also be removed, even when they are less than four per side. It does not removes zeros from blocks shorter than 3 bytes, because those usually are NOT simple data, but a vector or a value for some system register.

Examples: 6

-d | Write data only

Do not write address pointers to the output file, just the data. Be aware that if more than one block have to be written, all the data will be concatenated without any filler.

Examples: 8

-b | Read as binary data

This is usefull to add load address information from input files that only contains raw data, in order to create blocks for XEX files. If the input file already has address information, that will be considered as part of the data. Every input file is asigned to a different block with address 0. If the length of a file is more than 64K, it will be splitted into many blocks. To assign any load address to a raw data block, use "-a" option.

Examples: 8

-f | Fix input file errors

Allow some common errors in XEX binary files to be ignored: file without $FFFF headers, truncated files and gargabe at the end. Truncated blocks are filled with null bytes instead of to adjust the end address.

Examples: 10

Examples

(1) Analyzing XEX content

This example simply shows the binary structure of an XEX file. There is no output XEX file.

> xex-filter.pl GAME.XEX
Analyzing "GAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]

(2) Concatenating two XEX files

To insert a splash screen to be displayed while loading a long game, just concatenate both files into a new one.

> xex-filter.pl -o NEWGAME.XEX SPLASH.XEX GAME.XEX
Analyzing "SPLASH.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  1536-1791  [$0600-$06FF]   (256) <- CODE/DATA
  2:   738-739   [$02E2-$02E3]     (2) INITAD -> 1536 [$0600]
Analyzing "GAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  3:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  4: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  5:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "NEWGAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  1536-1791  [$0600-$06FF]   (256) <- CODE/DATA
  2:   738-739   [$02E2-$02E3]     (2) INITAD -> 1536 [$0600]
  3:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  4: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  5:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17504 bytes written

(3) Select or remove some blocks from XEX file

To remove unwanted blocks from a file, just list them using -x.

> xex-filter.pl -o GAMEONLY.XEX -x 1,2 NEWGAME.XEX
Analyzing "NEWGAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  1536-1791  [$0600-$06FF]   (256) <- CODE/DATA
  2:   738-739   [$02E2-$02E3]     (2) INITAD -> 1536 [$0600]
  3:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  4: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  5:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "GAMEONLY.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17238 bytes written

Other way to get the same result is to select only the required ones to be included in the new XEX file with -i. In this case, a range of blocks is selected.

> xex-filter.pl -o GAMEONLY.XEX -i 3-5 NEWGAME.XEX
Analyzing "NEWGAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  1536-1791  [$0600-$06FF]   (256) <- CODE/DATA
  2:   738-739   [$02E2-$02E3]     (2) INITAD -> 1536 [$0600]
  3:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  4: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  5:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "GAMEONLY.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17238 bytes written

(4) Splitting a block

This command line splits a block at a given address. The resulting blocks will have their own address pointers.

> xex-filter.pl -o SPLITTED.XEX -s $6000 GAME.XEX
Analyzing "GAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "SPLITTED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-24575 [$4000-$5FFF]  (8192) BANK <- CODE/DATA
  3: 24576-32767 [$6000-$7FFF]  (8192) <- CODE/DATA
  4:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17242 bytes written

(5) Joining blocks

To join two consecutive block, specify a zero bytes fill.

> xex-filter.pl -o JOINED.XEX -z 0 SPLITTED.XEX
Analyzing "SPLITTED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-24575 [$4000-$5FFF]  (8192) BANK <- CODE/DATA
  3: 24576-32767 [$6000-$7FFF]  (8192) <- CODE/DATA
  4:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "JOINED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17238 bytes written

(6) Removing null bytes

To shrink an XEX file, it is possible to remove rows of the byte zero from within the blocks. Be careful, if memory of the Atari is not clean at the loading addresses range, the squeezed XEX will fail when run.

> xex-filter.pl -o SQUEEZED.XEX -r GAME.XEX
Analyzing "GAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "SQUEEZED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-8198  [$2000-$2006]     (7) <- CODE/DATA
  2:  8254-8275  [$203E-$2053]    (22) <- CODE/DATA
  3:  8292-8313  [$2064-$2079]    (22) <- CODE/DATA
{removed}
 81: 32677-32684 [$7FA5-$7FAC]     (8) <- CODE/DATA
 82: 32694-32697 [$7FB6-$7FB9]     (4) <- CODE/DATA
 83: 32754-32756 [$7FF2-$7FF4]     (3) <- CODE/DATA
 84:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
14170 bytes written

(7) Recover a squeezed XEX

A squeezed XEX file has many blocks with non-zero data and can be loaded into clean memory to get the full program and run it. When Atari's memory was previously used by another program, it is better to load a single block with its zeros included. The following command inserts up to 250 null bytes between blocks.

> xex-filter.pl -o RECOVERED.XEX -z 250 SQUEEZED.XEX
Analyzing "SQUEEZED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-8198  [$2000-$2006]     (7) <- CODE/DATA
  2:  8254-8275  [$203E-$2053]    (22) <- CODE/DATA
  3:  8292-8313  [$2064-$2079]    (22) <- CODE/DATA
{removed}
 81: 32677-32684 [$7FA5-$7FAC]     (8) <- CODE/DATA
 82: 32694-32697 [$7FB6-$7FB9]     (4) <- CODE/DATA
 83: 32754-32756 [$7FF2-$7FF4]     (3) <- CODE/DATA
 84:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "RECOVERED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9027  [$2000-$2343]   (836) <- CODE/DATA
  2: 16384-32756 [$4000-$7FF4] (16373) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17225 bytes written

A filler of up to 250 bytes was enough to recover de file, but note that both main blocks are 2 bytes shorter than before. If we didn't know that, probably the new XEX would be fine, but if we really want to fix them, we have different ways to do that:

Method 1: Simulate a load and extract the same blocks with the right length:

> xex-filter.pl -o RECOVERED-1.XEX -e $2000-$2345,$4000-$7FFF,736-737 RECOVERED.XEX
nalyzing "RECOVERED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9027  [$2000-$2343]   (836) <- CODE/DATA
  2: 16384-32756 [$4000-$7FF4] (16373) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "RECOVERED-1.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17238 bytes written

Method 2: Include bytes at the original memory location of each block's last byte, then change the sequence of the blocks and fill with enough zeros to join the corresponding sub-blocks:

> xex-filter.pl -o RECOVERED-2.XEX -v $2345=0,$7FFF=0 -i 1,4,2,5,3 -z 100 RECOVERED.XEX
Analyzing "RECOVERED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9027  [$2000-$2343]   (836) <- CODE/DATA
  2: 16384-32756 [$4000-$7FF4] (16373) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Adding values...
  4:  9029-9029  [$2345-$2345]     (1) = 0 [$00] '00000000'
  5: 32767-32767 [$7FFF-$7FFF]     (1) = 0 [$00] '00000000'
Writing "RECOVERED-2.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17238 bytes written

(8) Modify a charset of a game

To change the characters or sprites of a game, you can use any character editor if you give it the right file. If the address where the charset resides inside an XEX has not been identified yet, it is possible to map the whole XEX into a 64K block and search for the bitmaps inside it.

> xex-filter.pl -o MAP.DAT -m -d GAME.XEX
Analyzing "GAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "MAP.DAT"...
1: (65536)
65536 bytes written

In this example, the charset was identified to be at $5400, so you can extract it and then insert it again after customization. The extraction could be easyly done using "-e $5400-$5BFF" option, but it won't be easy to insert it back if the XEX file is not previously prepared, so the first step is to create a 2K block inside the file by splitting at that addresses:

> xex-filter.pl -o GAMECHAR.XEX -s $5400-$5BFF GAME.XEX
Analyzing "GAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "GAMECHAR.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-21503 [$4000-$53FF]  (5120) BANK <- CODE/DATA
  3: 21504-23551 [$5400-$5BFF]  (2048) <- CODE/DATA
  4: 23552-32767 [$5C00-$7FFF]  (9216) <- CODE/DATA
  5:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17246 bytes written

The second step is to extract the 2K block and save it without binary headers:

> xex-filter.pl -o CHARSET.FNT -i 3 -d GAMECHAR.XEX
Analyzing "GAMECHAR.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-21503 [$4000-$53FF]  (5120) BANK <- CODE/DATA
  3: 21504-23551 [$5400-$5BFF]  (2048) <- CODE/DATA
  4: 23552-32767 [$5C00-$7FFF]  (9216) <- CODE/DATA
  5:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "CHARSET.FNT"...
1: (2048)
2048 bytes written

After the new charset has been saved by your favourite editor, it must be converted to XEX format:

> xex-filter.pl -o CHARSET.XEX -b -a $5400 CHARSET.FNT
Analyzing "CHARSET.FNT"...
  1:     0-2047  [$0000-$07FF]  (2048) <- CODE/DATA
Writing "CHARSET.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1: 21504-23551 [$5400-$5BFF]  (2048) <- CODE/DATA
2054 bytes written

The last step is to merge the new charset into the game, replacing the original 3rd block with the new one:

> xex-filter.pl -o MYGAME.XEX -i 1-2,6,4-5 -z 0 GAMECHAR.XEX CHARSET.XEX
Analyzing "GAMECHAR.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-21503 [$4000-$53FF]  (5120) BANK <- CODE/DATA
  3: 21504-23551 [$5400-$5BFF]  (2048) <- CODE/DATA
  4: 23552-32767 [$5C00-$7FFF]  (9216) <- CODE/DATA
  5:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Analyzing "CHARSET.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  6: 21504-23551 [$5400-$5BFF]  (2048) <- CODE/DATA
Writing "MYGAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17238 bytes written

(9) Load a cartridge into its original address

If a cartridge game was converted to an XEX, it is usually loaded at low memory and extra code is included to setup the game after the load and start it. It is possible to discard the setup routine and load the cartridge data directly in its original address, and this is how that could be set up.

The first step is to move the game data into a temporary file, and change the load address at the same time:

> xex-filter.pl -o CART.XEX -i 2 -a $8000 MYGAME.XEX
Analyzing "MYGAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "CART.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1: 32768-49151 [$8000-$BFFF] (16384) CARTB <- CODE/DATA
16390 bytes written

As the cartridge data will be loaded on top of the display list and garbage could be seen, we can add a mini block that loads a zero over SDMCTL register (559 or $22F), turning off the screen. Also, the load will fail if BASIC ROM is enabled on XL/XE computers, so we can also disable it by writing a $B3 into PORTB ($D301 or 54017), and we must move RAMTOP (106 or $6A) just below the cartridge data. At last, a RUNAD must specified to call the cartridge initialization routine, which in this example is at $8000. All this steps can be done in a single command:

> xex-filter.pl -o CARTGAME.XEX -v 559=0,PORTB=$B3,106=$80 -w 8000 -i 2-4,1,5 CART.XEX
Analyzing "CART.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1: 32768-49151 [$8000-$BFFF] (16384) CARTB <- CODE/DATA
Adding values...
  2:   559-559   [$022F-$022F]     (1) SDMCTL = 0 [$00] '00000000'
  3: 54017-54017 [$D301-$D301]     (1) PORTB = 179 [$B3] '10110011'
  4:   106-106   [$006A-$006A]     (1) RAMTOP = 128 [$80] '10000000'
Adding words...
  5:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8000 [$1F40]
Writing "CARTGAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:   559-559   [$022F-$022F]     (1) SDMCTL = 0 [$00] '00000000'
  2: 54017-54017 [$D301-$D301]     (1) PORTB = 179 [$B3] '10110011'
  3:   106-106   [$006A-$006A]     (1) RAMTOP = 128 [$80] '10000000'
  4: 32768-49151 [$8000-$BFFF] (16384) CARTB <- CODE/DATA
  5:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8000 [$1F40]
16411 bytes written

(10) Clean a XEX game extracted from a tape or block devices

Some tape loaders were unable to detect the end of file from the tape chunks, as well as some boot disk loaders that only managed sequential sectors instead of a file system, so copiers added a couple of bytes (usually $FEFE or $FFFE) as a flag at the end of the stored XEX, and that the loader could identify to finish the loading loop and start the game. You can detect this situation by an error message during the XEX analysis:

> xex-filter.pl TAPEGAME.XEX
Analyzing "TAPEGAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
ERROR - Extra pointers at end of file

In order to discard those extra bytes, create a new XEX using the following command:

> xex-filter.pl -o CLEANED.XEX -f TAPEGAME.XEX
Analyzing "TAPEGAME.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "CLEANED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-9029  [$2000-$2345]   (838) <- CODE/DATA
  2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
  3:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
17238 bytes written

(11) Make an XEX to be loaded from high mem to low mem

If you have a squeezed XEX file, composed by a lot of short blocks, it is possible to make them load in reverse order, just for fun or to confuse curious people. "-i" option allows ranges from a high value to a lower one, and this is useful for this trick:

> xex-filter.pl -o REVERSED.XEX -i 83-1,84 SQUEEZED.XEX
Analyzing "SQUEEZED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1:  8192-8198  [$2000-$2006]     (7) <- CODE/DATA
  2:  8254-8275  [$203E-$2053]    (22) <- CODE/DATA
  3:  8292-8313  [$2064-$2079]    (22) <- CODE/DATA
{removed}
 81: 32677-32684 [$7FA5-$7FAC]     (8) <- CODE/DATA
 82: 32694-32697 [$7FB6-$7FB9]     (4) <- CODE/DATA
 83: 32754-32756 [$7FF2-$7FF4]     (3) <- CODE/DATA
 84:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
Writing "REVERSED.XEX"...
  -: 65535       [$FFFF]               BINHEAD
  1: 32754-32756 [$7FF2-$7FF4]     (3) <- CODE/DATA
  2: 32694-32697 [$7FB6-$7FB9]     (4) <- CODE/DATA
  3: 32677-32684 [$7FA5-$7FAC]     (8) <- CODE/DATA
{removed}
 81:  8292-8313  [$2064-$2079]    (22) <- CODE/DATA
 82:  8254-8275  [$203E-$2053]    (22) <- CODE/DATA
 83:  8192-8198  [$2000-$2006]     (7) <- CODE/DATA
 84:   736-737   [$02E0-$02E1]     (2) RUNAD -> 8192 [$2000]
14170 bytes written

Download

XEX Filter version 1.7 (2022-02-02)

To run this tool you need to have the perl interpreter without any special module in your computer. It might be a linux box (perl is native there), Windows (with Strawberry Perl or ActivePerl) or Mac. It doesn't have a GUI, it just works in the command line (shell), using unix-like options.

Outside Windows, it might be needed to change the shebang (first line of the perl script) to set the path to the perl 5 interpreter (like "#!/usr/bin/perl" or "#!/usr/local/bin/perl").

Feedback

I'd like to receive bug reports and suggestions to improve this tool. Help requests or any comment about it are welcome too. Please post them in the "XEX Filter" thread at AtariAge forum or PM me through the forum.


© 2020-2022 by Víctor Parada - 2020-01-13 (updated: 2022-02-03)