Skip to content

Instantly share code, notes, and snippets.

@Pokechu22
Last active February 14, 2022 20:04
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Pokechu22/49455f9094ed0ff017da64e3f7aa0404 to your computer and use it in GitHub Desktop.
Save Pokechu22/49455f9094ed0ff017da64e3f7aa0404 to your computer and use it in GitHub Desktop.
Description of the pink rendering issue in EA Sports Active

Preliminaries

The fifolog in question is ea-pink, downloadable here, which shows gameplay from EA Sports Active. There are several oddities with how this is rendered; for instance, it is rendered in two halves (vertically) and some objects are only rendered in one of the halves (this is best seen by using freelook), similar to nhl-slap (but that game does it to a greater extent). The game also renders a vignette (causing the edges of the image to be darker) and the hud after the broken effect. But it is that broken effect, resulting in everything being pink, that this document is about.

The broken effect is object 819 in that fifolog (numbered 842 with better-fifo-analyzer-part-3).

See https://gist.github.com/Pokechu22/f06db7a9f226d9b0c72507886d7a72ce for additional images as well as the notes I took while investigating this (though those notes are fragmentary and somewhat incoherent).

The idea

EA Sports Active is trying to do some kind of color filtering (I think it's trying to increase saturation, but I'm not 100% sure). res_unfiltered.png is the input, and res_filtered_good.png is te intended output, but what actually shows up is res_filtered_bad.png. To perform this effect, it has a texture serving as a palette, and then uses indirect texturing to compute coordinates into that texture. The challenge is that colors have 3 channels (red, green, and blue), but the Wii doesn't support 3-dimensional textures, only 2-dimensional textures. This is solved by putting both red and blue on the x axis, while green is on the y axis. See Palette.png for the actual texture, and Palette_anim.gif and Palette_anim.png for the texture where blue is represented by time instead. (The first uses GIF, and its 256-color limitation makes it look ugly; theoretically there's a way to have 256 colors per frame instead of per file, but I couldn't figure it out. The second uses APNG, which should be supported by most browsers nowadays.)

Each color channel is given 4 bits in the texture (values from 0-15). You would think that this would result in a 16*16 by 16 texture (i.e. 256 by 16), but instead the texture is 512 by 16, with large stretches that are the same color. In fact, half of the texture seems to be wasted. And, yes, it is, but the gaps do serve a purpose: linear interpolation is enabled for the texture, which means that red and green are not restricted to 16 values each. However, if these 16 by 16 areas were placed next to each other, having (r, g, b) = (255, 0, 0) would end up sampling both (255, 0, 0) and (0, 0, 16), resulting in a very wrong color. (There's no way to clamp it within the 16 by 16 area; it's only possible to clamp to the whole boundary of the texture). This gap probably doesn't need to be 16 pixels wide (just 2 pixels wide would probably work), and could possibly be eliminated entirely with a cleverer matrix choice, but it still works. Note that half of the gap is the previous color (red/magenta or yellow/white) and half of it is the next color (black/blue or green/cyan), with 9 pixels being the previous color and 9 pixels being the new color; at the very end there are 17 pixels with the previous color instead as there is no new color to switch to. It is that very end that contains the pink that's being seen.

Raw data

First EFB copy

001489bc - 001489da

BP register BPMEM_COPYFILTER0
w0: 0
w1: 0
w2: 21
w3: 22

BP register BPMEM_COPYFILTER1
w4: 21
w5: 0
w6: 0

BP register BPMEM_EFB_TL
EFB Left: 0
EFB Top: 0

BP register BPMEM_EFB_WH
EFB Width: 640
EFB Height: 448

BP register BPMEM_MIPMAP_STRIDE
No description available

BP register BPMEM_EFB_ADDR
EFB Target address (32 byte aligned): 0x877900

BP register BPMEM_TRIGGER_EFB_COPY
Clamping: Top and Bottom
Converting from RGB to YUV: No
Target pixel format: RG8/Z16R (Note: G and R are reversed) (11)
Gamma correction: 1.0
Mipmap filter: No
Vertical scaling: No
Clear: No
Frame to field: Progressive (0)
Copy to XFB: No
Intensity format: No
Automatic color conversion: Yes
Second EFB copy

00148a11 - 00148a2f

Note that the copy filter is configured twice, for some reason; the first configuration doesn't matter (and I'm not showing it here)

BP register BPMEM_COPYFILTER0
w0: 0
w1: 0
w2: 1
w3: 2

