gonetz / GLideN64

A new generation, open-source graphics plugin for N64 emulators.
Other
776 stars 180 forks source link

Ogre battle, wrong texrects. #1047

Closed ghost closed 3 years ago

ghost commented 8 years ago

Ogre battle has a few texrects that are incorrectly displayed.

GLideN64 ogrebattle64-000

GLideN64, with dsdx and dtdy values hacked. ogrebattle64-003

The problematic textures are related to the text box (see first picture): 1 - The small on the left, too short. 2 - The big one in the middle, wrong color in the bottom. 3 - The small one on the right, the curve is slightly wrong. 4 - The triangular shaped one, the bottom is wrong.

After some testing, I noticed that if dsdx and dtdy are force to 1, the textures display correctly. This makes me think that the problem lies with the texture mapping. I measured the dsdx and dtdy values in _TexRect(u32 w0, u32 w1, bool flip) in RDP.cpp. The results are given in s5.10 fixed point format / floating point format.

1 - dsdx: 0x0491 / 1.141602, dtdy: 0x0410 / 1.015625 2 - dsdx: 0x0403 / 1.002930, dtdy: 0x0410 / 1.015625
3 - dsdx: 0x0465 / 1.098633, dtdy: 0x0410 / 1.015625 4 - dsdx: 0x0491 / 1.141602, dtdy: 0x0454 / 1.082031

These are weird values. Normally, a game designer would use integers or inverses of integers, so the value has a logical meaning (number of texture texel per number of screen pixel). I have only seen this types on number for animations where a rectangle shrinks.

As for why a N64 renders correctly, here are a few theories I can think of:

@gonetz What's your opinion on this regard?

gonetz commented 8 years ago

Software plugin renders this scene without glitches and hacks. I agree, the dsdx, dtdy values look suspicious. However, I'm sure that software plugin gets the same values. Thus, even if these values are wrong, N64 hardware can handle them correctly. I'm afraid the problem is on hardware side. As you may see, the glitches are result of texture wrap. Textures wrapped because texture coordinate slightly larger than necessary because of that bias above 1.0 in dsdx/dtdy. I tried to force texture clamp in my code, it does not help. Probably, N64 hardware just skips texels if texture coordinate goes behind tile bounds. You may run software plugin under debugger and see how it renders these texrects.

There might be some condition under which the N64 ignores the dsdx, dtdy values given by the display list and uses 1. I have read the documentation and tried many things, with no success whatsoever.

N64 texrect does not use fractional part of dsdx/dtdy only in copy mode. These texrects drawn in 1cycle mode, thus fraction part must be used.

ghost commented 8 years ago

These are dumps of the textures.

1 texture ogrebattle64 2f4ad335 2 0 50ecf550_cibyrgba

The texture's size is 16x61. Horizontally, 8 have meaningful pixels, 8 are transparent. Vertically, all 61 are meaningful pixels. Texrect's size is 8x61. Because the wrong dsdx value a transparent pixel is displayed. Wrong dtdy goes unnoticed because it clamps (or interpolates only with last pixel).

2 texture ogrebattle64 e9672bd8 2 0 4da57ae8_cibyrgba

The texture's size is 16x64. Horizontally, all pixels are meaningful. Vertically, 61 are meaningful and 3 contain garbage. Texrect's size is 237x61. With wrong dtdy value, garbage is shown in the bottom.

3 texture ogrebattle64 d72e4093 2 0 82e43f08_cibyrgba

The texture's size is 16x61. Horizontally, it has 11 meaningful pixels, 1 transparent and 4 garbage. Vertically all pixels are meaningful. The texrect's size is 11x61. Wrong dsdx goes unnoticed because it writes a transparent pixel. Wrong dtdy goes unnoticed because clamp.

4th texture ogrebattle64 daf23c20 2 1 8a04c20e_cibyrgba The texture's size is 8x16. All horizontal pixels are meaningful. Vertically, 12 pixels are meaningful, 1 is transparent and 4 contain garbage. Texrect's size is 8x13. Wrong dsdx goes unnoticed. Wrong dtdy shows garbage in the bottom.

