View Full Version : C++ direct screen writes instead of gotoxy/printf?

Mike Chambers
November 14th, 2009, 12:14 PM
i've moved my apple ][ emulator to Turbo C++ 3.0 and it's working fine so far, however in my screen write routines i'm using:

gotoxy(xp, yp);

and it's rather slow relatively speaking. it's tolerable, but i want to squeeze more performance out of it. i want to make direct screen writes into segment 0xB800. i'm sure this involves pointers.

how do i specify the segment of a pointer, is it just linear from 0 bytes all the way up to 640 KB in DOS? like seg * 16 + offset???

this IS vintage related, i'm programming it on a 386.

November 14th, 2009, 12:42 PM
To use a segment+offset address in Turbo C you put the segment in the upper 16-bits of a pointer variable and the offset in the lower 16-bits.

So segment B800, offset 0000 gives you a value of 0xB8000000.

You need to declare this as something like:

char* far screen = 0xB8000000;

November 14th, 2009, 12:49 PM
Welcome my son .. I've been awaiting your return...

I think your code is potentially unsafe - you are supposed to have a format string as the first parameter. If the next byte after your byte is not a null (0x00) you are going to have all sorts of problems. This is more correct:

gotoxy( xp, yp );
putchar( byte );

Gotoxy isn't a bad choice of function - it uses the BIOS calls. But the printf is going to be horribly slow because it is using stdout. There is a different method of writing to the screen called 'console I/O' that you can use - it looks very similar to printf, but it writes directly to the screen using BIOS calls or memory locations instead of writing to stdout. Take a look in conio.h for cprintf.

The function you really want is putch:

gotoxy( xp, yp );
putch( byte );

There is also a related global variable used with cprintf, putch and the related conio functions. The variable is called directvideo. Make sure that it is set to 1 to get the fastest performance. (When set to zero it uses BIOS calls, and when set to 1 it uses memory operations.) This code should do it for you:

// Put this at the top of the file after your #include statements
extern "C" int directvideo;

// Put this somewhere in main
directvideo = 1;

I use a unsigned char far * pointer to get at the screen base. Unsigned char is probably overkill, but it really is unsigned data so why not. To generate a pointer to the start of the B800 segment I use the MK_FP macro, which is defined in the online help.

unsigned char far *Screen_base = (unsigned char far *)MK_FP( 0xB800, 0 );

By putting the far keyword in there, we are telling Turbo C++ that no matter what the default pointer type is, this pointer is going to be a far pointer. So it has a segment and offset component. As far as you are concerned, it is just a normal pointer - you can add to it to move it forward. But when it hits the end of the 64K segment, it will wrap around, so be careful. For video buffer work this should not be an issue.

When you get really fancy, you won't bother with gotoxy unless you want the cursor placed. It has to make a BIOS call to move the cursor, which is expensive. You can compute the offset to put a character at using this formula:

// Assuming x and y are 1 based and you have an 80 column screen
unsigned char far *target = Screen_base + (y-1) * 160 + (x-1)*2;

And of course you can make the code faster by substituting in shifting instead of integer multiplication:

unsigned char far *target = Screen_base + ((y-1)<<7 + (y-1)<<5) + (x-1)<<1;

And if you can get rid of the 1 based silliness and start counting at zero you can get rid of 3 subtracts there.

That pointer will point at the character cell. The next immediate address is the attribute field for that character cell - don't forget to set it too. For normal white on black the value 0x07 works.

All together now ...

int x=40;
int y=12;
unsigned char far *target = Screen_base + ((y-1)<<7 + (y-1)<<5) + (x-1)<<1;
*target = byte;
*(target+1) = 0x07; // Attribute

Look at the IRCjr code I gave you for more complex operations like screen clearing.

Part of the reason my telnet client is taking so long is that I'm paying a lot of attention to screen handling performance. It's even more important than with IRC. Can you tell I've been studying lately? :-)


Mike Chambers
November 14th, 2009, 03:19 PM
i tried all those, thanks. the one using the screen_base pointer stuff is REALLY FAST!

i'm getting appoximately 1/20th the performance of my real apple //e on my XT clone! (which amazing on a 4.77 MHz PC to emulate a ~1.5(?) MHz 6502 machine)

November 16th, 2009, 07:56 PM
For the perverse:

struct { unsigned char chr, attr; } (far * display)[80] = MK_FP(...);

display[23][79].attr ^= 0x77;