BP register BPMEM_COPYFILTER1
w4: 1
w5: 0
w6: 0

BP register BPMEM_EFB_TL
EFB Left: 0
EFB Top: 0

BP register BPMEM_EFB_WH
EFB Width: 640
EFB Height: 448

BP register BPMEM_MIPMAP_STRIDE
No description available

BP register BPMEM_EFB_ADDR
EFB Target address (32 byte aligned): 0x903940

BP register BPMEM_TRIGGER_EFB_COPY
Clamping: Top and Bottom
Converting from RGB to YUV: No
Target pixel format: B8/Z8L (10)
Gamma correction: 1.0
Mipmap filter: No
Vertical scaling: No
Clear: No
Frame to field: Progressive (0)
Copy to XFB: No
Intensity format: No
Automatic color conversion: Yes
Texture configuration

00148b5f - 00148bb4; mipmap and tmem configuration is skipped since it's not relevant.

BP register BPMEM_TX_SETMODE0 Texture Unit 0
Wrap S: Clamp (0)
Wrap T: Clamp (0)
Mag filter: Linear (1)
Mipmap filter: None (0)
Min filter: Linear (1)
LOD type: Edge LOD (0)
LOD bias: 0 (0)
Max anisotropic filtering: 1 (0)
LOD/bias clamp: No

BP register BPMEM_TX_SETIMAGE0 Texture Unit 0
Width: 512
Height: 16
Format: RGBA8 (6)

BP register BPMEM_TX_SETIMAGE3 Texture Unit 0
Source address (32 byte aligned): 0x11D472E0
BP register BPMEM_TX_SETMODE0 Texture Unit 1
Wrap S: Clamp (0)
Wrap T: Clamp (0)
Mag filter: Linear (1)
Mipmap filter: None (0)
Min filter: Linear (1)
LOD type: Edge LOD (0)
LOD bias: 0 (0)
Max anisotropic filtering: 1 (0)
LOD/bias clamp: No

BP register BPMEM_TX_SETIMAGE0 Texture Unit 1
Width: 640
Height: 448
Format: IA8 (3)

BP register BPMEM_TX_SETIMAGE3 Texture Unit 1
Source address (32 byte aligned): 0x877900
BP register BPMEM_TX_SETMODE0 Texture Unit 2
Wrap S: Clamp (0)
Wrap T: Clamp (0)
Mag filter: Linear (1)
Mipmap filter: None (0)
Min filter: Linear (1)
LOD type: Edge LOD (0)
LOD bias: 0 (0)
Max anisotropic filtering: 1 (0)
LOD/bias clamp: No

BP register BPMEM_TX_SETIMAGE0 Texture Unit 2
Width: 640
Height: 448
Format: I8 (1)

BP register BPMEM_TX_SETIMAGE3 Texture Unit 2
Source address (32 byte aligned): 0x903940
Indirect stage 0

00148bcd - 00148be6. BPMEM_IREF and BPMEM_RAS1_SS0 cover multiple indirect stages and are written twice; I'm showing the relevant data from the final write with the applicable stage.

BP register BPMEM_IREF
Stage 0 ntexmap: 1
Stage 0 ntexcoord: 1

BP register BPMEM_RAS1_SS0
Indirect texture stages 0 and 1:
Even stage S scale: 0 (1)
Even stage T scale: 0 (1)

BP register BPMEM_IND_MTXA Matrix 0
Matrix 0 column A
Row 0 (ma): 0 (0)
Row 1 (mb): 0.5 (512)
Scale bits: 2 (shifted: 2)

BP register BPMEM_IND_MTXB Matrix 0
Matrix 0 column B
Row 0 (mc): 0.5 (512)
Row 1 (md): 0 (0)
Scale bits: 3 (shifted: 12)

BP register BPMEM_IND_MTXC Matrix 0
Matrix 0 column C
Row 0 (me): 0 (0)
Row 1 (mf): 0 (0)
Scale bits: 0 (shifted: 0), given to SDK as 0 (0)

BP register BPMEM_IND_CMD command 0
Indirect tex stage ID: 0
Format: ITF_8 (0)
Bias: None (0)
Bump alpha: Off (0)
Offset matrix index: Matrix 0 (1)
Offset matrix ID: Indirect (0)
Regular coord S wrapping factor: Off (0)
Regular coord T wrapping factor: Off (0)
Use modified texture coordinates for LOD computation: No
Add texture coordinates from previous TEV stage: No
Indirect stage 1

00148be1 - 00148bfa