This strongly suggests that dsdx and dtdy 1 should be used. It also explains why clamping is not working: tiles have garbage data too. Even if you clamp in the right place, you wouldn't get round edges in texture 3 with dsdx, dtdy != 1. Therefore I conclude that both N64 and angrylions plugin use dsdx=dtdy=1.

Do you know how can I get the virtual address of the texture? And do you know how to dump dlists passed by pj64 to angrylions plugin?

ghost commented 8 years ago

I investigated a little bit further. I noticed that the textures were part of an animation were the square gets bigger and moves. Hence, the dsdx, dtdy value should be changing during the animation. I tested the values with the plugin for texrec 2, they go as follows during the animation where it gets bigger.

dsdx -> -24.500977 -> 6.582031 -> 3.644531 -> 2.493164 -> 1.894531 -> 1.537109 -> 1.286133 -> 1.106445 -> 1.002930

I was not able to obtain the values for angrylion's software renderer to compare them, but since the last value should be one, it seems like they are incorrect. Most likely, they are being computed by the RDP dynamically and then used for display list creation. Besides, since the emulators display it right with software plugins, the calculation is most likely happening in the plugin. Also, I checked that there is no accuracy loss in the process from display list (w3 in _TexRect() ) to float (dsdx in gDPTextureRectangle() ).

gonetz commented 8 years ago

Texture address is set by SetTextureImage command. It is gDPSetTextureImage in GLideN64 code and rdp_set_texture_image in angrylion's code. I checked, software plugin gets the same values for dsdx, dtdy and works with both their integer and fractional parts. You may run software plugin under debugger, run the game and load savestate with this scene. Set conditional breakpoint in rdp_set_texture_image, for example for ti_address==0x002f94e8, it is left part of the rectangle with text. When breakpoint triggers, go to rdp_tex_rect and check how it works. You need to debug through edgewalker_for_prims to see how that texrect actually rendered. This is crazy work.

ghost commented 8 years ago

I'll look at it.

ghost commented 8 years ago

I've only checked the data used by GLideN64 so far, but I made some interesting findings. Mainly, the bias of the value of texelNumberdsdx never reaches one. For example, the texture coordinate of the pixel at position (7,0) of the texrect is (7dsdx, 0) = (7,991214,0).

Now, the N64 programming manual says that the texture must be seen as a grid with sample points on the TOP LEFT hand corner of the texel. So coordinates (7.991214,0) use texel (7,0). OpenGL-s nearest neighbor method samples in the MIDDLE and therefore coordinates (7.991214,0) use texel (8,0). So this could be the problem.

I tried writing a shader based on the bilinear filters but I have never worked with shaders and didn't manage to test:

const char * strTexrectDrawerTexPointSampleFilter =
MAIN_SHADER_VERSION
"uniform mediump vec4 uTextureBounds;                                                                           \n"
"lowp vec4 texFilter(in sampler2D tex, in mediump vec2 texCoord)                                                \n"
"{                                                                                                              \n"
"  lowp vec4 c = texture(tex, texCoord);                                                                        \n"
"  if (abs(texCoord.s - uTextureBounds[0]) < texelSize.x || abs(texCoord.s - uTextureBounds[2]) < texelSize.x) return c;    \n"
"  if (abs(texCoord.t - uTextureBounds[1]) < texelSize.y || abs(texCoord.t - uTextureBounds[3]) < texelSize.y) return c;    \n"
"  return texture(tex, vec2(floor(texCoord.s),floor(texCoord.t));                                                                   \n"
"}                                                                                                                  \n"
"                                                                                                               \n"
;

I tried to put it in place of the 3point filter (swapped every strTexrectDrawerTex3PointFilter with strTexrectDrawerPointSampleFilter) for a quick test and force G_TX_BILERP but I didn't manage to make it work. It used some bilinear filter, so the custom shader didn't load. (The mp64+ setting was set correctly. @gonetz Do you know what I did wrong? Do you mind testing?

ghost commented 8 years ago

I realized that rounding down is the same as subtracting 0.5 and then rounding to closest. So s,t, lrs, and lrt can be subtracted 0.5 and then applied regular OGL nearest neighbor filter. The only exception is when the initial values are integers, as it seems that nearest neighbor chooses the lower value for texture coordinates of the type x.5, when we would need up. Therefore, I use value 0.4999 which is practically the same. in gDPTextureRectangle() right after lrs, lrt calculations.

if (gDP.otherMode.textureFilter == G_TF_POINT){
    s -= 0.4999f;
    t -= 0.4999f;
    lrs -= 0.4999f;
    lrt -= 0.4999f;
}

It solves the issue in native resolution and creates no issues that I have found. I am unsure about why other resolutions are not rendering correctly, as theoretically it should work. Anyway, there are alignment issues in other games too with higher resolutions (like mario kart) so it might be something else interfering.

This last solution is not elegant, and the previous one should be applied. It should have the same effect though.

Still need to check angrylions code to check if he is filtering like this.

gonetz commented 8 years ago

Now, the N64 programming manual says that the texture must be seen as a grid with sample points on the TOP LEFT hand corner of the texel. So coordinates (7.991214,0) use texel (7,0). OpenGL-s nearest neighbor method samples in the MIDDLE and therefore coordinates (7.991214,0) use texel (8,0). So this could be the problem.

It is interesting hypotheses, I did not think about it that way. Your note about GL texture sampling is not quite correct though. Yes, N64 always uses origin in top-left corner, and texel origin is also in top-left. OpenGL always uses bottom-left for origin. That is texel's origin is in bottom left corner. Cite: "GL_NEAREST Returns the value of the texture element that is nearest (in Manhattan distance) to the center of the pixel being textured." That does not mean that GL nearest neighbor will always sample in the middle of the texel. It selects texel, which is closest to pixel's center. I found this document, explaining how texture samples selected: http://www.bpeers.com/articles/glpixel/ We may take actual width of texture rectangle in pixels, actual width of corresponding tile in texels and calculate pixel-texel correspondence for OpenGL using formulas from this document. I agree that N64 may sample texels differently and this difference may cause the problem. Probably, your solution is the right one.

This last solution is not elegant, and the previous one should be applied.

What is "previous one" solution?

ghost commented 8 years ago

What is "previous one" solution?

Actually I offered one solution and two possible implementations. Shifting s,t,lrs,lrt values and writing a filtering shader. What I meant is that the first one is no t elegant and a small hack is needed (a bias of 0.0001f for example). The second one is not implemented.

Very interesting article indeed! There is a slight difference with our case though.

In the article he offers the following formula for point sampling ( I'll assume left=0 for a simpler case and call M to the texrect size). i-th pixel's color should be the color of texel number

round((i+0.5)*N/M - 0.5)

Subtracting 0.5 and then applying round is the same as rounding down (except for negative coordinates with fractional part 0.5, which we don't get anyway). So it can be simplyfied to,

floor((i+0.5)*N/M)

However, in our case we don't know N as we can't rely on texture size (some textures are filled with garbage). Instead, we have dsdx to compute this value. In fact, dsdx = N/M. So the formula becomes,

floor((i+0.5)*dsdx)

So this is the texel coordinate of the color we want to show at pixel i of the texrect. Same goes for vertical value.

gonetz commented 8 years ago

Actually I offered one solution and two possible implementations. Shifting s,t,lrs,lrt values and writing a filtering shader. What I meant is that the first one is no t elegant and a small hack is needed (a bias of 0.0001f for example). The second one is not implemented.

Subtraction of 0.4999f is good enough for "proof of concept" solution. It can be put to separate dev branch.

we can't rely on texture size

We must rely on tile size, which is set by latest SetTileSize command for selected tile number.

ghost commented 8 years ago

We must rely on tile size, which is set by latest SetTileSize command for selected tile number.

The problem is that those are not necessarily set correctly by the game. The first tile has values: uls = 0, lrs = 15 and uly = 0, lry = 60. However, only 8 horizontal texels are used.

Also, according to the programming manual lrs and lry are only used for clamping, so there is no need for the game designer to use them unless he wants to clamp.

gonetz commented 8 years ago

Yes. However, graphics plugin uses tile size to calculate size of GL texture. In this case we have 16x61 texture in video memory. We know texture size, we know rect size - we can calculate pixel-texel correspondence for GL.

gonetz commented 8 years ago

I run software plugin under debugger. I did not dig deeply because the code is complex, but I found interesting thing about the case. It looks like the fixed point arithmetic helps N64 to do things right. Fixed point texture coordinate converted to int before texel fetch. It reads texels sequentially until accumulated sum of dsdx fraction exceeds 1. In fact it reads texels in the order: 0 1 2 3 4 5 6 7 9 10 11 ... Since software plugin renders 9 pixels in a row, not 8, it fetches texel 9 also, but it does not pass alpha test and discarded.

ghost commented 8 years ago

Fixed point texture coordinate converted to int before texel fetch. In fact it reads texels in the order: 0 1 2 3 4 5 6 7 9 10 11 ...

Ok, so this confirms the theory that we need to fetch texel on the left. The best thing we could do is pass dsdx value to shader (if it is possible) and compute the coordinates using this.

I think that, currently, the plugin computes texture size doing lrs = (lrx-ulx-1)*dsdx + 1 (the +1 is doing later in OpenGL.cpp) and then uses this value in the shader by calling textureSize(tex,0). Usually, that value is an integer and there is no problem. But in these extreme cases where it is not, the computations differ slightly.

I believe, I managed to make the effect work without touching the textures that were behaving well already, but I think that an implementation using dsdx directly in the shader would be more accurate.

I'll fork and pull the changes into a branch, but I don't have too much time these days.

Since software plugin renders 9 pixels in a row, not 8, it fetches texel 9 also, but it does not pass alpha test and discarded.

So why are 9 pixels being rendered when texrect width is 8?

gonetz commented 8 years ago

plugin computes texture size doing lrs = (lrx-ulx-1)*dsdx + 1

actually lrs = s + (lrx-ulx-1)*dsdx + dsdx

I think it is specially done that way. Texture tile is prepared in a special way: first 8 texels are for image, last 8 are auxiliary transparent texels. dsdx is calculated that way to slowly accumulate fractional part so the last pixel points to the auxiliary texel, which is discarded and that guarantees no overlapping between this and next parts of the image.

I think it is possible to support in shaders, but that case is very special and hardly is used in other games. It will be easier and better for shaders performance to just add a game-specific hack, as you did here: https://github.com/gonetz/GLideN64/issues/1047#issuecomment-231220179

So why are 9 pixels being rendered when texrect width is 8?

Last pixel is special case, "edge of the span". Ask angrylion about details, I don't know.

olivieryuyu commented 8 years ago

can it be related to this? https://github.com/gonetz/GLideN64/issues/936

SDEX seems always involved in this texrec issues (OB64 used SDEX as well i believe)

gonetz commented 8 years ago

No, it is usual texrect, used in unusual way.

ghost commented 8 years ago

It will be easier and better for shaders performance to just add a game-specific hack, as you did here:

1047 (comment)

Unfortunately, I noticed that it was working only because I had modified the calculation of texST[t] slightly.

I debugged the values texCoord handles in the shader and I can confirm that the values it takes are

(0.5 + index) /texRecSize

In order to emulate point sample filtering correctly, instead of calling texture(tex,texCoord), we need to call, texture(tex, texCoord - 0.5/texRecSize, in function lowp vec4 readTex(---). Notice that 1/texRecSize is actually vec2(dsdx,dtdy)/texSize. However, I can't get the values of texRecSize, dsdx, or dtdy inside the shader. I would need to know either of them or the index for which we are calculating the color.

In addition, an approximated solution doesn't seem feasible, because it requires very accurate computations.

gonetz commented 8 years ago

I debugged the values texCoord handles in the shader and I can confirm that the values it takes are

Which tool allows interactive shaders debugging?

Regarding texRecSize, dsdx, or dtdy - they can be added to shader as uniforms.

ghost commented 8 years ago

Which tool allows interactive shaders debugging?

I didn't use any tool, I wrote the values to the red channel of a screen pixel, took a screenshot and read from there.

ghost commented 8 years ago

Good news! I managed to implement the reading of dsdx, dtdy via uniforms and the result is nice filtering. Not only does it fix alignment issues in Ogre Battle, but also in Excitebike and partly in Beatle Adventure Racing, so its probably a step in the right direction. There are still some issues, I believe most have to do with negative dsdx and dtdy. I need to investigate further.

Before ogrebattle64-000 beetle_adventure_rac-000 excitebike64-008

After Textboxes correct. ogrebattle64-000 Text and bars are correct horizontally, vertically still incorrect beetle_adventure_rac-000 No white edges around position number when changing position. excitebike64-018

ghost commented 8 years ago

@gonetz I created a fork and put the code. It was my first time with github and I made a mess, but at the least you can see the changes.

ghost commented 8 years ago

It only works in native resolution. I am not sure why.

gonetz commented 8 years ago

I'll check it, thanks!

ghost commented 8 years ago

I made a change for negative dsdx and dtdy which solves some issues, like the ones in Beatle Adventure Racing.

All of this made the ingame hud in MK64 or all 2D in ISS64 wrong.

ghost commented 8 years ago

I am a little bit puzzled on why MK64 is not being rendered correctly. With the line " mediump vec2 coord = texCoord*texSize - vec2(0.4999); \n" it is rendered correctly while " mediump vec2 coord = texCoord*texSize - vec2(0.49)*abs(dsdx,dtdy); \n" gives wrong image.

dsdx and dtdy are 1 for all rectangles in-game, so the values must not be reaching the uniform. I am setting them in updateTextureInfo(). Is this not the right place?

gonetz commented 8 years ago

Yes, updateTextureInfo is good enough, it should be called for each texrect. You also may force call it by currentCombiner()->updateTextureInfo();

I also tried to make fix texrects coords problem using alternative calculation method, but it does not work right. Did not get why.

ghost commented 8 years ago

I found out the reason. The textures were being rendered in copy mode and I was passing dsdx = 4 to the uniform instead of dsdx = 1.

ghost commented 8 years ago

I was trying to squish a few of the remaining alignment bugs and found out that those are not texrects (or at least, they aren't being called from gDPTextureRectangle() ). gSPObjRectangle() is not being called either.

mariokart64-006

@gonetz Any idea of how the plugin renders those rectangles?

gonetz commented 8 years ago

Mario Kart does not use sprite ucode. If it is not texrect, it must be regular triangle command. Check gSPTriangle.

olivieryuyu commented 8 years ago

would it be linked to https://github.com/gonetz/GLideN64/issues/107?

gonetz commented 8 years ago

No, it is another issue.

gonetz commented 8 years ago

I uploaded my version of fixes for this problem in branch new_texrect_texcoord. It is incomplete and very dirty, just for experiments. As usual, some problems fixed, new glitches added. You may find the code useful if you want to make complete solution. Ogre Battle also fixed only in native resolution, but fixes in other games work in any resolution.

@olivieryuyu you may try it also. There are some nice fixes, like fixed barrel in GE intro, correct car position mark on BAR radar etc.

I'll be offline next week.

ghost commented 8 years ago

@gonetz Thanks! Do you mind posting a build?

AmbientMalice commented 8 years ago

Been doing some testing. Definitely improved in some areas. Some minor regressions, such as odd lines on icons in MK64's main menu. And also some major ones. 40_winks-002

But it's nice to see GoldenEye fixed.

ghost commented 8 years ago

@AmbientMalice Try changing vec2(0.5) for vec2(0.4999) in line 474 of Shaders_ogl3x.h . There's a precision error that makes the first coordinate wrong. Should eliminate odd lines on the top or left of textures.

ghost commented 8 years ago

What is the game in the picture?

AmbientMalice commented 8 years ago

@standard-two-simplex 40 Winks.

ghost commented 8 years ago

@AmbientMalice Try my branch for tests, it has a simpler implementation and i debugged it longer. Winks shows correctly and there are no bugs in MK64 menus.

Just a warning, I didn't implement it for non-native resolutions.

ghost commented 8 years ago

You may find the code useful if you want to make complete solution.

@gonetz I plan to find all alignment bugs if possible. I suspect that the bilinear filter is not perfect either (see the last picture about Mario Kart, the glowing rectangle is strange and bilinearly filtered). I want to find out as much as possible before thinking of a clean implementation.

olivieryuyu commented 8 years ago

can i get a binary? :D

ghost commented 8 years ago

I don't have GLideNUI running so I can only provide mupen64plus version. https://drive.google.com/open?id=0B8aIJkrbGl-zZ1hRWVI2VHFSZTg

The changes only affect point sampled textured rectangles. This second plugin doesn't show them, so if you find an alignment error, please check if this doesn't show the rectangle. https://drive.google.com/open?id=0B8aIJkrbGl-zTnNWNmR6bU1PY1U

When reporting issues, please take a screenshot in native render resolution (without upscaling the image before output), and make sure it is a point sampled textured rectangle. I am aware of issues related to bilinearly filtered rectangles.

ghost commented 8 years ago

The case of MK64 is a little bit strange. It seems that the bilinear filtering (normal or 3 point) requires the fractional parts to be calculated at (texSize*texCoord -1) instead of (texSize*texCoord -0.5) like most games.

Doing -0.5 is enough to fetch correct texels, but the incorrect fractional part makes it filtered in the middle of the square.

I don't know if texCoords is calculated by some fixed functionality of OpenGL. I tried to trace it back in the code, but I don't know where most of the variables involved come from.

I rewrote the bilinear filter to make the code easier to test. The texel fetch is done correctly (top left has correct values). If I use point sampling I get the correct result. For a 1:1 correspondence the bilinear filter should give the same result as the point sample. Due to the bias of it doesn't.

An example of a real N64 https://www.youtube.com/watch?v=kIIzE_H7D2g

olivieryuyu commented 8 years ago

MK64 is one of those few games (mostly as well games using RSP SW Version: 2.0D) where the texrec is not defined in the same way than the rest (G_RDPHALF_1 and G_RDPHALF_2 defined differently or gsSPTextureRectangle macro possibly uses G_RDPHALF_2 and G_RDPHALF_CONT) (command 12 and 13 instead of command 11). A kind of heuristic exists in the plugin to detect this matter. May be there is a link or not, dunno.

Do you have the same issue with games like SuperMario64 US or PilotWings, Mortal Kombat Trilogy- Dark Rift, Power Wayne Gretzky's 3D Hockey, Blast Corps, J. League Live 64, Killer Instinct, Mieschief Makers??

ghost commented 8 years ago

where the texrec is not defined in the same way than the rest (G_RDPHALF_1 and G_RDPHALF_2 defined differently or gsSPTextureRectangle macro possibly uses G_RDPHALF_2 and G_RDPHALF_CONT)

Do you know where that is documented? I checked the code and they are being used as flags so as to know where to fetch the texrec info.

gonetz commented 8 years ago

It is HLE stuff, defined in GBI.h If you want to focus on texrects, you may use LLE mode and not care about ucode peculiarities.

ghost commented 8 years ago

I've checked some more and the problems don't seem game dependent. In each game, there are primitives that behave as I expected and others that do not.

@gonetz I wan't to know how texCoord is computed. There is a calcTexCoord() function in the vertex shader which I don't know what it does and variable aTexCoord that I don't know where it comes from. Can you help me with this?

gonetz commented 8 years ago

You may read N64 programming manual for Texture Mapping explanation, e.g: http://n64devkit.square7.ch/pro-man/pro13/index.htm

N64 texture coordinate is fixed point number in range [0, 1024] OpenGL uses floats in range [0,1]. calcTexCoord() converts original vertex's texture ST coordinates passed via aTexCoord0 attribute to GL format. That conversion performed on CPU side with original glN64 code, but I moved it to vertex shader. You may check old c++ code for better understanding, what that function is doing.

Texrect is a special triangle command. It does not specify texture coordinates directly. The coordinates derived from other parameters. Plus, it uses some hacks to force texture clamp. Thus, conversion to OpenGL texture coordinates still made on CPU side in OGLRender::drawTexturedRect and result is passed to aTexCoord0 and aTexCoord1 attributes of vertex shader. aTexCoord0 holds coordinates for tile0 and aTexCoord1 for tile1 respectively. That is vertex shader does not convert texture coordinates in case of texrect (uRenderState == 3) and uses aTexCoord0 and aTexCoord1 attributes as is.

olivieryuyu commented 8 years ago

i guess 13.7.5.3 Bilinear Filtering and Point Sampling section is the most interesting here for the above issue

ghost commented 8 years ago

I think the problem lies in the rasterization process. The texture coordinates for the vertices seem correct, but the texture coordinates of the fragments are biased. I have been checking rasterization algorithms but I haven't drawn any conclusion. Unfortunately, the rasterizer step of the graphics pipeline is not programmable, so there is not much that can be done except trying to correct the values afterwards.