skyfloogle / red-viper

A Virtual Boy emulator for the 3DS
764 stars 17 forks source link

Locomotive games (SD Gundam, V-Tetris, Virtual Fishing) have incorrect colors #55

Open vaguerant opened 3 months ago

vaguerant commented 3 months ago

Issue

It looks like the shades of red are not displaying correctly in Virtual Fishing (EDIT: and other Locomotive games). Everything with the "one above black" shade, used in Virtual Fishing for highlights and backgrounds is an extremely dim shade of red somewhere in the decimal 16-17/255 range, making it difficult to tell which menu item is selected, etc.

![image](https://github.com/skyfloogle/red-viper/assets/5259025/4e87a319-5108-4ca2-9f3d-fc872c700f22) ![image](https://github.com/skyfloogle/red-viper/assets/5259025/1330168e-da16-49ba-b6e2-b615c2a969d7)

In these screenshots, FEMALE and START are (barely) highlighted on the respective images. These screenshots come from the English translation patch available on virtual-boy.com for ease of navigation, but the same issue occurs in a clean Japanese ROM.

Expected behavior

Here's some Mednafen shots:

![Virtual Fishing (Japan) English Translation -240327-221925](https://github.com/skyfloogle/red-viper/assets/5259025/b0fc2fec-b10f-4a5f-b9f1-0436a05c34f5) ![Virtual Fishing (Japan) English Translation -240327-221936](https://github.com/skyfloogle/red-viper/assets/5259025/38602bef-a4fe-4ad2-a4ff-784494b3be3d)

You can see here that I have MALE and START highlighted on the respective screens, the name plate and RECORDS section have a visible background color, etc.

Versions tested

Same behavior on all versions of Red Viper I've tested; this does not appear to be a regression.

Edit

I just noticed that V-Tetris also displays dimly and has barely-visible "one-shade-above-black" display. The Mode Select menu in V-Tetris has a BPS (Bullet Proof Software) logo scrolling across the background which is mostly obscured in Red Viper. Comparison shot from Mednafen.

![image](https://github.com/skyfloogle/red-viper/assets/5259025/b8a4fc03-ff3d-453d-aff8-2b3234d62d1c) ![V-Tetris (Japan)-240327-233349](https://github.com/skyfloogle/red-viper/assets/5259025/7a960625-b1b7-4133-81e9-99d6895654c2)

Locomotive was also responsible for SD Gundam: Dimension War, so that's another case that may be (EDIT: definitely is) impacted by the same issue.

skyfloogle commented 3 months ago

I noticed previously that the back of the ship in Vertical Force looked a bit dim. The Virtual Boy's display works through effectively PWM, and the brightness values indicate how long the LEDs should be turned on. Modern screens don't work in the same way. I directly pass the Virtual Boy's brightness values to the 3DS display, and that doesn't line up with the expected appearance. This is probably what gamma correction is for, but I haven't figured out how to set that up yet (or how to prevent it from messing with Luma's built-in stuff). Also worth noting: the image looks different in Citra on my laptop screen, iirc closer to what you'd see in Mednafen.

vaguerant commented 3 months ago

Such a weird system.

So would you say this isn't a Locomotive issue specifically, beyond that maybe they liked to use dimmer colors than other developers? It doesn't really harm V-Tetris or SD Gundam as much from my testing because the darker tones are just used for background elements, but Virtual Fishing is pretty hard to navigate on 3DS because it uses them in the menus.

vaguerant commented 3 months ago

Here's what I'm doing on my end currently:

diff --git a/source/3ds/video.c b/source/3ds/video.c
index 374dcb4..daf793d 100644
--- a/source/3ds/video.c
+++ b/source/3ds/video.c
@@ -197,6 +197,13 @@ void video_init() {
        C3D_RenderTargetSetOutput(finalScreen[1], GFX_TOP, GFX_RIGHT, DISPLAY_TRANSFER_FLAGS);
 }

+
+#define BRIGHTFLOOR 20
+u8 brightnessFloor (u8 brightness) {
+    if (brightness > 0) return clamp127(BRIGHTFLOOR + (127-BRIGHTFLOOR) * brightness / 127);
+    else return 0;
+}
+
 void video_render(int alt_buf) {
        if (tDSPCACHE.ColumnTableInvalid)
                processColumnTable();
@@ -207,9 +214,9 @@ void video_render(int alt_buf) {
        int col_scale = maxRepeat;
        #endif
        brightness[0] = 0;
-       brightness[1] = clamp127(tVIPREG.BRTA * col_scale);
-       brightness[2] = clamp127(tVIPREG.BRTB * col_scale);
-       brightness[3] = clamp127((tVIPREG.BRTA + tVIPREG.BRTB + tVIPREG.BRTC) * col_scale);
+       brightness[1] = brightnessFloor(tVIPREG.BRTA * col_scale);
+       brightness[2] = brightnessFloor(tVIPREG.BRTB * col_scale);
+       brightness[3] = brightnessFloor((tVIPREG.BRTA + tVIPREG.BRTB + tVIPREG.BRTC) * col_scale);

    C3D_AttrInfo *attrInfo = C3D_GetAttrInfo();
    AttrInfo_Init(attrInfo);

I'm making a big assumption here: that the brightness of a lit LED can't go below a certain level, basically theorizing that there's a gap in brightness between 0/off (black) and the minimum (red) brightness achievable. I absolutely do not know this, I have zero experience with the real Virtual Boy and no anecdotal evidence that suggests this approach is right.

This approach necessarily reduces the dynamic range somewhat, since it now goes from 20-127 instead of 1-127 for lit pixels. BRIGHTFLOOR is a define for ease of adjustment, in production this could be inlined, converted to a user-configurable option, etc. but right now I'm doing this based on nothing, so that's not a concern currently.

I'm also only using integer math, because this is all back of the envelope stuff anyway. Precision is kind of pointless when I'm just eyeballing whatever looks OK.

Here's a couple of screenshots demoing Virtual Fishing and V-Tetris with a brightness floor of 20:

![image](https://github.com/skyfloogle/red-viper/assets/5259025/4da99476-a059-4bf2-a1ae-5b91c5f3f9ba) ![image](https://github.com/skyfloogle/red-viper/assets/5259025/3e13da53-8336-499d-bf72-64b465ab71a6)

EDIT: And a test build for anybody who wants one: red-viper_brightness-floor.zip

vaguerant commented 3 months ago

I can say with some confidence that my approach above is wrong. The game Virtual Boy Wario Land seems to use screen transitions which never set the brightness to 0; my approach above means that graphics that are still on-screen during transitions sit at that (arbitrary) floor of 20, when it seems likely that it's supposed to be completely dark even though they are technically not at 0 brightness.

Doing some more arbitrary nonsense, I'm running with this now:

diff --git a/source/3ds/video.c b/source/3ds/video.c
index 9481e39..cc32262 100644
--- a/source/3ds/video.c
+++ b/source/3ds/video.c
@@ -209,6 +209,12 @@ void video_init() {
        GSPGPU_WriteHWRegs(0x400424, &vtotal, 4);
 }

+#define BRIGHTFLOOR 20
+u8 brightnessFloor (u8 brightness) {
+       if (brightness > 6) return clamp127(BRIGHTFLOOR + (127-BRIGHTFLOOR) * brightness / 127);
+       else return 0;
+}
+
 void video_render(int alt_buf) {
        if (tDSPCACHE.ColumnTableInvalid)
                processColumnTable();
@@ -219,9 +225,9 @@ void video_render(int alt_buf) {
        int col_scale = maxRepeat;
        #endif
        brightness[0] = 0;
-       brightness[1] = clamp127(tVIPREG.BRTA * col_scale);
-       brightness[2] = clamp127(tVIPREG.BRTB * col_scale);
-       brightness[3] = clamp127((tVIPREG.BRTA + tVIPREG.BRTB + tVIPREG.BRTC) * col_scale);
+       brightness[1] = brightnessFloor(tVIPREG.BRTA * col_scale);
+       brightness[2] = brightnessFloor(tVIPREG.BRTB * col_scale);
+       brightness[3] = brightnessFloor((tVIPREG.BRTA + tVIPREG.BRTB + tVIPREG.BRTC) * col_scale);

        C3D_AttrInfo *attrInfo = C3D_GetAttrInfo();
        C3D_AttrInfo *attrInfo = C3D_GetAttrInfo();
        AttrInfo_Init(attrInfo);

The only change is I'm now doing if (brightness > 6) ... else return 0; instead of if (brightness > 0). 6 is just another arbitrary choice to get black during Wario Land fades since that seems to be as low as Wario goes. Obviously this is a total mess and nonsensical as far as real-world accuracy.

Really it comes down to the gamma ramp as you've said. It presumably isn't linear: it must ramp up quite slowly at the bottom-end (which suits the behavior of Wario Land), then a bit faster (so that the colors in Virtual Fishing are visible), before presumably stabilizing into something more linear after that? But I'll quit speculating since it really doesn't matter until you can test on real hardware.