BP register BPMEM_IREF
Stage 1 ntexmap: 2
Stage 1 ntexcoord: 1

BP register BPMEM_RAS1_SS0
Indirect texture stages 0 and 1:
Odd stage S scale: 0 (1)
Odd stage T scale: 0 (1)

BP register BPMEM_IND_MTXA Matrix 1
Matrix 1 column A
Row 0 (ma): 0 (0)
Row 1 (mb): 0 (0)
Scale bits: 3 (shifted: 3)

BP register BPMEM_IND_MTXB Matrix 1
Matrix 1 column B
Row 0 (mc): 0.5 (512)
Row 1 (md): 0 (0)
Scale bits: 1 (shifted: 4)

BP register BPMEM_IND_MTXC Matrix 1
Matrix 1 column C
Row 0 (me): 0 (0)
Row 1 (mf): 0 (0)
Scale bits: 1 (shifted: 16), given to SDK as 1 (16)

BP register BPMEM_IND_CMD command 1
Indirect tex stage ID: 1
Format: ITF_8 (0)
Bias: None (0)
Bump alpha: Off (0)
Offset matrix index: Matrix 1 (2)
Offset matrix ID: Indirect (0)
Regular coord S wrapping factor: Off (0)
Regular coord T wrapping factor: Off (0)
Use modified texture coordinates for LOD computation: No
Add texture coordinates from previous TEV stage: Yes
Normal TEV configuration

00148c09 - 00148c18

BP register BPMEM_TREF number 0
Stage 0 texmap: 0
Stage 0 tex coord: 0
Stage 0 enable texmap: Yes
Stage 0 color channel: Zero (7)
Stage 1 texmap: 0
Stage 1 tex coord: 0
Stage 1 enable texmap: Yes
Stage 1 color channel: Zero (7)

BP register BPMEM_TEV_COLOR_ENV Tev stage 0
dest.rgb = 0

a: ZERO (15)
b: ZERO (15)
c: ZERO (15)
d: ZERO (15)
Bias: 0 (0)
Op: Add (0) / Comparison: Greater than (0)
Clamp: Yes
Scale factor: 1 (0) / Compare mode: R8 (0)
Dest: prev (0)

BP register BPMEM_TEV_COLOR_ENV Tev stage 1
dest.rgb = tex.rgb

a: tex.rgb (8)
b: ZERO (15)
c: ZERO (15)
d: ZERO (15)
Bias: 0 (0)
Op: Add (0) / Comparison: Greater than (0)
Clamp: Yes
Scale factor: 1 (0) / Compare mode: R8 (0)
Dest: prev (0)
Texture coordinate scale configuration

00148c22 - 00148c40

BP register BPMEM_SU_SSIZE number 1
S size info:
Scale: 640
Range bias: No
Cylindric wrap: No
Use line offset: No (s only)
Use point offset: No (s only)

BP register BPMEM_SU_TSIZE number 1
T size info:
Scale: 448
Range bias: No
Cylindric wrap: No
Use line offset: No (s only)
Use point offset: No (s only)

BP register BPMEM_SU_SSIZE number 0
S size info:
Scale: 512
Range bias: No
Cylindric wrap: No
Use line offset: No (s only)
Use point offset: No (s only)

BP register BPMEM_SU_TSIZE number 0
T size info:
Scale: 16
Range bias: No
Cylindric wrap: No
Use line offset: No (s only)
Use point offset: No (s only)
Other configuration

00148c45 - 00148c50

BP register BPMEM_GENMODE
Num tex gens: 2
Num color channels: 1
Unused bit: 0
Flat shading (unconfirmed): No
Multisampling: No
Num TEV stages: 2
Cull mode: None (0)
Num indirect stages: 2
ZFreeze: No

CP register VCD_LO
Position and normal matrix index: Not present
Texture Coord 0 matrix index: Not present
Texture Coord 1 matrix index: Not present
Texture Coord 2 matrix index: Not present
Texture Coord 3 matrix index: Not present
Texture Coord 4 matrix index: Not present
Texture Coord 5 matrix index: Not present
Texture Coord 6 matrix index: Not present
Texture Coord 7 matrix index: Not present
Position: 8-bit index (2)
Normal: Not present (0)
Color 0: Not present (0)
Color 1: Not present (0)

CP register VCD_HI
Texture Coord 0: 8-bit index (2)
Texture Coord 1: 8-bit index (2)
Texture Coord 2: Not present (0)
Texture Coord 3: Not present (0)
Texture Coord 4: Not present (0)
Texture Coord 5: Not present (0)
Texture Coord 6: Not present (0)
Texture Coord 7: Not present (0)
Primitive data

