I have a hankering to write a full-screen games launcher for DOS, in the same manner as I have done for the NEC PC-98 and Sharp X68000.
I want to target 8086 and up, with 512kb VGA as a minimum (so that I can use 640x480 or 640x400 in 256 colours). So DJGPP and protected mode is out of the question.
I suspect using OpenWatcom (plain C) is probably the best toolset, if I'm not developing on the actual hardware, but what I'm not sure about yet is how to access the base VESA modes (probably 100h/101h) in real mode. Unchained 320x200 is easy, but without a linear framebuffer how do you go about writing to these modes?
It's been a while but as I remember linear framebuffer was preferred mode to use SVGA modes, but optional. It was introduced in 2.0. You should be able to access pixels using banks mapping to 64K windows in real mode address space.
Yes, it's perfectly possible to access SVGA frame buffer in real mode.
The video memory is in segment A000h, pixel format in 256-color modes is like in mode 13h, and the only difference is the need for bank-switching - the segment size is 64 KB, while the frame buffer in 640x480x8bpp is about 300 KB.
There may be, however, one big problem with running an SVGA game on a 8086-class machine: SPEED.
Zaglądali do kufrów, zaglądali do waliz, nie zajrzeli do dupy - tam miałem klimatyzm.
It's been a while but as I remember linear framebuffer was preferred mode to use SVGA modes, but optional. It was introduced in 2.0. You should be able to access pixels using banks mapping to 64K windows in real mode address space.
Yes, I think so.
However, wasn't VESA VBE BIOS 1.x made to be called from Real-Mode and 2.x from Protected-Mode?
"Time, it seems, doesn't flow. For some it's fast, for some it's slow.
In what to one race is no time at all, another race can rise and fall..." - The Minstrel
VBE 2.0 is superset of 1.2. Yes, there is LFB and the new protected mode banking interface for improved performance, but 1.x functions accessed using int 10 are still there.
Chances are that if you target 8086 then you are unlikely to be dealing with 2.0 hardware anyway
However, wasn't VESA VBE BIOS 1.x made to be called from Real-Mode and 2.x from Protected-Mode?
No.
All VBE versions support calling INT 10h from real mode.
Protected mode is only necessary to access the linear frame buffer, as it's located far beyond 1 MB.
And the linear frame buffer is optional.
Zaglądali do kufrów, zaglądali do waliz, nie zajrzeli do dupy - tam miałem klimatyzm.
Grzybwrote on 2021-01-14, 14:17:No.
All VBE versions support calling INT 10h from real mode.
Protected mode is only necessary to access the linear frame buffer, […] Show full quote
However, wasn't VESA VBE BIOS 1.x made to be called from Real-Mode and 2.x from Protected-Mode?
No.
All VBE versions support calling INT 10h from real mode.
Protected mode is only necessary to access the linear frame buffer, as it's located far beyond 1 MB.
And the linear frame buffer is optional.
Ah, I see. Never mind. 😅
I've found online what I thought about.
VBE 2.x sorta got an optional PM interface it seems.
"Time, it seems, doesn't flow. For some it's fast, for some it's slow.
In what to one race is no time at all, another race can rise and fall..." - The Minstrel
Thanks for the pointers (ha!) folks. Just to clarify, this isn't for a game - it's a game browser/launcher interface, so speed isn't an outright priority. I've linked the two previous projects I wrote for a couple of Japanese systems, below:
The X68000 is neat, as the graphics ram is just all completely accessible directly - it's really, really nice; plus you get a lovely 16bit packed-pixel model (well, 15bit plus a 'brightness' bit). The PC-98, in PEGC hardware guise anyway, is far closer to the IBM PC/DOS; earlier machines have a bank switched video hardware, but the PEGC hardware and above has a VESA-like packed-pixel linear framebuffer so it's trivial to write to a buffer in memory and then flip it to the framebuffer as needed.
I can probably leverage most of what I wrote for the PC-98, since it's just a few minor DOS calls (it uses a slightly modified DJGPP library) that differ and the final copy from memory to the framebuffer (in 64kb chunks I see, in real-mode DOS). If I pick 640x400x256 as the display resolution, would it be a fair assumption that almost all VGA cards of 512kb and above that are >= VESA 1.0 compliant should be able to run it?
I'd like it to run on as wide a range of systems as possible, so trying to keep it <640KB with 512kb (Super) VGA hardware in real-mode only.
If I pick 640x400x256 as the display resolution, would it be a fair assumption that almost all VGA cards of 512kb and above that are >= VESA 1.0 compliant should be able to run it?
Yes, even some of those early 256KB SVGA cards support 640x400x256.
However, such cards don't support VBE in their BIOSes, so some TSR would be necessary.
Zaglądali do kufrów, zaglądali do waliz, nie zajrzeli do dupy - tam miałem klimatyzm.
If I pick 640x400x256 as the display resolution, would it be a fair assumption that almost all VGA cards of 512kb and above that are >= VESA 1.0 compliant should be able to run it?
Yes, even some of those early 256KB SVGA cards support 640x400x256.
However, such cards don't support VBE in their BIOSes, so some TSR would be necessary.
I second that, tried it myself a few times also on authentic hardware.
Early cards like the Paradise Professional VGA (PVGA1A/B/.. and WDC90Cxx) did include 640x400x256c drivers for popular programs of the day (AutoCAD, Publisher, Win 2.x, GEM)..
"Time, it seems, doesn't flow. For some it's fast, for some it's slow.
In what to one race is no time at all, another race can rise and fall..." - The Minstrel
Brilliant, that confirms my thoughts of using that mode as a baseline.
I suppose I could have targetted 320x200 which would work on *all* VGA cards, but using that mode to display box artwork, screenshots etc is just too much of a compromise for the "works on anything with VGA" advantage it comes with. It wouldn't be much of a game/art/launcher if every box art or screenshot was something silly like 80 pixels high!
Just got OpenWatcom installed and doing a first pass through of all of the non-hardware related support code I wrote for the PC-98. I've already picked up a few gotchas from the previous C99 code that was written for GCC/DJGPP such as my preprocessor directives for struct alignment not being valid in Watcom, as well as the (annoying) warning about lack of trailing return characters at the end of every file.
So I've gotten some basic stuff up and running to query the VESA BIOS, mainly the INT10 call to retrieve the basic VBE data structure and enumerate the mode list. So far, so good.
There are two functions so far:
vesa_getvbeinfo
1int vesa_getvbeinfo(vbeinfo_t *vbeinfo){ 2 // Retrieve basic VESA BIOS information 3 4 union REGS r; 5 struct SREGS s; 6 7 // Set VESA BIOS call parameters and store pointer 8 // to vbeinfo datastructure which will hold info 9 // after this call. 10 r.x.ax = VESA_BIOS_INFO; 11 r.x.di = FP_OFF(vbeinfo); 12 s.es = FP_SEG(vbeinfo); 13 14 // Call interrupt for VESA BIOS 15 int86x(0x10, &r, &r, &s); 16 17 if (r.x.ax != VESA_BIOS_SUCCESS){ 18 // VESA BIOS call was not successful 19 if (VESA_VERBOSE){ 20 printf("%s.%d\t Error, Unable to query VESA BIOS [return code %x]\n", __FILE__, __LINE__, r.x.ax); 21 } 22 return -1; 23 } 24 25 // Process VESA mode info structure data 26 if (VESA_VERBOSE){ 27 printf("%s.%d\t VESA BIOS queried successfully!\n", __FILE__, __LINE__); 28 vesa_printvbeinfo(vbeinfo); 29 vesa_printvbemodes(vbeinfo); 30 } 31 32 return 0; 33}
vesa_hasmode
1void vesa_hasmode(int mode, vbeinfo_t *vbeinfo){ 2 // Finds if a given VESA mode is present (and supported) 3 // by the current video adapter 4 5 int i; 6 int found; 7 unsigned short int *modes; 8 vesamodeinfo_t *modeinfo = NULL; 9 modeinfo = (vesamodeinfo_t *) malloc(sizeof(vesamodeinfo_t)); 10 11 found = 0; 12 modes = (unsigned short int*) vbeinfo->mode_list_ptr; 13 14 // Find the mode in the list of modes 15 for (i = 0; modes[i] != VESA_MODELIST_LAST; i++){ 16 if (modes[i] == mode){ 17 found = 1; 18 } 19 } 20 21 if found){ 22 vesa_getmodeinfo(mode, modeinfo); 23 if (modeinfo->ModeAttributes != 0){ 24 // Mode present and available 25 return 0; 26 } else { 27 // Mode present, but unavailable (e.g too little RAM) 28 return -1; 29 } 30 } else { 31 // Mode not present 32 return -1; 33 } 34 35}
I'm in the middle of writing the vesa_getmodeinfo function, but that will do the obvious and return the detailed mode data for a given mode, so that the application can determine whether 0x100h is available or not (for example). I suppose the graceful way to detect 640x400x256c support would be to enumerate the mode list for 0x100h first, and then if not found, try all the other modes listed and find another that matches the X and Y resolution and colour depth and allow the user to choose.
The two print routines are trivial, so I won't post them.
Yes, I'm aware there isn't a free() call in that function. There also isn't in the outer graphics library that is calling these vesa functions in turn - but as of right now its only running the once and exiting.
If you are going to use some sort of PutPixel function, buffer the current bank number somewhere and check whether you really have to update it.
A PutPixel function that explicitly sets the bank via VBE call every time is abysmally slow.
I'm using a double buffer, so I have a 256KB buffer in memory, write all of the updates to that, then flush it to VRAM in one operation (which will be in 64KB chunks compared to the X68000 and PC-98). There's no direct setting of pixels, and since there is no animation involved (only a scrollbar moving from one highlighted game to the next, or each games boxart or screenshot loading as it is selected) the latency of doing that once, say, every 500ms is probably going to be good enough. The rest of the application should easily fit in the other 300KB or thereabouts.
It's certainly not how you would do it for something reasonably fast like a game, or if you were memory constrained with a lot of logic, but keeping it this way means that I can keep almost all of the graphics routines (draw box, print bitmap font at this location, flash this text box etc) independent from the actual platform code (be it writing directory to memory mapped IO regions for the Sharp, a linear framebuffer on the NEC, or to paged 64KB video regions on PC/VGA).
I haven't got any graphical output running yet, but all of the utility code (scraping games from named directories, loading/saving config files, parsing keyboard input etc) is working, just like it did on the other platforms.
Got my code querying the VBE, able to interrogate support for individual modes (I'm only interested in 100h for now) and able to set a new mode and return back to the original one.
I think I have one more function to implement, which is the flip of the local buffer to video memory, of course this will involve chunking the local 256KB buffer into 64KB windows in turn.
Once that's done I think most of the rest of the supporting graphics functions which are common for X68K and PC-98 should "just work".
Thanks to my new-found understanding of some of the implicit type casting issues I have taken for granted all my C-programming life, I now have a working set of functions to set the VESA mode and calculate the size of the memory window, number of pixels per window, etc.
What I'm now trying to work out is exactly the relationship between the memory windows (4 as defined in 640x400x256c) and the on screen image.
I don't need to worry about placing individual pixels in the windows - I just need to copy an entire contiguous block of memory to all 4 windows.
I was working on something like this:
1unsigned char *vram; 2unsigned char vram_buffer[256000]; 3long int window_bytes; 4unsigned char *VGA=(unsigned char *)0xA0000000L; 5 6void gfx_Flip(){ 7 // Copy a buffer of GFX_ROWS * GFX_COLS bytes to 8 // the active VRAM framebuffer for display. 9 10 unsigned short int window; 11 12 // Set the vram pointer to the start of the buffer 13 vram = vram_buffer; 14 15 // for each window in the number of windows for this video mode 16 for(window = 0; window < windows_in_use; window++ ){ 17 vesa_SetWindow(window); 18 memcpy(&vga_segment, vram, window_bytes); 19 vram += window_bytes; 20 }; 21 22 // Reset vram buffer pointer position 23 vram = vram_buffer; 24}
I do have the actual segment address of the window, as returned from the VBE mode info, but I'm just hardcoding the VGA segment address for now (it appears that for 99% of cases they'll be the same?).
Should something like this work? Am I right to assume that each window in turn is sequential in relation to screen layout; so the first window will be the first 64kb worth of pixels, then the second window continues on directly from that pixel position, etc.
The attachment progress1.png is no longer available
That should be a single bitmap loaded and placed in the middle of the screen (so at least one of them is correct 🤣!), along with the start of a progress bar and some bitmap font text horizontally centred just above the bottom. It's mostly working without modification from the NEC/Sharp versions.
It looks like I may have some calculations for the copy from my local screen buffer to the VESA windows off a little (hence the duplicated output), and I can see that my palette setting code isn't quite right (the colours on the font are reversed, and the progress bar should be a cyan-like shade of blue).