ghaerr / elks

Embeddable Linux Kernel Subset - Linux for 8086
Other
983 stars 106 forks source link

Kernel system call for some BIOS functions #1648

Closed toncho11 closed 1 year ago

toncho11 commented 1 year ago

This is a continuation of the discussion on how a BIOS call can be properly executed in ELKS. Maybe not all BIOS calls should be allowed, but just certain such as 10h BIOS call that sets the screen mode.

I found this: https://stackoverflow.com/questions/23338332/possible-to-use-bios-interrupts-in-your-code-on-linux

So in Linux there is/was a system call for that for real mode which is the case with ELKS.

ghaerr commented 1 year ago

In the previous discussion, it was stated that BIOS calls are generally unsafe from ELKS applications, which is true, when the kernel may also be performing disk I/O.

However, I'm not sure its worth the effort to add this capability into the kernel, since a simple INT 10h mode change could be made safe by just disabling interrupts before the call, if this is the only call the application will be making.

Thus, I suggest perhaps using libi186 after all and trying the mode change (both to graphics, maybe draw a line, then back), for initial testing. The problem you'll run into quickly is that if there are any problems, the system will have to be rebooted (unless you have a serial console running to kill the app), as the console will be in graphics mode and no text will be displayable.

toncho11 commented 1 year ago

Why not a system call? Because it will increase the size of the kernel memory?

But disabling the interrupts means that they are ignored by the cpu. Wouldn't that make the IO operations fail?

You mean I should use cli before the 10h call and then sti in a gcc inline assembly?

__asm__("cli;" 
"mov %al,0;" 
"mov %ah,0x13;"
"int $0x10;"
"sti;");

Does it look even remotely OK?

toncho11 commented 1 year ago

Or better use int10 from elksutil.h:

__asm__("cli;");
int10(0x13,0);
__asm__("sti;");
ghaerr commented 1 year ago

Why not a system call? Because it will increase the size of the kernel memory?

A number of reasons: yes, extra overhead, the kernel then becomes somewhat responsible for graphics modes that aren't supported anywhere else, adding more dependencies to the kernel... at this time, probably better to get things working from an application program before integrating into the kernel. And we already have Nano-X.

But disabling the interrupts means that they are ignored by the cpu. Wouldn't that make the IO operations fail?

There wouldn't be any floppy or disk I/O, since those are all synchronous. It could lead to loss of fast incoming serial characters, but the network drivers already disable interrupts for relatively long periods (during transfers of packet data to/from NIC, for example). The CLI would stop the hardware timer from interrupting the process time slice, and it'd only happen once, right when the user started the program. All the graphics would then be drawn directly by the program (just like Nano-X does), without disabling interrupts.

You mean I should use cli before the 10h call and then sti in a gcc inline assembly? Does it look even remotely OK?

Yes, that's exactly what I had in mind.

Or better use int10 from elksutil.h:

Not sure where elksutil.h comes from, probably better to do the whole thing in one asm call.

toncho11 commented 1 year ago

But I should be saving and restoring registers for the 10h call? It looks like this is what int10 from elksutil.h does.

Thank you @ghaerr !

ghaerr commented 1 year ago

But I should be saving and restoring registers for the 10h call?

Oops - yes. You'll want to push and pop SI, DI, BP, ES and probably DS just in case. AX, BX, CX and DX are assumed scratch and SS won't change.

toncho11 commented 1 year ago

This is what I am working on, specifically the function set_mode :

/**************************************************************************
 * lines.c                                                                *
 * written by David Brackeen                                              *
 * http://www.brackeen.com/home/vga/                                      *
 *                                                                        *
 * This is a 16-bit program.                                              *
 * Tab stops are set to 2.                                                *
 * Remember to compile in the LARGE memory model!                         *
 * To compile in Borland C: bcc -ml lines.c                               *
 *                                                                        *
 * This program will only work on DOS- or Windows-based systems with a    *
 * VGA, SuperVGA or compatible video adapter.                             *
 *                                                                        *
 * Please feel free to copy this source code.                             *
 *                                                                        *
 * DESCRIPTION: This program demostrates drawing how much faster it is to *
 * draw lines without using multiplication or division.                   *
 **************************************************************************/