00148da6 - 00148da9

Primitive GX_DRAW_TRIANGLES (2) VAT 0

Primitive GX_DRAW_QUADS (0) VAT 0

00  00  00  
01  01  01  
02  02  02  
03  03  03  

This works out to drawing 0 triangles (i.e. nothing), and then a quad (using array information that isn't listed above):

x y z s0 t0 s1 t1
640.00 0.00 -10.00 0.00 0.00 1.00 0.00
640.00 448.00 -10.00 0.00 0.00 1.00 1.00
0.00 0.00 -10.00 0.00 0.00 0.00 0.00
0.00 448.00 -10.00 0.00 0.00 0.00 1.00

The implementation

After doing the main rendering, EA Sports makes two EFB copies. The first EFB copy to physical address 0x877900, and has the color format set to RG8 which, for each pixel, stores 8 bits (1 byte) of the green channel followed by 8 bits of the red channel. (Note that this may be backwards from what one would expect.) The second EFB copy goes to physical address 0x903940, and has the color format set to B8 which stores 8 bits of the blue channel for each pixel.

For the actual drawing, texture 0 is set to use the palette texture. Texture 1 is set to the first EFB copy with format IA8; for each pixel, the first byte is the intensity value (going into the red, green, and blue channels) and the second byte is the alpha value. This means that the red, green, and blue channels have the original EFB's green channel, and the alpha channel has the original EFB's red channel. Texture 2 is set to the second EFB copy with format I8; all channels (including alpha, I think, but definitely red, green, and blue).

The indirect coordinate step involves a matrix; this matrix is multiplied by a column vector of the indirect texture's alpha, red, and green channels (the blue channel isn't available). Each element of the matrix is a number in the range -1024 through 1023 (inclusive), which acts like the range -1 to 1023/1024. There is also a scale value, which functions as a power of 2 (biased by 17, so a value of 17 acts as 2^(17-17) = 2^0 = 1). The inability to represent the value +1 means that usually the scale is set 1 higher and the matrix value is set to .5 (i.e. 512).

The first indirect stage uses texture 1 with this matrix:

[ 0, .5, 0]
[.5,  0, 0]

with scale 14 indicating 2^(14-17) = 2^-3 = 1/8. This gives actual matrix values of:

[   0, 1/16, 0]
[1/16,    0, 0]

which indicates that s is the the red channel of texture 1 divided by 16, and t is the alpha channel of texture 1 divided by 16. This corresponds s being the EFB's red value (divided by 16) and t being the EFB's green value (divided by 16); note that s and t can have a fractional part (which can be used for linear interpolation in the palette). Since the red and green values range from 0 to 255, and the base texture coordinates (s0 and t0) that the indirect coordinate are being added to is always 0, our new coordinates both range from 0 to 255/16 (i.e. 15 + 15/16). So far, so good.

The second indirect stage uses texture 2 with this matrix:

[0, .5, 0]
[0,  0, 0]

with scale 23 indicating 2^(23-17) = 2^6 = 64. This gives actual matrix values of:

[0, 32, 0]
[0,  0, 0]

Thus, the new s value is texture 2's red channel multiplied by 32 (and t is 0). This corresponds to s being the EFB's blue value multiplied by 32. Since the blue value presumably ranges from 0 to 255, this would result in values from 32 to 8160 in increments of 32. The second indirect stage has add-to-previous enabled, so this value will be added to the previous stage's coordinates, as well as s0 and t0 (both 0). Thus, the final s value ranges from 0 to 8160 + 255/16 (i.e. 8176 - 1/16), which is bad because the texture is 512 by 16. Since the texture has clamping enabled, this means that blue values that are 16 or higher will all result in the rightmost part of the palette texture begin used (magenta to white), and values less than that will be 16 times as blue as they should be. This is clearly not right.

Incorrect ideas

Maybe the scale is wrong?

If the indirect matrix was supposed to multiply by 2 instead of 32, then it would change the range from 0-255 into 0-510. That range looks OK, except that it's still added to the previous stage's results, resulting in a range of 0-526. Furthermore, that means that blue offsets by 2, and it needs to offset by 16 instead. It would mostly prevent the pink, but coloration would be generally being wrong since the red channel no longer properly gets a range of 0-15. The bottom 4 bits of the blue channel need to be ignored.

Maybe it's something with the texture coordinate scale?

