[net.micro.pc] High Speed Console I/O in MS-DOS

dan@srs.UUCP (Dan Kegel) (10/31/86)

Quick console output on MS-DOS
(or, It's Better When It's Raw)

The "Inside the IBM PC" issue of BYTE (October 1986) makes it painfully
obvious that many people, contributing authors to BYTE included, still don't 
know that MS-DOS is capable of extremely high speed I/O to/from character
devices.  This note tries to remedy this by showing an estimate of the delays 
involved in performing output to a character device, and shows how to 
speed up output by a factor of 10 to 100.

Writing to standard output when the device driver ANSI.SYS is installed
costs about (N * 1700) microseconds (on a 4.77 MHz 8088 system), 
where N is the number of characters written (measured without scrolling).
What takes so long?  
Usually, when DOS is called to write N bytes to the console, it sits
in a tight loop:
    dos_write() {
	for each character {
	    call device driver write routine to write 1 character
	    check for ^C by calling device driver status routine
	}
    }
However, if the device has been put into RAW mode with the DOS IOCTL 
function call [footnote 2], DOS will handle a write request differently:
    dos_write() {
	call device driver write routine to write N characters
    }
This gets rid of the overhead of 1-2 device driver calls per character,
and moves the inner loop to the inside of the device driver.
Setting RAW mode brings the cost of writing to ANSI.SYS down to about
(600 + N * 1100) microseconds.  That's better, but still not very good.
Disassembly of ANSI.SYS shows that its inner loop is slow mostly because
it calls BIOS to write to the screen- and BIOS calls are notoriously
slow.  

What can be done?  The best solution is to write an efficient device 
driver which bypasses BIOS, and writes directly to video memory.
This has been done [footnote 1]; writing to such a device driver in
RAW mode takes about (1000 + N * 70) microseconds per character.
Note that these measurements were made under non-scrolling conditions;
scrolling is (usually) a fairly slow process.  Fortunately, most screen-
intensive applications- for example, games, spreadsheets, and text 
editors- can usually avoid scrolling the screen.  (Advanced ANSI functions 
such as Insert N Lines and Delete N Lines, provided by both the device 
drivers in footnote 1, can make it easier to avoid scrolling.)

These measurements also assume that the entire write is done with a single
call to DOS; if your program calls DOS once for each character written, 
output will be extremely slow whether or not the device is in RAW mode.

If you are using C, it helps to use buffered output rather than unbuffered
output; usually (on MS-DOS) stdout defaults to being unbuffered.  You can
force it to be buffered with the following code:
    #include <stdio.h>
    static char stdoutbuf[BUFSIZ];
    main()
    {
	setbuf(stdout, stdoutbuf);
	...
    }
If you do this, you will have to call fflush(stdout) whenever you finish
updating a screen or after writing a user prompt.

In short, all that is needed to achieve high-speed output to devices
is (1) set RAW mode during output [see footnote 2],
   (2) use the proper device driver [footnote 1],
   (3) avoid scrolling, and
   (4) write more than one character per DOS call, preferably several lines'
       worth (which, in C, means using setbuf(), fflush(), and buffered
       output).

--- Daniel Kegel
...seismo!rochester!srs!dan

---------------------------------------

[Footnote 1]
The device driver FANSI, aka FCONSOLE.DEV, is a fancy commercial product 
which offers speedy output during RAW mode.  It is advertized in BYTE.

The device driver NANSI.SYS is a simpler program in the public domain which
has the advantage that it is free and comes with sources; it is available
occasionally over Usenet (on net.micro.pc), and probably on Compuserve.

Both of these drivers offer many features not found in the standard ANSI.SYS,
which was written by somebody who loathed speed and functionality.

---------------------------------------

[Footnote 2]
Setting RAW mode
The following Microsoft C subroutines can be used to set or reset RAW mode on
the device(s) associated with stdin and stdout (usually, the console).
Note that RAW mode is not really intended to be left on all the time; when
RAW mode is on, echoing and line editing is disabled- which can cause some 
programs to appear to crash when asking for input from the console.
Applications should always restore the RAWness of stdin/stdout when exiting.

/*--- setraw.c -------------------------------------------------
  Routines to set and reset raw mode on stdin/stdout for Microsoft C.
  Stolen from the sources to the mighty game Hack.
  Thanks to Mark Zbikowski (markz@microsoft.UUCP).
------------------------------------------------------------------*/
#include <dos.h>
#include <stdio.h>

#define DEVICE		0x80
#define RAW		0x20
#define IOCTL		0x44
#define STDIN		fileno(stdin)
#define STDOUT		fileno(stdout)
#define GETBITS		0
#define SETBITS		1

static unsigned	old_stdin, old_stdout, ioctl();

#define BREAKCHECK	0x33

static unsigned old_breakchk, breakctl();

/*--- set_raw ---------------------------------------------------------
  Call this to set raw mode; call restore_raw() later to restore
  console to old rawness state.  (Don't strand user in RAW mode!)
  Not only sets raw mode, but also turns off ^C trapping on random DOS calls.
  All character input from stdin should be done with DOS fn call 7 to
  avoid ^C trapping on input.
-----------------------------------------------------------------------*/
set_raw()
{
	old_stdin = ioctl(STDIN, GETBITS, 0);
	old_stdout = ioctl(STDOUT, GETBITS, 0);
	old_breakchk = breakctl(GETBITS, 0);
	if (old_stdin & DEVICE)
		ioctl(STDIN, SETBITS, old_stdin | RAW);
	if (old_stdout & DEVICE)
		ioctl(STDOUT, SETBITS, old_stdout | RAW);
	(void) breakctl(SETBITS, 0);
}

restore_raw()
{
	if (old_stdin)
		(void) ioctl(STDIN, SETBITS, old_stdin);
	if (old_stdout)
		(void) ioctl(STDOUT, SETBITS, old_stdout);
	if (old_breakchk)
		(void) breakctl(SETBITS, old_breakchk);
}

static unsigned
ioctl(handle, mode, setvalue)
unsigned setvalue;
{
	union REGS regs;

	regs.h.ah = IOCTL;
	regs.h.al = mode;
	regs.x.bx = handle;
	regs.h.dl = setvalue;
	regs.h.dh = 0;			/* Zero out dh */
	intdos(&regs, &regs);
	return (regs.x.dx);
}

/* Being sensitive to ^C is quite hazardous for a text editor */
/* This could and perhaps should have been done via the SIGNAL mechanism */
static unsigned
breakctl(mode, setvalue)
unsigned setvalue;
{
	union REGS regs;

	regs.h.ah = BREAKCHECK;
	regs.h.al = mode;
	regs.h.dl = setvalue;
	intdos(&regs, &regs);
	return (regs.x.dx & 0xff);
}

/*------ end of setraw.c ----------------------------------------------*/