#include <stdio.h>
#include <stdlib.h>
//#include <dos.h>
#include "vgaplan4.h" //TODO: needed for int10, the implementation should be added at compile level - not tested

#define VIDEO_INT           0x10      /* the BIOS video interrupt. */
#define SET_MODE            0x00      /* BIOS func to set the video mode. */
#define VGA_256_COLOR_MODE  0x13      /* use to set 256-color mode. */
#define TEXT_MODE           0x03      /* use to set 80x25 text mode. */

#define SCREEN_WIDTH        320       /* width in pixels of mode 0x13 */
#define SCREEN_HEIGHT       200       /* height in pixels of mode 0x13 */
#define NUM_COLORS          256       /* number of colors in mode 0x13 */

#define sgn(x) ((x<0)?-1:((x>0)?1:0)) /* macro to return the sign of a
                                         number */
typedef unsigned char  byte;
typedef unsigned short word;

byte *VGA=(byte *)0xA0000000L;        /* this points to video memory. */
word *my_clock=(word *)0x0000046C;    /* this points to the 18.2hz system
                                         clock. */

/**************************************************************************
 *  set_mode                                                              *
 *     Sets the video mode.                                               *
 **************************************************************************/

void set_mode(byte mode)
{
   __asm__("cli;");
   int10(mode,0);
   __asm__("sti;");
}

/**************************************************************************
 *  plot_pixel                                                            *
 *    Plot a pixel by directly writing to video memory, with no           *
 *    multiplication.                                                     *
 **************************************************************************/

void plot_pixel(int x,int y,byte color)
{
  /*  y*320 = y*256 + y*64 = y*2^8 + y*2^6   */
  VGA[(y<<8)+(y<<6)+x]=color;
}

/**************************************************************************
 *  line_slow                                                             *
 *    draws a line using multiplication and division.                     *
 **************************************************************************/

void line_slow(int x1, int y1, int x2, int y2, byte color)
{
  int dx,dy,sdx,sdy,px,py,dxabs,dyabs,i;
  float slope;

  dx=x2-x1;      /* the horizontal distance of the line */
  dy=y2-y1;      /* the vertical distance of the line */
  dxabs=abs(dx);
  dyabs=abs(dy);
  sdx=sgn(dx);
  sdy=sgn(dy);
  if (dxabs>=dyabs) /* the line is more horizontal than vertical */
  {
    slope=(float)dy / (float)dx;
    for(i=0;i!=dx;i+=sdx)
    {
      px=i+x1;
      py=slope*i+y1;
      plot_pixel(px,py,color);
    }
  }
  else /* the line is more vertical than horizontal */
  {
    slope=(float)dx / (float)dy;
    for(i=0;i!=dy;i+=sdy)
    {
      px=slope*i+x1;
      py=i+y1;
      plot_pixel(px,py,color);
    }
  }
}

/**************************************************************************
 *  line_fast                                                             *
 *    draws a line using Bresenham's line-drawing algorithm, which uses   *
 *    no multiplication or division.                                      *
 **************************************************************************/

void line_fast(int x1, int y1, int x2, int y2, byte color)
{
  int i,dx,dy,sdx,sdy,dxabs,dyabs,x,y,px,py;

  dx=x2-x1;      /* the horizontal distance of the line */
  dy=y2-y1;      /* the vertical distance of the line */
  dxabs=abs(dx);
  dyabs=abs(dy);
  sdx=sgn(dx);
  sdy=sgn(dy);
  x=dyabs>>1;
  y=dxabs>>1;
  px=x1;
  py=y1;

  VGA[(py<<8)+(py<<6)+px]=color;

  if (dxabs>=dyabs) /* the line is more horizontal than vertical */
  {
    for(i=0;i<dxabs;i++)
    {
      y+=dyabs;
      if (y>=dxabs)
      {
        y-=dxabs;
        py+=sdy;
      }
      px+=sdx;
      plot_pixel(px,py,color);
    }
  }
  else /* the line is more vertical than horizontal */
  {
    for(i=0;i<dyabs;i++)
    {
      x+=dxabs;
      if (x>=dyabs)
      {
        x-=dyabs;
        px+=sdx;
      }
      py+=sdy;
      plot_pixel(px,py,color);
    }
  }
}

