Atari Lynx programming tutorial series:
In part 16 we investigated the way cartridges work at a reasonably low level and how the CC65 libraries help out by providing cartridge read functions from a start position and onwards. In this part we will take this one step further and introduce the file system that Epyx introduced and is still used in most cartridges. Also, we will see how such files are created and put into the ROM image. That will bring us right back to segments and memory from part 15.
All kinds of images
So far we haven’t really looked at how ROM images are organized. There are quite a few types of images available. Most of these images are related to the development kit that was used to create them.
||Uploadable file for Pinky or Howard
||ROM image with unencrypted header
||.o (or .obj)
||Uploadable file, not to be confused with the CC65 object files (also .o extension)
||Headerless ROM image
||.lyx file with Handy header
Two of these image types are more alike than you might expect. Even coming from different development kits they use the same directory and files structures. The Epyx .rom, the .lyx images (and therefore also the CC65 .lnx images) all have a directory that lists one or more files for the game to load from cart into RAM memory. The Epyx development kit introduced this as a (partially optional) means of partitioning content on a cartridge. You could choose not to use it, but you must have at least one file entry to make a bootable game on real Lynx hardware with the original encrypted headers. If you create your own boot loader in the encrypted header of a cartridge you could dispose file entries and the directory all together. As it turned out, the mechanism of having a directory structure proved to be really useful. Useful and relatively easy, as most segmented games have used it ever since. The functions to load files from the cartridge have been put to the test extensively and work like a charm.
Directory and files
The contents or binary image of a cartridge typically have at least three distinct areas:
- Encrypted header
- Directory structure
- Individual files containing code or data
Each of these areas consist of one or more items. Here’s a graphical view of a cartridge that you could find in an arbitrary, generic retail cartridge for the Lynx.
The three areas are marked in green, orange and grey. The header with its encrypted two stage loader is a requirement for games that need to run on actual hardware. There are variations to this theme (it does not have to be a two-stage loader per se), but the original retail releases all had them. The directory holds the file entries, which in essence are offset pointers to the “files” on the cartridge. These files follow the directory and consist of any number of up to 256 files. Typical for the Epyx two stage loader is that the first file holds (must hold) the title screen that is shown during the booting of the Lynx and game. The second one is the main executable. After that there can be any number of files holding code, data, music, graphics, whatever.
Here is a zoomed in picture of the directory and file entries pointing to the various files:
The file entries contain the information to find the start of the particular bytes on the cartridges, expressed in terms of cartridge page and offset in the page. Three bytes are used for that: one byte for the page number, as there are at most 256 pages in a cartridge, then two bytes for the offset, because the pagesize can be as much as 2048 (sometimes 4096) bytes.
The other things that a file entry has are the length of the file in number of bytes and the load address in memory. That’s another two bytes for the length (no use to have files longer than $10000 because that is the maximum RAM memory size, assuming you do not intend to stream from the cartridge) and two for the load address in the $FFFF address space. That’s 7 bytes, plus an 8th byte that is used as a reserved flag with value 0x00. It is not actually used in the Epyx style of file entries, although the BLL style file entries use it to mark it as an executable (0x88) or normal data file (0x00).
The structure of a file entry in bytes look like this (it’s the first file entry for APB in case you want to know):
You can see the offset, length and load address in LSB, MSB format. This particular file entry of APB is used to load the title screen from the cartridge at the first (zero) cartridge page with a 666 (0x29A) bytes offset. The title screen has a length of 3291 (0xCDB) bytes and is loaded at 0x8000 in RAM memory.
The next file entry in the directory is located right after the first and usually points to the file directly after the first file. Some calculations are in order. If the first entry points to page 0, 0x29A offset for a file 3291 bytes long, then it occupies bytes 666 to 666+3291-1 = 3956. The next file should be start at 3957 (0xF75). APB is a 256 KB cartridge, so each page is 1024 bytes. Taking the ROUND(3957 / 1024) = 3 means that it starts in the third page of the cartridge. The 3 pages take 3072 bytes, which means that location 3957 has an offset of 3957-3072 = 885 or 0x375 in hexadecimal. How did we do? Let’s check if we calculated that correctly. This is the next file entry of APB for the resident startup code:
Refer back to the previous picture and you should see that it indeed shows that the second file is located in page 3, 0x375. It is 0x1B4 bytes long and loaded at 0x0400 (which is typical as it is right behind the loader code from 0x200 to 0x3FF). You could repeat the process to calculate the next file location from there and keep continuing until you reach the end of the directory and the start location of the first file.
Note that it doesn’t always work out like this with files being contiguously located on the cartridge. You might decide that a file needs to start at a page boundary, meaning an offset of zero. This is much more efficient for loading, as you do not have to do dummy reads to advance the counter. The cartridge will end up with some unused space (call it fragmentation) caused by gaps from the end of a file to the start of the next page. It’s a trade-off between compactness/space and efficiency of loading files.
All right, so far we know that each file entry is 8 bytes in size. You can find a particular file entry by skipping the 8-byte sets. For example, to read file number 3 (zero-based numbering) you would skip 3*8=24 bytes from the start of the directory, bypassing file number 0, 1 and 2, and reading the 8 byte tuple for the 4th entry. From the bytes the cartridge’s shift register for the page number is set, the dummy reads to advance the ripple counter are done and you start copying the bytes from every read to RCART0 to the destination RAM address until you have copied enough bytes for the length specified.
In CC65 all logic to do the preparations (shift register and ripple counter) have been taken and captured in the assembler code from lynx-cart.s. It holds the functions to skip bytes, read a single and a number of bytes, plus page (block) selection. To load individual files as specified inside a directory by a file entry you need the lynx_load function from load.s. This functions is available by using the include file lynx.h. I couldn’t imagine a scenario where you haven’t already included the file, so you should be good to go.
/* Accessing the cart */
void __fastcall__ lynx_load(int fileno);
/* Load a file into ram. The first entry is fileno=0. */
void __fastcall__ lynx_exec(int fileno);
/* Load a file into ram and execute it. */
The lynx_load functions is really simple to use: pass the file number and the file’s bytes will be read into RAM memory as specified in the file entry. Typically you will use define statements to give the symbolic, readable names to the file numbers. More on that later.
Laying out your memory
Before we can start creating a directory structure we need to know how CL65 as a linker will create an image and how our “files” are laid out in that big binary blob. You see, CL65 does not know nor care about files. It views your code and resources in terms of segments that are loaded into memory. You might want to refresh your knowledge of that by rereading the tutorial part on “Memory and Segments” before continuing.
Of the 64KB memory that the Lynx has a smaller number is available after the video buffers, C stack have been assigned.
If you have only a little code and data that will all fit in the remaining memory, you will probably not bother with files. But just imagine the challenges that arise when you have more code and data than fit into memory at one point in time. You will need to plan what will be in memory when. From that you will create a memory plan that shows the various memory areas that exist.
The special areas, such as zeropage, stacks and the videobuffers, are required for any Lynx game to function. This is regardless of the development kit you are using, except maybe for the C-stack. From here on these are not shown anymore, even though they might be located else in the memory space than in the initial pictures. Their location is fixed throughout the lifetime and state of the game and usually at the bottom and top of the 64K memory range, like shown in the picture.
Looking at an example
The example game we will use is a game that starts with an intro, consisting of a title screen and some music. Pressing A will start the game, where you will battle some nasty aliens, listening to different, exciting music. When you loose all your lives in the game, you will see a game over screen that plays some sad music. After this you will return to the intro screen and this loop starts over. The core of your program is always be in memory and in charge of loading and running the other parts of the program. There is also a resident part related to the C-runtime, because we are using C and CC65.
Perhaps a good way to design your memory areas is to start from the building blocks of your game. You pick your building blocks (intro, main game, outro, music, et cetera) and determine their required memory size. Next you need to place these blocks in the available memory space. If the sizes required by the blocks are small enough you can create a single memory area that accommodates all blocks at the same time. You would have the intro, outro, game and all three music pieces in memory simultaneously. Lucky you.
In this case, you create one big memory area that spans from $0200 to $BE38 and place all code and data in there. You could read it as one big blob from cartridge and execute it. If you have come this far in the tutorial series and have already created a game without having to use the lynx_load function, then you have done this already.
Here is the MEMORY definition from the lynxcart.cfg file that shows a single RAM section. The name for the area that holds the C-runtime and required startup code and data must be named RAM. We saw this before when we talked about memory and segments.
ZP: file = "", define = yes, start = $0000, size = $0100;
HEADER:file = %O, start = $0000, size = $0040;
BOOT: file = %O, start = $0200, size = __STARTOFDIRECTORY__;
DIR: file = %O, start = $0000, size = 5*8;
RAM: file = %O, define = yes,
start = $0200, size = __VIDEO__ - __CSTACK__ - $0200;
However, if you find that it does not fit, you will have to time-share the memory space in a smart way. That means creating areas where the “occupants” are not in their assigned “appartment” at the same time.
It is like a puzzle: what should be together in memory when? The resident part will always be in memory and never be unloaded or replaced. The functional parts however will occasionally be in memory and not necessarily at the same time. That’s a good thing, because when the total size of these part exceeds the available memory it wouldn’t be possible anyway.
The following pictures shows possible layouts of memory at three points in time for our game that has an intro, actual game and a game over outro screen, all with different music to play:
Let’s look at the three layouts in a bit more detail:
The top layout shows the intro in memory (yellow), next to the intro music and the core part plus some additional resident stuff (all blue) that the C runtime needs.
- Main game
During gameplay the intro is replaced with the game code and resources. There will be different music, but the area in memory is the same. It’s just that other music will be loaded.
- Game over
Essentially the same as the intro, except with other code and resources, and different music.
For now the important parts are those that vary, and they are represented as the yellow and dark blue areas. The resident block is not free to use to your liking, but reserved. Notice how the amount of code and data loaded into the area before music differs. The intro requires less space, but the main part of the game uses the full amount of memory available in that area.
The core is responsible for loading the intro, game and outro. It should also load the correct music data. Because the music will always be playing regardless of the state the game is in, the code to play the music must always be present. That means that either the core or music area should hold the code to play the music. Placing it in the core is more efficient (placing it in each music area means it has to be loaded each time and must be present on the cartridge three times as well). The yellow areas are self-supporting parts of the game that have all code and data inside their respective memory blocks.
When designing your memory layout you need to be aware that there is a difference in the area of memory and the code and data that is loaded there at a particular point in time. Looking at our scenario you might be tempted to think that there is 1 memory area for the yellow blocks, from $0200 to around $7000, which we will name YELLOW for the sake of argument. But, in actuality there are three memory areas and YELLOW is not relevant. The memory areas are INTRO, MAINGAME and OUTRO and they just happen to be positioned on top of each other.
You could create a flexible layout that gives the music as much space as is available after the yellow area. That’s a valid option in which case INTRO would be $0200-$5000, MAINGAME from $0200-$7000 and OUTRO from $0200-$6000. Instead our approach chose to keep the memory sizes fixed from $0200-$7000 across the three game states, because the layout does not require the empty space left by INTRO and OUTRO. The same approach is taken for MUSIC. It is actually three areas (MUSIC1, MUSIC2 and MUSIC3) when the music loaded at each time is different. They can be the same size even though the exact size of music pieces 1, 2 and 3 are unlikely to be exactly the same.
We will compute the yellow blocks from the start of available memory $0200 and place the music right after it. The required RAM area needs to be located at the end of available memory right before the C-stack. Therefore, it is calculated from $BE38 backwards.
We will need some symbols to make things readble, flexible and maintainable. The bolded items are of interest for now. They indicate the sizes of our various blocks.
__STACKSIZE__: type = weak, value = $0800;
__STARTOFDIRECTORY__: type = weak, value = $00CB;
__BLOCKSIZE__: type = weak, value = 2048;
__EXEHDR__: type = import;
__BOOTLDR__: type = import;
__RESIDENT__: type = weak, value = $1000;
__GAMESIZE__: type = weak, value = $3000;
__MUSICSIZE__: type = weak, value = $1000;
__VIDEO__: type = weak, value = $BE38;
With these symbols we can calculate the memory requirements for each of our blocks. When you create a MEMORY area
ZP: file = "", define = yes, start = $0000, size = $0100;
HEADER: file = %O, start = $0000, size = $0040;
BOOT: file = %O, start = $0200, size = __STARTOFDIRECTORY__;
DIR: file = %O, start = $0000, size = 5*8;
RAM: file = %O, define = yes, size = __RESIDENT__,
start = __VIDEO__ - __STACKSIZE__ – __RESIDENT__;
INTRO: file = %O, define = yes, start = $0200, size = __GAMESIZE__;
MAIN: file = %O, define = yes, start = $0200, size = __GAMESIZE__;
GAMEOVER:file = %O, define = yes, start = $0200, size = __GAMESIZE__;
MUSIC1: file = %O, define = yes, start = $0200 + __GAMESIZE__,
size = __MUSICSIZE__;
MUSIC2: file = %O, define = yes, start = $0200 + __GAMESIZE__,
size = __MUSICSIZE__;
MUSIC3: file = %O, define = yes, start = $0200 + __GAMESIZE__,
size = __MUSICSIZE__;
From segments to memory to files
After laying out the memory requirements and areas we essentially have a few buckets where we need to put our code and data in. At this point we return to where we left of talking about memory and segments. By assigning code and data to the various segments, we can fill the buckets with all types of segments CODE, DATA and RODATA. BSS segments are never placed on in binary files, because it is all uninitialized memory anyway and nothing more than a memory range.
The first part should look familiar:
EXEHDR: load = HEADER, type = ro;
BOOTLDR: load = BOOT, type = ro;
DIRECTORY:load = DIR, type = ro;
STARTUP: load = RAM, type = ro, define = yes;
LOWCODE: load = RAM, type = ro, optional = yes;
INIT: load = RAM, type = ro, define = yes, optional = yes;
CODE: load = RAM, type = ro, define = yes;
RODATA: load = RAM, type = ro, define = yes;
DATA: load = RAM, type = rw, define = yes;
BSS: load = RAM, type = bss, define = yes;
ZEROPAGE: load = ZP, type = zp;
EXTZP: load = ZP, type = zp, optional = yes;
APPZP: load = ZP, type = zp, optional = yes;
It is the usual definition of the required segments that go into the standard memory areas. For the game we’ve been discussing some additional segments are required.
INTRO_CODE: load = INTRO, type = ro, define = yes;
INTRO_RODATA: load = INTRO, type = ro, define = yes;
INTRO_DATA: load = INTRO, type = rw, define = yes;
INTRO_BSS: load = INTRO, type = bss, optional = yes;
OUTRO_CODE: load = OUTRO, type = ro, define = yes;
OUTRO_RODATA: load = OUTRO, type = ro, define = yes;
OUTRO_DATA: load = OUTRO, type = rw, define = yes;
OUTRO_BSS: load = OUTRO, type = bss, optional = yes;
# Main game
MAIN_CODE: load = MAIN, type = ro, define = yes;
MAIN_RODATA: load = MAIN, type = ro, define = yes;
MAIN_DATA: load = MAIN, type = rw, define = yes;
MAIN_BSS: load = MAIN, type = bss, optional = yes;
MUSIC1_RODATA: load = MUSIC1, type = ro, define = yes;
MUSIC2_RODATA: load = MUSIC2, type = ro, define = yes;
MUSIC3_RODATA: load = MUSIC3, type = ro, define = yes;
It is actually not that special. All code and data with the INTRO_ prefixed segments are assigned to go into the INTRO memory area. The same is true for the MAIN_ and OUTRO_ segments. You might notice how the MUSIC1, 2 and 3 segments only have a RODATA segment defined, as the music itself is just read-only data, not changing data, nor code.
You can safely assume that the linker will place the segments in a memory area in a certain order. How it is laid out is not really relevant. As long as you make sure you have loaded the code and data into the area before you start using it by calling the code or referencing the data.
What is more important is that the linker will create the binary file that holds all code and data in the order that the memory areas have been defined in the MEMORY section. The linker will emit the binary image with “files” for each of the areas that have a file=%O in their definition.
HEADER: file = %O, start = $0000, size = $0040;
This attribute will make the linker emit the contents of that memory area in the final file. You can see how it is referring to %O for each area, effectively appending the contents to the same, single output file. This file will be called whatever you have set as your target in the make file.
$target = tutorial-files.lnx
objects = lynx-160-102-16.o lynx-stdjoy.o \
$(target) : $(objects)
$(CL) -t $(SYS) -o $@ $(objects) lynx.lib
The bolded item show how the $target is passed into the linker statement that will look like this when expanded:
cl65.exe –t LYNX –o tutorial-files.lnx lynx-160-102-16.o lynx-stdjoy.o tutorial.o lynx.lib
Long story short: the target is the file that is called %O in the memory area. In it all memory areas are written in the order in which they are declared. The order of the segments per area is not relevant.
Entries in a directory
The final thing that keeps us from having a fully functional file system is a directory. The purpose of the directory is to list the “files” and their respective positions within the binary image. In the end, the virtual file system of an Atari Lynx cartridge is nothing more than make believe. When we are able to compute the file entries like we saw at the beginning of this part, we are good to go.
The directory.asm file builds no code, just data representing the file entries. Here is the skeleton of the file, where details have been omitted for now.
; More imports
; File entries go here
The directory is created in a segment called DIRECTORY. It will hold the 8 byte entries that indicate where the files are located in the binary image on the cartridge.
You can see how start and end address symbols (__DIRECTORY_START__ and __DIRECTORY_END__) are declared these are used to compute the length of the directory itself. The first entry is our RAM area where the C-runtime and other resident code and data are located.
; Entry 0 - Resident executable (RAM)
.word off0 & (__BLOCKSIZE__ - 1)
First, the offset of the RAM area off0 is computed, by taking the start location of the directory and adding the length of the directory to it. The page number is computed from the blocks already taken up. Next the length of the current area is calculated by adding the startup and initialization segment sizes, plus the code, data and read-only data segments. All of these were defined to go into the RAM area. Finally the file entry is added as raw bytes with the .byte and .word statements. Take a look at the picture above to see that it creates the right data. Normally you do not need to change this first entry at all. It is special in that it has multiple parts and requires the total size to be computed from them.
The next file entries are more of the same. The help in creating them easily a macro is included in the default directory.asm file.
.macro entry old_off, old_len, new_off, new_block, new_len, new_size, new_addr
.word (new_off & (__BLOCKSIZE__ - 1))
You use the macro entry by feeding it the offset (which has the page number and offset) and length of the previous (old) area, plus the size and load address of the current (new) area.
This is what a call to the macro looks like
entry off0, len0, off1, block1, len1,__INTRO_CODE_SIZE__+
The variables off0, len0 are the offset and length from the RAM area. off1, block1 and len1 are variables that are passed without meaning values. They will get a value after the macro has executed. Two of these values (new_off and new_len) will be used to feed into another macro call to create the next entry.
You will also add the new size and address. This repeats over and over again until all entries have been created.
entry off1, len1, off2, block2, len2, __OUTRO_CODE_SIZE__+
One thing we didn’t cover so far is where all these double underscore pre- and postfixed values come from. They are imported at the top of the file and come from values that the linker has emitted. Remember how the linker would create _SIZE__ and _LOAD__ postfixed values for each MEMORY and SEGMENTS declared area and segment that has the define=yes attribute set. From these values you can calculate the file entries, by simply adding the sizes to the old offsets. It’s like creating a long line of the memory areas, one after the other.
Typically you will use the _LOAD__ of the first segment in an area. The order of the segments is not really relevant, except that you need to take the first one listed per area. Like the areas themselves, the segments are laid out sequentially in a particular area. This implies that the first segment is located at the beginning. The various sizes you add together come from all the segments that are located in the area. There could be more than just one of each CODE, DATA and RODATA. It all depends on how you assigned segments to areas.
You need to adjust the lynxcart.cfg file to accommodate the space required in the directory. This is as simple as specifying the number of file entries in the multiplication (6 file entries in the example below):
DIR: file = %O, start = $0000, size = 6*8;
After that you are good to go.
Putting it all together
Let’s do a quick recap of what is needed to create a files and corresponding directory.
- Create your memory areas based on the sizes they will need.
- Make a layout of the areas in memory and moments in time.
- Determine the start locations and finalize the definitions of the areas in the lynxcart.cfg
- Create segments that correspond to the areas and list them in lynxcart.cfg
- Write code and make resources and put these into the correct segments
- Create a directory entry that lists all memory areas, starting with the RAM area.
- In each part of your code, be mindful of the expectations for segments that need to be loaded. Call lynx_load before accessing functions, variables or resources in a segment.
To help out with the loading, do yourself a favor and define symbolic names to represent the file numbers. The file start at number 0, which is the RAM area and resident code. It is unlikely that you will ever have to load this yourself. After that the files
#define FILE_INTRO 1
#define FILE_OUTRO 2
#define FILE_MAINGAME 3
Here is a typical piece of code that shows how to use the lynx_load and file numbers.
In the code fragment, which could be inside your void main() static entry point routine, the show_intro function is located in the INTRO area. It needs to be loaded before it can be called. Hence the call to lynx_load, passing in the FILE_INTRO symbol. Having the names and number of files decoupled will be very useful when you need to reorder files in the binary image. You can change the file numbers in one place and will not have to hunt your code to check where you used the particular file number that has changed.
At this point it is worth mentioning that the first file (RAM) is only there if you use the mini-bootloader. That loader does not require a startup sprite. For games that use the Epyx bootloader, you would have seen a first file pointing to sprite data, and the second file to the resident RAM file.
You might be lucky and get it all to work the first time. If you did not manage to do so, or want some more internal look at what has been done and generated, you need some more information. That’s where the map file comes in handy. The map file shows a lot of things for your program/game, including the segment locations and what is located where.
To generate a map file an additional argument is needed in the call to the CL65.exe linker.
$(CL) -t $(SYS) -m cart.map -C lynxcart.cfg -o $@ $(objects) lynx.lib
By adding –m and passing a filename the linker will emit a map file (cart.map in this case) that holds valuable info. Here is an excerpt:
Name Start End Size Align
DIRECTORY 000000 000027 000028 00001
EXEHDR 000000 00003F 000040 00001
ZEROPAGE 000000 000019 00001A 00001
EXTZP 00001A 000032 000019 00001
BOOTLDR 000200 0002CA 0000CB 00001
INTRO_CODE 000200 000246 000047 00001
OUTRO_CODE 000200 00023D 00003E 00001
OUTRO_RODATA 00023E 00025C 00001F 00001
INTRO_RODATA 000247 000263 00001D 00001
INTRO_BSS 000264 000264 000001 00001
STARTUP 003200 00327C 00007D 00001
INIT 00327D 0032AB 00002F 00001
CODE 0032AC 0046E0 001435 00001
RODATA 0046E1 004818 000138 00001
DATA 004819 00491A 000102 00001
BSS 00491B 004A32 000118 00001
The segment list above shows the start and end addresses, plus the sizes of the segments that are created. It is not the complete list, but you can notice how the OUTRO_BSS is missing. Apparently nothing was created in the BSS segment for the OUTRO and there was no need to emit it.
_FileBlockByte 00002F RLZ _FileBlockOffset 000027 RLZ
_FileCurrBlock 00002E RLZ _FileDestAddr 00002A RLZ
_FileDestPtr 000031 RLZ _FileEntry 000026 RLZ
_FileFileLen 00002C RLZ _FileStartBlock 000026 RLZ
__BLOCKSIZE__ 000800 REA __BOOTLDR__ 000001 REA
__BSS_RUN__ 00491B RLA __BSS_SIZE__ 000118 REA
__CODE_SIZE__ 001435 REA __CONSTRUCTOR_COUNT__ 000000 REA
__CONSTRUCTOR_TABLE__ 0032AC RLA __DATA_SIZE__ 000102 REA
__DESTRUCTOR_COUNT__ 000000 REA __DESTRUCTOR_TABLE__ 004815 RLA
__EXEHDR__ 000001 REA __INIT_SIZE__ 00002F REA
__INTERRUPTOR_COUNT__ 000002 REA __INTERRUPTOR_TABLE__ 004815 RLA
__INTRO_CODE_LOAD__ 000200 RLA __INTRO_CODE_SIZE__ 000047 REA
__INTRO_DATA_SIZE__ 000000 REA __INTRO_RODATA_SIZE__ 00001D REA
__OUTRO_CODE_LOAD__ 000200 RLA __OUTRO_CODE_SIZE__ 00003E REA
__OUTRO_DATA_SIZE__ 000000 REA __OUTRO_RODATA_SIZE__ 00001F REA
__RAM_SIZE__ 008638 REA __RAM_START__ 003200 RLA
__RODATA_SIZE__ 000138 REA __STACKSIZE__ 000800 REA
The bolded items will look familiar by now. Inspecting these values help find any overflow errors that the linker might report, or troubleshoot directory issues. A detailed look at how to track these errors is for another time.
We’ve looked at how to design your memory and create segments, files and the entries for your directory. With this you can start building beyond the 64KB limit that you otherwise have. Next time we will look at encryption of headers, or maybe input. Who knows. Till next time.