Turbo Rascal uses a Pascal-like syntax that allows you to develop programs that target a whole bunch of retro machines. That Pascal is compiled down using the built-in Orgasm, or third party assemblers.
But did you know you can use TRSE as a high-level macro assembler?
Programming Low-Level So I Can Code High-Level
Even though TRSE can do amazing things on individual machines, as the brilliant demos from Nicolaas prove, my goal is usually to do as much cross-platform as possible even if my end-project only appears on one machine (Eg. PETFrog).
This is because the more switching around I do, the less “in the zone” I become, and the less productive I will be. It is, for example, why I do so much day to day with Python and C++, even when there might be a “better” tool.
To further my goal of doing cross-platform work on TRSE, I set about developing a cross-platform “Text Mode” input and output unit (library, basically), and that meant digging deep into each of my chosen target platforms, starting with 6502.
Using the Text Mode Unit
Most of my future projects will involve this unit but we need to discuss how to use it here just so you have context for how things happen under the hood.
program hello_world; @projectsettings "system" "C64" @use "text/txt"
To start with, after naming your program in your .RAS file, you can specify in the code which target system you will be compiling for right now. You can also change this in Project Settings, but I prefer doing this on a per-file level and keeping the project agnostic.
Then we tell TRSE we are “using” the text unit.
After this we need to clear the screen. Right now I have packed a bit more than just clearing the screen into the
cls() function so it is more a “screen initialize” than just clear screen.
We can then do things like turn off the flashing cursor and print ‘hello world’:
txt::cls(); txt::cursor_off(); txt::move_to(2,20); txt::print_string("HELLO WORLD!", True);
True in the print is that , yes, we want to output a carriage return to start a new line after the print.
Under the Hood
The challenge of making all this work across different machines is each achieve the same things (eg. text on screen) in very different ways.
On Commodore machines, if you know where screen memory is located, you can simply put bytes into that memory area.
The Apple II has screen memory, but to save chip count (but not make the machine less expensive for the buyer), Steve “Woz” Wozniak made a bizarre, possibly drug-fueled, memory map.
The BBC Micro has Mode 7 which is a nice and handy low-memory using “Teletext” mode, but that is missing on it’s sibling the cost-reduced Acorn Electron, which doesn’t have a text mode but a text-written-to-bitmap mode.
Annnnd … Yes, there might also be some translation from ASCII into whatever passes as screen character codes too.
So what looked easy is bloody hard. How do I achieve this?
Tapping the Operating System
Fortunately each machine has routines built in to ROM to do what we need.
Where they live, and how they work, will be different for each, but they all have to have ways to, say, output text for them to be usable.
On the Atari 800 therefore, to print a single text character on screen at the current cursor location, we call the operating system routine that lives at
In TRSE we have helpful variables mapped to the main registers, and before calling the print we need to put the chosen character into the
_A register, so for Atari 800 the function becomes:
// Put a character at current cursor location procedure put_ch(CH:byte); begin _A:=CH; call(^$F6A4); end;
But on Apple II it looks like:
// Put a character at current cursor location procedure put_ch(CH:byte); begin togglebit(CH,7,1); _A:=CH; call(^$FDED); end;
If we don’t toggle the high bit on Apple II the text outputs, but reversed black on white or even stranger things happen!
TRSE also has
POKE for writing individual bytes of memory, just like in BASIC.
For example, to see which screen mode we are currently using on BBC we can do
Unlike BASIC we can shove around more than one byte at a time:
We can set up an array (
array of byte) to represent the screen data we wish to push to screen and then simply use
petscii_pointer:=#petscii2; CopyFullScreen(petscii_pointer, #$8000);
What About Assembler Programming in TRSE?
What about assembly?
Here is how we (Colin actually) output a string on Amstrad in Z80 Assembler using TRSE:
// Prints the string pointed to by p at the current cursor position. // Control chars are obeyed, not printed. procedure Print(p: global pointer); begin asm(" ld hl, [Text_p] text_print_loop: ld a, (hl) cp 0 jr z, text_print_finished call #bb5a inc hl jp text_print_loop text_print_finished: "); end;
As you can see, the Turbo Rascal function is a wrapper around inline assembler, much the same as you might do in C.
So how does this knowledge impact the game we are building?
Cross-Platform to Custom Per Machine
Just because the core of the game is cross-platform does not mean we need to neglect the abilities of each target machine.
For example, while the Commodore PET has monochrome character-only output, VGA-equipped PCs have 256 colour bitmap output which would be a waste to ignore.
Other machines, such as Vic 20 and C64, allow us to customize the characters to give a graphic feel but still using text functions.
If you saw my C64 BASIC tutorials then you will know what is coming next!
// Set to use the new characterset CopyCharsetFromRom(^$3000); SetCharsetLocation(^$3000); // Destination address dest:=$3000; temp_s:=#alpha; MemCpy(temp_s, 0, dest,27*8);
This loads the default characters for us to then customize, and we copy 8-bytes per character from an Array of the alphabetical font + @ symbol. This seems to happen instantly, unlike when we did the same thing in BASIC!
TRSE does have a level editor but for now let’s use a simple Array to keep things easy.
All we need is an Array 20×20 big. Unfortunately at the time of writing, TRSE doesn’t have two-dimensional arrays, but we can build our map as a string, and the math to turn 2D into a straight list of bytes is easy!
map: string = (" ========== = = # < # = = = = = = = = = = = = = > = = ========#= ");
// Location of screen memory screen:=$400; // Pointer to our map array/string temp_s:=#map; // Loop through each 20-byte row of data for y:=0 to 20 do begin MemCpyFast(temp_s, 0, screen,20); screen:=screen+40; temp_s:=temp_s+20; end;
Yay! Now we have our little adventurous dude in a semi-recognizable dungeon locale we can start to add game features.
But now we are deviating from code-once-compile-many, how do we avoid having to re-write large parts of our program?
We will look at that next time as we build out our game!