/**************************************************************************
 *  Main                                                                  *
 *    Draws 5000 lines                                                    *
 **************************************************************************/

void main()
{
  int x1,y1,x2,y2,color;
  float t1,t2;
  word i,start;

  srand(*my_clock);                   /* seed the number generator. */
  set_mode(VGA_256_COLOR_MODE);       /* set the video mode. */

  start=*my_clock;                    /* record the starting time. */
  for(i=0;i<5000;i++)                 /* randomly draw 5000 lines. */
  {
    x1=rand()%SCREEN_WIDTH;
    y1=rand()%SCREEN_HEIGHT;
    x2=rand()%SCREEN_WIDTH;
    y2=rand()%SCREEN_HEIGHT;
    color=rand()%NUM_COLORS;
    line_slow(x1,y1,x2,y2,color);
  }

  t1=(*my_clock-start)/18.2;          /* calculate how long it took. */

  set_mode(VGA_256_COLOR_MODE);       /* set the video mode again in order
                                         to clear the screen. */

  start=*my_clock;                    /* record the starting time. */
  for(i=0;i<5000;i++)                 /* randomly draw 5000 lines. */
  {
    x1=rand()%SCREEN_WIDTH;
    y1=rand()%SCREEN_HEIGHT;
    x2=rand()%SCREEN_WIDTH;
    y2=rand()%SCREEN_HEIGHT;
    color=rand()%NUM_COLORS;
    line_fast(x1,y1,x2,y2,color);
  }

  t2=(*my_clock-start)/18.2;          /* calculate how long it took. */
  set_mode(TEXT_MODE);                /* set the video mode back to
                                         text mode. */

  /* output the results... */
  printf("Slow line drawing took %f seconds.\n",t1);
  printf("Fast line drawing took %f seconds.\n",t2);
  if (t2 != 0) printf("Fast line drawing was %f times faster.\n",t1/t2);

  return;
}
toncho11 commented 1 year ago

http://vitaly_filatov.tripod.com/ng/asm/asm_023.1.html

void set_mode(byte mode)
{
   __asm__("cli;" 
  "mov %ah,0;" 
  "mov %al,%0;"
  "int $0x10;"
  "sti;" 
     : /* no outputs */
     : "r" (mode)
     : "AX", "SP", "BP", "SI", "DI" ); //modified registers
}
ghaerr commented 1 year ago

: "AX", "SP", "BP", "SI", "DI" ); //modified registers

I recommend an explicit save of the registers, rather than just telling the compiler that they've changed. The SI, DI and BP register must be saved explicitly, as previously called functions expect their copies of SI/DI to be valid.

This is what I am working on, specifically the function set_mode :

Yes, I can see how that works. There will need to be possibly heavy modification for that to work on original EGA cards. You should probably start with a very small ELKS program that just changes the video mode to graphics, sleeps for 3 seconds, then changes it back. After that, start adding functions from the above file.

toncho11 commented 1 year ago

I thought the compiler will take care of saving them if they are marked as modified?

I am using mode 13h: 13h = G 40x25 8x8 320x200 256/256K . A000 VGA,MCGA,ATI VIP which is VGA and not EGA? The code of the program is known to work on DOS, so if the screen mode is set correctly and the video memory is accessible then it should work. I intend to test on a VGA card.

Interestingly plotting a pixel does not require a BIOS call.

void plot_pixel(int x,int y,byte color)
{
  /*  y*320 = y*256 + y*64 = y*2^8 + y*2^6   */
  VGA[(y<<8)+(y<<6)+x]=color;
}

It seems just changing the color in the video memory is enough to plot a pixel.

ghaerr commented 1 year ago

I thought the compiler will take care of saving them if they are marked as modified?

No. It just tells the compiler that those registers have been changed, so that it can deal with that with subsequent code being genarated for the current function.

which is VGA and not EGA? The code of the program is known to work on DOS.

What we're talking about here is the hardware, not the BIOS call. The BIOS will switch modes appropriately, but the EGA and VGA operate quite differently in graphics modes.

so if the screen mode is set correctly and video memory accessible then it should work.

Yes, the INT 10h mode switch will work. I'm talking about the pixel drawing.

Interestingly plotting a pixel does not require a BIOS call.

That's right. BIOS is not required to draw pixels. Instead, the video RAM is accessed directly.

Welcome to graphics! It can get quite complicated though.

It seems just changing the color in the video memory is enough to plot a pixel.

Yes - in VGA with the appropriate driver setup, one can just change video memory with a single byte. That's called framebuffer. Earlier systems, including EGA, require much more complicated fooling around. Please take a look at vgaplan4.c to see. (The file should be named egaplan4.c. "plan4" is short for "4 planes". Each color bit has to be written in a separate "plane", instead of a single byte, thus the complications. You might also read up on EGA hardware (not VGA).

The VGA code you are looking at just "draws" directly by accessing video memory, and changing a single byte to change a color. That won't work on EGA. Also, the code won't work directly on ELKS even with VGA hardware as written, as the byte *VGA will need to be declared byte __far *VGA. All this is why I recommend starting tiny and taking a step at a time, as it will get complicated.

Bottom line - look at the Nano-X "driver" for EGA - scr_bios.c and vgaplan4.c these can be taken apart, copied and reused in your own smaller ELKS program. Take your time to study them, and then I'll help you create much smaller version of the code you can use in your own ELKS program, and you can start drawing pixels yourself :)

ghaerr commented 1 year ago

define VGA_256_COLOR_MODE 0x13 / use to set 256-color mode. /

I just noticed this. EGA doesn't support the "VGA 256 color mode", but if your system has a VGA, then it will work. So the program isn't portable but hardware dependent. The Nano-X code I suggested you study is for 16-color EGA, which will work on ALL PC systems (except monochrome). So you need to figure out what system(s) you want to test and run your graphics software on, and then we can start building a graphics library from there.

toncho11 commented 1 year ago

I got it. Thank you! And it should be medium or large (not sure it exists on ELKS) model for the program during the compilation?

ghaerr commented 1 year ago

And it should be medium or large (not sure it exists on ELKS) model for the program during the compilation?

Neither. Large model isn't supported and medium is just for large code segments. You'll be creating a standard small-model ELKS program. Start there, get "Hello World" running, then add the INT 10h graphics on/sleep/off and we'll go from there.

toncho11 commented 1 year ago

I think that for int 10 in the case of AH=0 (screen mode selection) only the AX register should be saved?

#include <stdio.h>
#include <stdlib.h>

#define VIDEO_INT           0x10      /* the BIOS video interrupt. */
#define VGA_256_COLOR_MODE  0x13      /* use to set 256-color mode. */
#define TEXT_MODE           0x03      /* use to set 80x25 text mode. */

#define SCREEN_WIDTH        320       /* width in pixels of mode 0x13 */
#define SCREEN_HEIGHT       200       /* height in pixels of mode 0x13 */
#define NUM_COLORS          256       /* number of colors in mode 0x13 */

#define sgn(x) ((x<0)?-1:((x>0)?1:0)) /* macro to return the sign of a
                                         number */
typedef unsigned char  byte;
typedef unsigned short word;

byte __far *VGA=(byte *)0xA0000000L;        /* this points to video memory. */

/**************************************************************************
 *  set_mode                                                              *
 *     Sets the video mode.                                               *
 **************************************************************************/

void set_mode(byte mode)
{
   // SI, DI, BP, ES and probably DS
   __asm__(
  "cli;"
  "push %%ax;"
  "mov %%ah,0;" 
  "mov %%al,%0;"
  "int $0x10;"
  "pop %%ax;"
  "sti;"
     : /* no outputs */
     : "r" (mode)
     : "ax" ); //modified registers
}

/**************************************************************************
 *  plot_pixel                                                            *
 *    Plot a pixel by directly writing to video memory, with no           *
 *    multiplication.                                                     *
 **************************************************************************/

void plot_pixel(int x,int y,byte color)
{
  /*  y*320 = y*256 + y*64 = y*2^8 + y*2^6   */
  VGA[(y<<8)+(y<<6)+x]=color;
}

/**************************************************************************
 *  line_slow                                                             *
 *    draws a line using multiplication and division.                     *
 **************************************************************************/

void main()
{
  set_mode(VGA_256_COLOR_MODE);       /* set the video mode again in order
                                         to clear the screen. */

  plot_pixel(100,100,5);

  set_mode(TEXT_MODE);                /* set the video mode back to
                                         text mode. */

  return;
}
ghaerr commented 1 year ago

I think that for int 10 in the case of AH=0 (screen mode selection) only the AX register should be saved?

No, you're calling a function that could trash some registers, which would cause C code to crash. Not all BIOSes are created equal, and many early versions didn't save registers.

SI, DI, BP and ES need to be saved. AX, BX, CX and DX are scratch so they don't need to be. AX is the return value for ASM functions called by C.

toncho11 commented 1 year ago

With the code below I can switch to VGA mode on a 486 machine, but I can not draw lines. It seems the access to the video memory is not correct. It seems to corrupt the memory. Going back to text mode is semi successful when drawing lines.

Compilation: ../cross/bin/ia16-elf-gcc ./vgatest.c -o vgatest -melks-libc -mcmodel=small

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

//#define VIDEO_INT           0x10      /* the BIOS video interrupt. */
#define VGA_256_COLOR_MODE  0x13      /* use to set 256-color mode. */
#define TEXT_MODE           0x03      /* use to set 80x25 text mode. */

#define SCREEN_WIDTH        320       /* width in pixels of mode 0x13 */
#define SCREEN_HEIGHT       200       /* height in pixels of mode 0x13 */
#define NUM_COLORS          256       /* number of colors in mode 0x13 */

#define sgn(x) ((x<0)?-1:((x>0)?1:0)) /* macro to return the sign of a
                                         number */
typedef unsigned char  byte;
typedef unsigned short word;

byte __far *VGA=(byte *)0xA0000000L;        /* this points to video memory. */

/**************************************************************************
 *  set_mode                                                              *
 *     Sets the video mode.                                               *
 **************************************************************************/

void set_mode(byte mode)
{
   // SI, DI, BP, ES and probably DS
   __asm__(
  "push %%si;"
  "push %%di;"
  "push %%bp;"
  "push %%es;"
  "mov %%ah,0;" 
  "mov %%al,%0;"
  "int $0x10;"
  "pop %%es;"
  "pop %%bp;"
  "pop %%di;"
  "pop %%si;"
     : /* no outputs */
     : "r" (mode)
     : ); //modified registers
}

/**************************************************************************
 *  plot_pixel                                                            *
 *    Plot a pixel by directly writing to video memory, with no           *
 *    multiplication.                                                     *
 **************************************************************************/

void plot_pixel(int x,int y,byte color)
{
  /*  y*320 = y*256 + y*64 = y*2^8 + y*2^6   */
  VGA[(y<<8)+(y<<6)+x]=color;
}

/**************************************************************************
 *  line_slow                                                             *
 *    draws a line using multiplication and division.                     *
 **************************************************************************/

int main()
{
  set_mode(VGA_256_COLOR_MODE);       /* set the video mode again in order to clear the screen. */

  for (int i=0;i<60;i++)
    plot_pixel(100+i,100,5);

  for (int i=0;i<60;i++)
        plot_pixel(100,100+i,0xA);

  sleep(3);

  set_mode(TEXT_MODE);                /* set the video mode back to text mode. */

  return 0;
}
ghaerr commented 1 year ago

It seems the access to the video memory is not correct.

Yes - the declaration of your far pointer to VGA memory should be changed to:

byte __far *VGA = (byte __far *)0xA0000000L;        /* this points to video memory. */

In your code second cast causes the long value on the far right to be cast to sizeof (byte *) == 16, thus setting the VGA pointer to 0.

I don't quite understand why you're seeing data corruption, as the far pointer to 0:0 should just crash the system, but instead it looks like the pointer is being set to DS:0, which would overwrite your data segment. In any case, try the above fix.

toncho11 commented 1 year ago

Wow. It works!!! Thanks @ghaerr !!! It draws the two lines. Before it was corrupting predefined strings in the data segment. Which made me think the vga pointer is incorrect.

Below is the final code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

