Page 1 of 1

Problems with character rendering

Posted: Wed May 25, 2022 11:23 pm
by ThatCodingGuy89
The problem is that nothing is displayed whatsoever. I've tried both normally callling the terminal_writestring() function,
and directly calling terminal_putc(), and neither display anything. Here's the relevant code & a link to the Github repository.

For context, this is in a UEFI environment after ExitBootServices has been called.

bootloader_tty.h:

Code: Select all

#include "font.h"

uint64_t* framebuffer_addr = 0;
uint32_t pitch = 0;
int framebuffer_width = 0;
int framebuffer_height = 0;

int cursorX = 0;
int cursorY = 0;

int consoleHeight = -1;
int consoleWidth = -1;

// Initilizes the "terminal" with the given framebuffer address and pitch
void terminal_initialize(uint64_t* _framebuffer_addr, uint32_t _pitch, int width, int height)
{
    framebuffer_addr = _framebuffer_addr;
    pitch = _pitch;
    framebuffer_width = width;
    framebuffer_height = height;

    consoleHeight = framebuffer_height / 8; // Console height is framebuffer height / 8 because the characters are 8 pixels in size
    consoleWidth = framebuffer_width / 8; // Console width is the same
}

// Prints character 'c' at X, Y
void terminal_putc(char c, int x, int y, int fgcolor)
{
	uint64_t *dest;
	uint64_t *dest32;
	unsigned char *src;
	int row;
	uint32_t fgcolor32;
 
	fgcolor32 = fgcolor | (fgcolor << 8) | (fgcolor << 16) | (fgcolor << 24);
	src = &font[0] + c * 16;
	dest = framebuffer_addr + y * (pitch * 32) + x;
	for(row = 0; row < 8; row++) {
		if(*src != 0) {
			uint32_t mask_low = font_mask[*src][0];
			uint32_t mask_high = font_mask[*src][1];
			dest32 = dest;
			dest32[0] = (dest[0] & ~mask_low) | (fgcolor32 & mask_low);
			dest32[1] = (dest[1] & ~mask_high) | (fgcolor32 & mask_high);
		}
		src++;
		dest += (pitch * 32);
	}
}

// Writes the string `data` of length `length` to the "terminal"
void terminal_write(const char* data, size_t length)
{
    if (consoleWidth == -1 || consoleWidth == -1)
    {
        // Console hasn't been initilized, display error
        // TODO: Actually display error

        return;
    }

    for (size_t i = 0; i < length; i++)
    {
        char c = data[i];

        if (c == '\n')
        {
            cursorY++;
            break;
        }

        terminal_putc(c, cursorX * 8, cursorY * 8, 0xFF);

        cursorX++;

        if (cursorX > consoleWidth)
        {
            cursorX = 0;
            cursorY++;
        }
    }
    
}

// Writes the string `data` to the "terminal"
void terminal_writestring(const char* data)
{
	terminal_write(data, strlen(data));
}
The way I'm calling the functions:

Code: Select all

    // Initilize the terminal so printing is possible
    terminal_initialize((uint64_t*)gop->Mode->FrameBufferBase, gop->Mode->Info->PixelsPerScanLine, gop->Mode->Info->HorizontalResolution, gop->Mode->Info->VerticalResolution);

    terminal_putc('!', 100, 100, 0xFF);

    terminal_writestring("Loading kernel...\n");
Github repo

Re: Problems with character rendering

Posted: Thu May 26, 2022 1:45 am
by davmac314
I don't see how this can work:

Code: Select all

   for(row = 0; row < 8; row++) {
      if(*src != 0) {
         uint32_t mask_low = font_mask[*src][0];
         uint32_t mask_high = font_mask[*src][1];
         dest32 = dest;
         dest32[0] = (dest[0] & ~mask_low) | (fgcolor32 & mask_low);
         dest32[1] = (dest[1] & ~mask_high) | (fgcolor32 & mask_high);
      }
      src++;
      dest += (pitch * 32);
   }
You're looping through 8 rows, but what about the 8 columns? It looks like you're setting 2 pixels per row:

Code: Select all

         dest32[0] = (dest[0] & ~mask_low) | (fgcolor32 & mask_low);
         dest32[1] = (dest[1] & ~mask_high) | (fgcolor32 & mask_high);
Shouldn't you be looping through bits in the mask, for example, and setting one pixel per bit? It's not clear to me what 'font_mask' is either nor what is stored in 'font'. Typically you'd have a single array with the bitmask defining the shape of each character (it could be a 2-dimensional array, but having two arrays seems odd).

Re: Problems with character rendering

Posted: Thu May 26, 2022 1:48 am
by klange
The code you blindly copied from the wiki and which was explained to you in your other thread is for 8bpp framebuffers. UEFI is absolutely not going to give you one of those. There are also bugs in the original code on the wiki that haven't been addressed.

Re: Problems with character rendering

Posted: Thu May 26, 2022 2:04 am
by ThatCodingGuy89
klange wrote:The code you blindly copied from the wiki and which was explained to you in your other thread is for 8bpp framebuffers. UEFI is absolutely not going to give you one of those. There are also bugs in the original code on the wiki that haven't been addressed.
That's pretty much what I expected, but honestly I have no clue how I could get a version that works for the 32bpp framebuffer that UEFI gives me on my machine. I tried to understand the principle behind the optimization, but it just makes no sense to me. Of course, that's probably because I don't understand some core concept of what exactly the code is doing.

Re: Problems with character rendering

Posted: Thu May 26, 2022 2:45 am
by iansjack
Possibly best to stick with the unoptimised version until you are in a position to understand the optimised code.

Copy-pasting code that you don't understand is always going to lead to tears before bedtime.

Re: Problems with character rendering

Posted: Thu May 26, 2022 3:21 am
by davmac314
ThatCodingGuy89 wrote:That's pretty much what I expected, but honestly I have no clue how I could get a version that works for the 32bpp framebuffer that UEFI gives me on my machine. I tried to understand the principle behind the optimization, but it just makes no sense to me. Of course, that's probably because I don't understand some core concept of what exactly the code is doing.
Oh I see, you've borrowed code for 8bpp. At least the code makes sense in that context.

But, not unsurprisingly, you can't use an optimised technique for blitting fonts to 8bpp framebuffers for doing the same to a 32bpp framebuffer. I suggest just sticking with the simpler form of the code which (assuming your "putpixel" works) will work correctly regardless. You're unlikely to find that the speed is a problem at this stage.

Re: Problems with character rendering

Posted: Sat May 28, 2022 10:16 am
by nullplan
You know, it would probably be better to understand what it is you are doing.

I imagine you have some font that assigns a monochrome 8x16 picture to each code point. So for capital A you would start with something like

Code: Select all

	--------
	--------
	--------
	--####--
	-#----#-
	-#----#-
	-#----#-
	-#----#-
	-######-
	-#----#-
	-#----#-
	-#----#-
	-#----#-
	--------
	--------
	--------
and end at

Code: Select all

static const uint8_t cap_a[16] = {0x00, 0x00, 0x00, 0x3c, 0x42, 0x42, 0x42, 0x42, 0x7e, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00};
Now what do you do with that? What do you actually want? You want to take eight pixels in a row and paint each of them white if the corresponding bit in the font is set, else paint them black (yes, yes, more generally, foreground and background color, respectively, but let us start simply for now). For 8bpp, 0xFF is a white pixel and 0x00 is a black one, but for 32bpp, it is 0xFFFFFFFF and 0x00000000 respectively. So on an 8bpp framebuffer, you want to turn each byte of font data into eight bytes of framebuffer data, thus turning each byte into 64 bits. But on a 32bpp FB, you want to turn each byte into 32 bytes of FB data.

So, for a 32-bpp FB, the easiest is probably to model the FB as a pitch x height array of 32-bit units. The nice thing about pointers into arrays is also that they are also simultaneously spans of the array, so you could write this like something like:

Code: Select all

void render_char_32bpp(uint32_t *fb, size_t pix_x, size_t pix_y, const uint8_t font[static 16], size_t fb_pitch)
{
  uint32_t *line = fb + pix_y * fb_pitch + pix_x;
  for (size_t i = 0; i < 16; i++) {
    for (size_t j = 0; j < 8; j++) {
      line[j] = ((font[i] >> (7-j)) & 1)? WHITE_PIXEL : BLACK_PIXEL;
    }
    line += fb_pitch;
  }
}
Of course, that is nowhere near the end. For one, you could save the X and Y parameters by making the initial calculation of "line" external and handing that over to the function to start with. That way, a possible string rendering function would be able to just keep track of that pointer itself, and would only have to advance one pointer between the characters. Also, the function should probably stop rendering when reaching FB width or height. As it stands, it must be called with all parameters firmly in bounds, or else it'll write all over some other memory. Although, if the callers can guarantee that they will only ever call the function with correct parameters then that checking will not be necessary.

That routine above is the principle idea for all framebuffers, the only difference is how you model the FB. In a 32bpp FB, it might be as array of 32-bit units. In an 8bpp FB, you might use an array of 8-bit units. Only a minor change is needed to also give it foreground and background colors as parameters, rather than hardcoding white and black in these roles, and then you have your working version.

I suspect it might be fast enough that way already, because any firmware worth its salt is going to set the memory type for the frame buffers as write-combining. Thus it really doesn't matter much if you access everything as 8-bits, 16-bits, or 32-bits, it's getting combined to 64 bytes anyway.

Re: Problems with character rendering

Posted: Sat May 28, 2022 11:31 am
by ThatCodingGuy89
nullplan wrote:You know, it would probably be better to understand what it is you are doing.

I imagine you have some font that assigns a monochrome 8x16 picture to each code point. So for capital A you would start with something like

Code: Select all

	--------
	--------
	--------
	--####--
	-#----#-
	-#----#-
	-#----#-
	-#----#-
	-######-
	-#----#-
	-#----#-
	-#----#-
	-#----#-
	--------
	--------
	--------
and end at

Code: Select all

static const uint8_t cap_a[16] = {0x00, 0x00, 0x00, 0x3c, 0x42, 0x42, 0x42, 0x42, 0x7e, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00};
Now what do you do with that? What do you actually want? You want to take eight pixels in a row and paint each of them white if the corresponding bit in the font is set, else paint them black (yes, yes, more generally, foreground and background color, respectively, but let us start simply for now). For 8bpp, 0xFF is a white pixel and 0x00 is a black one, but for 32bpp, it is 0xFFFFFFFF and 0x00000000 respectively. So on an 8bpp framebuffer, you want to turn each byte of font data into eight bytes of framebuffer data, thus turning each byte into 64 bits. But on a 32bpp FB, you want to turn each byte into 32 bytes of FB data.

So, for a 32-bpp FB, the easiest is probably to model the FB as a pitch x height array of 32-bit units. The nice thing about pointers into arrays is also that they are also simultaneously spans of the array, so you could write this like something like:

Code: Select all

void render_char_32bpp(uint32_t *fb, size_t pix_x, size_t pix_y, const uint8_t font[static 16], size_t fb_pitch)
{
  uint32_t *line = fb + pix_y * fb_pitch + pix_x;
  for (size_t i = 0; i < 16; i++) {
    for (size_t j = 0; j < 8; j++) {
      line[j] = ((font[i] >> (7-j)) & 1)? WHITE_PIXEL : BLACK_PIXEL;
    }
    line += fb_pitch;
  }
}
Of course, that is nowhere near the end. For one, you could save the X and Y parameters by making the initial calculation of "line" external and handing that over to the function to start with. That way, a possible string rendering function would be able to just keep track of that pointer itself, and would only have to advance one pointer between the characters. Also, the function should probably stop rendering when reaching FB width or height. As it stands, it must be called with all parameters firmly in bounds, or else it'll write all over some other memory. Although, if the callers can guarantee that they will only ever call the function with correct parameters then that checking will not be necessary.

That routine above is the principle idea for all framebuffers, the only difference is how you model the FB. In a 32bpp FB, it might be as array of 32-bit units. In an 8bpp FB, you might use an array of 8-bit units. Only a minor change is needed to also give it foreground and background colors as parameters, rather than hardcoding white and black in these roles, and then you have your working version.

I suspect it might be fast enough that way already, because any firmware worth its salt is going to set the memory type for the frame buffers as write-combining. Thus it really doesn't matter much if you access everything as 8-bits, 16-bits, or 32-bits, it's getting combined to 64 bytes anyway.
Ohhh. That makes so much more sense than when I was staring at the code attempting to understand it. Still going to stick to the version I have working until I notice a actual major performance hit, because the more complex I make things, the more difficult it's going to be to understand my own code later.