ducalex / retro-go

Retro emulation for the ODROID-GO and other ESP32 devices
GNU General Public License v2.0
578 stars 131 forks source link

gnuboy shader #58

Open 32teeth opened 2 years ago

32teeth commented 2 years ago

Is your feature request related to a problem? Please describe. Not related to a problem

Describe the solution you'd like A great feature to have a closer to hardware experience would be to add a pixel grid scanline overlay, this can be a boolean option in the gnuboy hud

Additional context Pixalized look FTW!

shader

ducalex commented 2 years ago

I agree this looks pretty cool but the LCDs we use do not have enough resolution for that :( .

It would need to be at least 320x280 to get that crisp pixel grid effect.

32teeth commented 2 years ago

If I want to experiment, which routing in lcd.c would I start?

IIRC in RetroArch it's just a PNG overlay

IndiePix commented 1 year ago

Retroarch just puts an image on top of the emulation, yes, but I don't think the ESP32 is capable of such task, it looks pretty cpu intensive to me as it has to blend the overlay and the game before sending it to the screen buffer.

I think the most optimal way to do this grid, is by making the parts of the screen lighter so when the game is displayed it only has to calculate the same color but adding a little bit of brightness on some pixels to simulate that grid effect.

I don't know where on the source code this can be made, ducalex will know it for sure, but I guess in rg.display.c maybe we can add a boolean function there to select the pixels we want to form a grid and make them lighter.

If we take into account that the screen is 320x240, both are even numbers, you can make a condition that each odd pixel will be lighter on both X and Y axis. The result would be a grid effect. Unfortunately it is impossible with such low resolution to make a realistic grid like the original console, so that would be the closest it can get probably on a 240p display...

The only extra task that the system has to make is to take the pixel color and if it is an odd pixel increase the brightness of it, the more bright the more exaggerated will be the grid, I guess there's a limit where it would look pretty bad. It doesn't look like a pretty intensive task, so I think it would be affordable for the ESP32.

The end result should look like this, I made a macro that basically adds a 30% opacity pixel each odd pixel on the 320x240 on both axis on the image so you get the idea:

gridgb

As you can see the image becomes a lot lighter, that's inevitable, as you making the 50% of the screen about 30% lighter, but the grid effect is achieved. You can even make a variable to manipulate the color of those odd pixels and see which one works the best.

ducalex commented 1 year ago

I think that would be the best approach.

I think it should be done after scaling and filtering? So basically in rg_display.c just before lcd_send_data(line_buffer, scaled_width * lines_to_copy * 2); (currently line 556).

But I suspect doing the math on the fly would be too slow (breaking down RGB, then applying a factor from a gamma table, then repacking rgb, for each pixel). So we might need one or two full 16 bit lookup tables (one brighter, one darker?). Still worth trying I think.

Anyone willing to try implementing that?

IndiePix commented 1 year ago

I agree, adding the grid after scaling and filtering would be the most optimal way in combination along partial updates whenever it's possible. Filtering would make the grid really blurry as it will try to blend the grid itself with the displayed frame. It would look pretty bad, so it is probably mandatory to apply the grid after the filtering.

Scaling is trickier, because if we take the scaling variable into the grid, the grid will have to be modified to fit the new scaled width, and if the width is not even in number of pixels it may glitch the whole grid and make artefacts because it probably will skip some lines. Another option would be to do a grid that fills all the screen resolution even when reducing the scale of the game so it will always be a perfect pixel grid across the lcd.

The idea is to add a constant RGB value to the intended pixels, no matter the colour of it. Actually, 100% white pixels on the original gameboy matches the grid color, as the grid it is basically the blank open spaces of the LCD between the polarizing tiles. This would look different when applying palettes, so it would be interesting to make that addition of RGB values a variable that can be debugged and modified on the fly through the in-game menu for example. If we take for example the classic green gameboy coloring scheme, it would make more sense to add 30% Green, 5% Red and 5% Blue so the grid will match better the palette. This wouldn't modify the original idea, in fact it could lead to optimizations, because imagine if you only alter one of the R G B colors, for example adding only 30% green to a grayscale image leaving red and blue intact, will result in something like this:

GB GRID EXAMPLE 2

As you can see in the image, a grayscale can become green by adding a 30% green on top of it, so we can obtain the color palette and the grid itself by simply adding a colored grid (same will happen to red and blue), I added how it would look on a gameboy color game with a 30% white grid, although we can experiment to add only one of two of the RGB colors if optimizations are needed.

So how the code would look like? The code may look like almost the same as it is without the grid, but adding in the for( ) loop where the pixels are filled into the buffer a condition so they will be modified accordingly, in this case adding color to the pixels across the Y axis.

for (int x = 0; y < frame_width; ++x){
           for (int y = 0; y < frame_height; ++y) {
                 if (y % 2 == 0) {
                 pixel += addcolor;
                 }
             }
}

The code probably looks terrible, but I guess the idea is there. I don't know how slow would be that loop, but maybe we can play with the timings around it and also modify only the pixels that are updating, basically just how partial updates work. Maybe there's a more optimal way to do all of this, but I don't think it would be simpler.

I will try some of these experimentation myself, but I can't promise anything. First I have to understand how the process works, from the first pixel calculation, filters, buffering etc...

IndiePix commented 1 year ago

I think I found a way to do it just before sending the data into a line buffer. I tried to study the code to find were we can modify the pixels in a RGB format directly just after the blending:

https://github.com/ducalex/retro-go/blob/6c439f6a1e8587ed1a5ef448a9c0fc7caa665556/components/retro-go/rg_display.c#L387-L414

Just after the blending of pixels in r, g and b happens, it would be possible to just add the necessary push of brightness there by adding a constant to rv, gv and bv which are the just blended pixels: https://github.com/ducalex/retro-go/blob/6c439f6a1e8587ed1a5ef448a9c0fc7caa665556/components/retro-go/rg_display.c#L406-L408

The tricky part here is that we need a condition to sum the constant value only when the pixel is in certain position of the display to basically make the grid. As I can see, correct me if I'm wrong, is that X axis works filling pixels and then Y axis works filling a whole line made of X pixels.

This logic will change a little bit the approach, as we probably need to modify X pixels across the lines and then modify the necessary lines on the Y axis without touching the pixels that are already modified, let me put a graphical example:

grid structure

As you can see, certain lines (0,2,4,6,... from the example) can be modified entirely as they carry the actual lines of the final grid. The other lines (1,3,5,7...) have to be modified pixel by pixel (x=0,2,4,6...) so in the end they close the grid across the X axis without overwriting the already modified ones.

What do you think about this @ducalex? Do you think this approach would be possible? Is my analysis of the pixels and lines correct?

ducalex commented 1 year ago

blend_pixels is used for bilinear filtering so it is called only to fill empty lines/columns when scaling. This will result in an uneven grid with most emulators. For example, in gameboy, the image is 160x144 scaled to 320x240. Modifying blend_pixels will produce the desired vertical lines (because 320/160 = 2), but the horizontal lines would appear at weird intervals. Whether or not that is better than an even grid, I can't say.

This snippet will create a full screen even grid, add it before the lcd_send_data:

for (size_t y = 0; y < lines_to_copy; ++y)
{
    size_t real_y = screen_y - lines_to_copy + y;
    for (size_t x = 0; x < scaled_width; ++x)
    {
        size_t real_x = screen_left + RG_SCREEN_MARGIN_LEFT + x;
        if ((real_x & 1) || (real_y & 1))
        {
            // Do pixel math here:
            // unsigned pixel = line_buffer[y * scaled_width + x];
            // unsigned r = (pixel >> 11) & 0x1F;
            // unsigned g = (pixel >> 5) & 0x3F;
            // unsigned b = (pixel) & 0x1F;
            // do stuff to r g b
            // pixel = (r << 11) | (g << 5) | (b);
            // line_buffer[y * scaled_width + x] = (pixel << 8) | (pixel >> 8);
            // Or simply black grid:
            line_buffer[y * scaled_width + x] = 0;
        }
    }
}

It can obviously be further optimized (we can remove the odd condition by aligning x/y then +2ing, etc).

IndiePix commented 1 year ago

in gameboy, the image is 160x144 scaled to 320x240. Modifying blend_pixels will produce the desired vertical lines (because 320/160 = 2), but the horizontal lines would appear at weird intervals.

These scaling problems appeared as well when I tried to put overlays to the retroarch emulator on my gpi case (same screen resolution). My approach then was exactly what you did, which is making a full screen even grid, so the grid will always fit any possible scale, even 1x1 scaling. In the end there's not much we can do about it, this grid will be the best we can do with this screen resolution.

You're totally right. I was trying to assign the final values to r g b pixels just after blending, so I though they could be modified just before the little/big endian modification but it would not work properly because of this...

it is called only to fill empty lines/columns when scaling

Adding a shade to form a transparent grid

The pixel modification works similar as you did with the shade of the system background on the list menu, which makes the background darker to improve visibility of the list:

 if ((real_x & 1) || (real_y & 1))
        {
            // Do pixel math here:
            pixel = line_buffer[y * scaled_width + x];
            unsigned r = ((pixel >> 11) & 0x1F) + gridshade; 
            unsigned g = ((pixel >> 5) & 0x3F)  + gridshade; 
            unsigned b = ((pixel) & 0x1F) + gridshade;       
            pixel = (r << 11) | (g << 5) | (b);
            line_buffer[y * scaled_width + x] = (pixel << 8) | (pixel >> 8);
        }

The problem I see with this is that imagine if we add a certain value of white (equal r, g, b) grid to the r,g,b pixels, some of them will overflow their maximum value when adding the gridshade (maybe this is tackled somewhere in the code for other functions idk). To avoid this I would add something like:

const int max_r = 0x1F;
const int max_g = 0x3F;
const int max_g = 0x1F;

unsigned r = ((pixel >> 11) & 0x1F) + gridshade;
if (r > max_r) r = max_r;
unsigned g = ((pixel >> 5) & 0x3F)  + gridshade; 
if (g > max_g) g = max_g;
unsigned b = ((pixel) & 0x1F) + gridshade;    
if (b > max_b) b = max_b;  

Gridshade ingame menu option

It would be nice to be able to modify grid from the ingame menu to make it from 0% (no grid) to 100% in intervals of 10% to make it brighter or dimmer. For the moment I would keep only the constant white grid, which is the simplest and most versatile for any type of palette or color.

Gridshade alternative colors

The grid can also go darker instead of lighter, this would make the effect of a darker transparent grid, which sometimes can be good for games that are really bright and have lots of white in general. Maybe the condition of min_r, min_b and min_g so the pixels can't go negative isn't needed as we are using unsigned declarations.

And finally, once all the above works, the most complex one will be to add a gridshade for each r, g, b color, so the grid will be able to be tinted. This will help with some palettes and color games. For example, if we have a grayscale image and we add only gridshade on the green pixels, we will obtain a grid but also a green tinted image as a result, so it would be interesting to experiment with this to see how it affects the system.

32teeth commented 1 year ago

Hey all, been a while. Picking this conversation up. I cam across https://github.com/HarlequinVG/shaders and wondering if there are potential for lifting anything there.

The shaders have pixel and blur emulation as well

IndiePix commented 1 year ago

Hey 32teeth, I've been out of the loop for a while too, here I am giving some love to the retro-go retroesp32 again :)

Well, the shaders you passed are great, but are too heavy for the retro-go to work I'm afraid. I think my method of making like a oddpixel grid playing with the alpha value of the pixels is the only way that can be done in an esp32 because we are working with a very low power processor.

My next holydays are coming soon, so I will try it and see how it goes, I already tried this method of grid before in a raspberry pi with retropie and looks great on the 320x240 lcd, it looks pretty convincing.