//#define VIDEO_INT           0x10      /* the BIOS video interrupt. */
#define VGA_256_COLOR_MODE  0x13      /* use to set 256-color mode. */
#define TEXT_MODE           0x03      /* use to set 80x25 text mode. */

#define SCREEN_WIDTH        320       /* width in pixels of mode 0x13 */
#define SCREEN_HEIGHT       200       /* height in pixels of mode 0x13 */
#define NUM_COLORS          256       /* number of colors in mode 0x13 */

#define sgn(x) ((x<0)?-1:((x>0)?1:0)) /* macro to return the sign of a
                                         number */
typedef unsigned char  byte;
typedef unsigned short word;

byte __far *VGA=(byte __far *)0xA0000000L;        /* this points to video memory. */

/**************************************************************************
 *  set_mode                                                              *
 *     Sets the video mode.                                               *
 **************************************************************************/

void set_mode(byte mode)
{
   // SI, DI, BP, ES and probably DS
   __asm__(
  "push %%si;"
  "push %%di;"
  "push %%bp;"
  "push %%es;"
  "cli;"
  "mov %%ah,0;" 
  "mov %%al,%0;"
  "int $0x10;"
  "sti;"
  "pop %%es;"
  "pop %%bp;"
  "pop %%di;"
  "pop %%si;"
     : /* no outputs */
     : "r" (mode)
     : ); //modified registers
}

/**************************************************************************
 *  plot_pixel                                                            *
 *    Plot a pixel by directly writing to video memory, with no           *
 *    multiplication.                                                     *
 **************************************************************************/

void plot_pixel(int x,int y,byte color)
{
  /*  y*320 = y*256 + y*64 = y*2^8 + y*2^6   */
  VGA[(y<<8)+(y<<6)+x]=color;
}

int main()
{
  set_mode(VGA_256_COLOR_MODE);       /* set the video mode again in order to clear the screen. */

  for (int i=0;i<60;i++)
    plot_pixel(100+i,100,5);

  for (int i=0;i<60;i++)
        plot_pixel(100,100+i,0xA);

  sleep(3);

  set_mode(TEXT_MODE);                /* set the video mode back to text mode. */

  return 0;
}

Tested with this emulator https://copy.sh/v86/ with this image fd1440-fat.zip

ghaerr commented 1 year ago

Good news @toncho11! Now that you've got pixel drawing working, you're on your way to more extravagant functions and your own graphics library :) I'll bet your next functions will be draw line and rectangle draw/fill... Those will start the process of worrying about fast code, as filling shows off the good and bad aspects of software and hardware solutions.

I'm happy to help you along the way. I'm wondering if we ought to close this issue, since its name is a bit off-topic, and then have you open another one as you proceed into having fun with drawing?

toncho11 commented 1 year ago

I will probably start a wiki page and I will close this issue later. I just do not have the time right now.

toncho11 commented 1 year ago

@ghaerr Do you think it is a good idea to add the above code as a small program called vgatest in elkscmd/vga? It will be a very small executable. I will refer to this code from the wikipage I am about to write.

Later elkscmd/vga can be home for my future graphics library.

ghaerr commented 1 year ago

Do you think it is a good idea to add the above code as a small program called vgatest in elkscmd/vga

We recently moved all the "test" programs that don't actually do much over to elkscmd/test, where they are also automatically added to the distribution if CONFIG_APP_TEST=y. This keeps the problem of running out of disk image space at bay. So for now, until you really get going on a library, I would suggest putting all your stuff in elkscmd/test/vga, where it won't have to be maintained as a normal application. You can easily add the vga directory to the SUBDIRS= in elkscmd/test/Makefile, then do whatever you want in elkscmd/test/vga/Makefile, etc.

When the vga library becomes more useful, it could be moved into elkscmd/vga. There are still some issues as many machines might not support VGA (only EGA) so additional testing may be required before switching modes, or perhaps checking the return code from INT 10h if there is one.

toncho11 commented 1 year ago

I see. But the idea is not only to test ELKS, but to test if the hardware on which ELKS is running supports a selected VGA mode through the BIOS and whether the video memory is at 0xA0000000L. I will put the code in my wikipage.

toncho11 commented 1 year ago

I started the Games wiki page.