The texture coordinates are scaled multiplied based on BPMEM_SU_SSIZE and BPMEM_SU_TSIZE, which generally match the size of a texture. (I think this is the part that is when texture coordinates go from being called u/v to s/t, but I'm not entirely sure of the distinction and am always going to use s/t in this document). In this case, texture coordinate 0 is scaled by 512 horizontally and 16 vertically, while texture coordinate 1 is by 640 horizontally and 448 vertically.

The texture coord 1 scale makes sense for addressing the two EFB copies, but maybe the texture coord 0 scale also matters? It doesn't; this scale is applied before any other processing is applied to the texture coordinate, and not afterwards to the indirect coordinates. So, it applies to s0 and t0, but those are zero before, and multiplying zero by anything gives zero. Similarly, the scale seems to only be applied once, not multiple times after each indirect operation.

Maybe the indirect texture scale does something?

There is also an indirect texture scale function. Here, the value is set to 0 for both stages, but even if it wouldn't work, it wouldn't help as the scale is applied to the texture coordinate used for the indirect texture, not for the result of sampling the indirect texture (that's what the matrix is for). It could be used if you want to use the same texture coordinate for multiple textures (and the size differs by a power of 2, as the scale is a power of 2 to divide by). Since s0 and t0 are zero, this doesn't apply.

Maybe it's related to gamma correction?

For EFB copies, there's a field called "Gamma correction" which is showing as 1.0 for both texture 1 and texture 2. Although it feels like that could be related, this is actually a field that can only indicate values of 1.0, 1.7, and 2.2 (with a fourth value marked as "reserved" which might do something else, but it hasn't been documented). So that isn't it either.

The actual cause

The assumption that texture 2 (containing the EFB's blue channel) had values ranging from 0-255 was incorrect. The texture format allows for 0-255, but in practice the values only range from 0-15. This is because EA Sports Active is using the copy filter (previously discussed in the April/May 2018 progress report) for this! The copy filter's main purpose is to do deflickering, but it can also adjust brightness.

The first EFB copy (texture 1, with red and green) has the copy filter set to its default value (0, 0, 21, 22, 21, 0, 0); the first 2 values are used for the row of pixels above the current pixel, the middle 3 are used for the current row, and the last 2 values are used for the row after. The software renderer notes that there are multiple values per row for multisampling, which isn't currently implemented. It also mentions that the values should sum to 64 for the normal brightness to be used, which is the case.

The second EFB copy (texture 2, for blue) has the copy filter set to (0, 0, 1, 2, 1, 0, 0); this sums to 4. Note that 64/16 is 4, which means that the values here are a sixteenth of what they would be otherwise. This exactly matches the division needed to make everything work out, especially since the color values in the texture don't have fractional parts (unlike what would happen if they were divided after being converted to a texture coordinate).

But given that dolphin implemented the copy filter already, why doesn't this work? There is a "disable copy filter" setting, but that actually still applies brightness effects (it just sums all values together). The answer is that, for some reason, Dolphin was only applying the copy filter for XFB copies, and not for EFB copies. This is a trivial fix:

diff --git a/Source/Core/VideoCommon/BPStructs.cpp b/Source/Core/VideoCommon/BPStructs.cpp
index dd23b94da6..1ec097fe6e 100644
--- a/Source/Core/VideoCommon/BPStructs.cpp
+++ b/Source/Core/VideoCommon/BPStructs.cpp
@@ -274,14 +274,13 @@ static void BPWritten(const BPCmd& bp)
     if (PE_copy.copy_to_xfb == 0)
     {
       // bpmem.zcontrol.pixel_format to PixelFormat::Z24 is when the game wants to copy from ZBuffer
       // (Zbuffer uses 24-bit Format)
-      static constexpr CopyFilterCoefficients::Values filter_coefficients = {
-          {0, 0, 21, 22, 21, 0, 0}};
       bool is_depth_copy = bpmem.zcontrol.pixel_format == PixelFormat::Z24;
       g_texture_cache->CopyRenderTargetToTexture(
           destAddr, PE_copy.tp_realFormat(), copy_width, copy_height, destStride, is_depth_copy,
           srcRect, PE_copy.intensity_fmt, PE_copy.half_scale, 1.0f, 1.0f,
-          bpmem.triggerEFBCopy.clamp_top, bpmem.triggerEFBCopy.clamp_bottom, filter_coefficients);
+          bpmem.triggerEFBCopy.clamp_top, bpmem.triggerEFBCopy.clamp_bottom,
+          bpmem.copyfilter.GetCoefficients());
     }
     else
     {

With that, everything works!

Other possible implementations

While trying to understand how this effect was implemented, I came up with a few other ways that it could be implemented. I haven't actually tried to implement these and they may not actually work.

  • My first instinct while looking at this was to wonder why they were using the ITF_8 indirect texture format. As previously discussed with the Skyward Sword map issue, the other formats let you use the top 5, 4, or 3 bits for the purpose of indirect offsets instead of using all 8 bits. ITF_4 would be ideal here. In that case, the blue texture would give values of 0, 16, 32, 48, 64, etc., up to 240. Then the indirect matrix could be used to multiply that by 2 to get offsets of 0, 32, 64, etc. (Of course, if they had used it, it would have been broken in Dolphin until a few months ago when the Skyward Sword issue was fixed.) Perhaps bump alpha could also be used to interpolate between different blue values similar to how Skyward Sword does it.
  • Similarly, using IA4 instead of I8 as the texture format for the blue channel could also work since that extracts the top 4 bits (but I don't think that has any advantages over ITF_4; in fact it might behave exactly the same?).
  • As I mentioned earlier on, the gaps between 16 by 16 areas didn't need to be 16 pixels wide. They probably could have had 2-pixel gaps and offset it by 18 instead. This might have been harder with ITF_4, due to the precision available with the indirect matrices, but a 4-pixel gap would allow multiplication by 5/4 (i.e. 20/16) which would fit cleanly. I also might be unaware of some limitations of non-power-of-2 textures (I do know they cause issues with wrapping, but here the wrap mode is clamp); it's possible that they use up the same amount of memory as the encompassing power-of-2 texture.
  • They might not have needed any gap at all if they multiplied red and green values by 15/16 and applied a 1/32 offset. Then, the linear filtering for the texture would look at pixels in a 16 by 16 area instead of a 17 by 17 area. Although it's not possible to apply an offset with the indirect matrix, indirect texture coordinates are added to the direct coordinates (s0 and s1) which were set to 0 for all 4 vertices, so setting them to 1/32 would probably work. There might be complications to this (such as pixel centers being 7/12, though that might not apply here), but the benefits would be halving the size of the palette texture (i.e. it would be 256 by 16 instead of 512 by 16) and better linear interpolation (which may not matter that much).
  • They could have used multiple textures (one (16 by 16) for both red and green and a second one (16 by 1) for blue, or three separate textures (all 16 by 1) for red, green, and blue individually) instead of trying to simulate a 3D texture. Then, additional TEV stages could have added the different results together. Using 3 separate textures also means that the textures could be 256 by 1, allowing for more detailed color choices instead of relying on linear interpolation. Even with 16 by 1 textures, blue could now be linearly interpolated along with red and green. This probably would require additional TEV stages, and would not be able to model the same functions. I'm not sure of a good way to describe this (it's not so much that the red channel couldn't vary based on the blue channel, as each of the textures would still have control over RGB, and it's not exactly limited as a linear combination as the individual textures can model any function); in pure math terms, instead of having the new red channel r_n be a function f(r, g, b), it would instead be f_1(r) + f_2(g) + f_3(b), and similar would apply to the green and blue channels. This might not be enough to exactly match the current effect.
  • Rather than having an extra texture coordinate that is set to 0 for all vertices, the ITW_0 wrap mode could be used to set the value to 0 (in which case it could use the same coordinate as is used for the indirect texture lookup. Of course, this would conflict with using that texture coordinate for a 1/32 offset. (With how add-to-previous works, it may be necessary to use ITW_0 for one of the stages and ITW_OFF on the other, so that the texture coordinate is only added once.)

Summary

EA Sports Active attempts to filter colors. It does this by trying to simulate a 3D texture with a 2D texture, using indirect textures with an EFB copy to map the red and green channels into a 16 by 16 area. It then multiplies the blue channel by 32 to pick one of those 16 by 16 areas. However, the blue channel needs to be divided by 16 first for this to work properly, and the game uses the copy filter to do this. Although Dolphin does have an implementation of the copy filter, it only used it on XFB copies and not on EFB copies.

@Pokechu22
Copy link
Author

Here are some additional images of the two EFB copies before and after the fix that might make things clearer: https://imgur.com/a/xxjW2xp

@Pokechu22
Copy link
Author

Pokechu22 commented Feb 9, 2022

And here is some further explanation as to why the rightmost shirt still looks wrong, along with some more images of the palette which might be easier to understand, and transformed blue channel: dolphin-emu/dolphin#10439 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment