Panda3DS/src/core/renderer_gl/renderer_gl.cpp
wheremyfoodat e854d4ba63 Formatting
2023-07-04 18:15:27 +03:00

993 lines
No EOL
36 KiB
C++

#include "renderer_gl/renderer_gl.hpp"
#include "PICA/float_types.hpp"
#include "PICA/gpu.hpp"
#include "PICA/regs.hpp"
using namespace Floats;
using namespace Helpers;
using namespace PICA;
const char* vertexShader = R"(
#version 410 core
layout (location = 0) in vec4 a_coords;
layout (location = 1) in vec4 a_quaternion;
layout (location = 2) in vec4 a_vertexColour;
layout (location = 3) in vec2 a_texcoord0;
layout (location = 4) in vec2 a_texcoord1;
layout (location = 5) in float a_texcoord0_w;
layout (location = 6) in vec3 a_view;
layout (location = 7) in vec2 a_texcoord2;
out vec3 v_normal;
out vec3 v_tangent;
out vec3 v_bitangent;
out vec4 v_colour;
out vec3 v_texcoord0;
out vec2 v_texcoord1;
out vec3 v_view;
out vec2 v_texcoord2;
flat out vec4 v_textureEnvColor[6];
flat out vec4 v_textureEnvBufferColor;
// TEV uniforms
uniform uint u_textureEnvColor[6];
uniform uint u_textureEnvBufferColor;
uniform uint u_picaRegs[0x200-0x47];
//Helper so that the implementation of u_pica_regs can be changed later
uint readPicaReg(uint reg_addr){
return u_picaRegs[reg_addr-0x47];
}
vec4 abgr8888ToVec4(uint abgr) {
const float scale = 1.0 / 255.0;
return scale * vec4(
float(abgr & 0xffu),
float((abgr >> 8) & 0xffu),
float((abgr >> 16) & 0xffu),
float(abgr >> 24)
);
}
vec3 rotateVec3ByQuaternion(vec3 v, vec4 q){
vec3 u=q.xyz;
float s = q.w;
return 2.0*dot(u, v)*u + (s*s - dot(u, u))*v + 2.0*s*cross(u, v);
}
void main() {
gl_Position = a_coords;
v_colour = a_vertexColour;
// Flip y axis of UVs because OpenGL uses an inverted y for texture sampling compared to the PICA
v_texcoord0 = vec3(a_texcoord0.x, 1.0 - a_texcoord0.y, a_texcoord0_w);
v_texcoord1 = vec2(a_texcoord1.x, 1.0 - a_texcoord1.y);
v_texcoord2 = vec2(a_texcoord2.x, 1.0 - a_texcoord2.y);
v_view = a_view;
v_normal = normalize(rotateVec3ByQuaternion(vec3(0.0,0.0,1.0), a_quaternion));
v_tangent = normalize(rotateVec3ByQuaternion(vec3(1.0,0.0,0.0), a_quaternion));
v_bitangent = normalize(rotateVec3ByQuaternion(vec3(0.0,1.0,0.0), a_quaternion));
for (int i = 0; i < 6; i++) {
v_textureEnvColor[i] = abgr8888ToVec4(u_textureEnvColor[i]);
}
v_textureEnvBufferColor = abgr8888ToVec4(u_textureEnvBufferColor);
}
)";
const char* fragmentShader = R"(
#version 410 core
in vec3 v_tangent;
in vec3 v_normal;
in vec3 v_bitangent;
in vec4 v_colour;
in vec3 v_texcoord0;
in vec2 v_texcoord1;
in vec3 v_view;
in vec2 v_texcoord2;
flat in vec4 v_textureEnvColor[6];
flat in vec4 v_textureEnvBufferColor;
out vec4 fragColour;
uniform uint u_alphaControl;
uniform uint u_textureConfig;
// TEV uniforms
uniform uint u_textureEnvSource[6];
uniform uint u_textureEnvOperand[6];
uniform uint u_textureEnvCombiner[6];
uniform uint u_textureEnvScale[6];
uniform uint u_textureEnvUpdateBuffer;
// Depth control uniforms
uniform float u_depthScale;
uniform float u_depthOffset;
uniform bool u_depthmapEnable;
uniform sampler2D u_tex0;
uniform sampler2D u_tex1;
uniform sampler2D u_tex2;
uniform sampler1DArray u_tex_lighting_lut;
uniform uint u_picaRegs[0x200-0x47];
//Helper so that the implementation of u_pica_regs can be changed later
uint readPicaReg(uint reg_addr){
return u_picaRegs[reg_addr-0x47];
}
vec4 tevSources[16];
vec4 tevNextPreviousBuffer;
bool tevUnimplementedSourceFlag = false;
// OpenGL ES 1.1 reference pages for TEVs (this is what the PICA200 implements):
// https://registry.khronos.org/OpenGL-Refpages/es1.1/xhtml/glTexEnv.xml
vec4 tevFetchSource(uint src_id) {
if (src_id >= 6u && src_id < 13u) {
tevUnimplementedSourceFlag = true;
}
return tevSources[src_id];
}
vec4 tevGetColorAndAlphaSource(int tev_id, int src_id) {
vec4 result;
vec4 colorSource = tevFetchSource((u_textureEnvSource[tev_id] >> (src_id * 4)) & 15u);
vec4 alphaSource = tevFetchSource((u_textureEnvSource[tev_id] >> (src_id * 4 + 16)) & 15u);
uint colorOperand = (u_textureEnvOperand[tev_id] >> (src_id * 4)) & 15u;
uint alphaOperand = (u_textureEnvOperand[tev_id] >> (12 + src_id * 4)) & 7u;
// TODO: figure out what the undocumented values do
switch (colorOperand) {
case 0u: result.rgb = colorSource.rgb; break; // Source color
case 1u: result.rgb = 1.0 - colorSource.rgb; break; // One minus source color
case 2u: result.rgb = vec3(colorSource.a); break; // Source alpha
case 3u: result.rgb = vec3(1.0 - colorSource.a); break; // One minus source alpha
case 4u: result.rgb = vec3(colorSource.r); break; // Source red
case 5u: result.rgb = vec3(1.0 - colorSource.r); break; // One minus source red
case 8u: result.rgb = vec3(colorSource.g); break; // Source green
case 9u: result.rgb = vec3(1.0 - colorSource.g); break; // One minus source green
case 12u: result.rgb = vec3(colorSource.b); break; // Source blue
case 13u: result.rgb = vec3(1.0 - colorSource.b); break; // One minus source blue
default: break;
}
// TODO: figure out what the undocumented values do
switch (alphaOperand) {
case 0u: result.a = alphaSource.a; break; // Source alpha
case 1u: result.a = 1.0 - alphaSource.a; break; // One minus source alpha
case 2u: result.a = alphaSource.r; break; // Source red
case 3u: result.a = 1.0 - alphaSource.r; break; // One minus source red
case 4u: result.a = alphaSource.g; break; // Source green
case 5u: result.a = 1.0 - alphaSource.g; break; // One minus source green
case 6u: result.a = alphaSource.b; break; // Source blue
case 7u: result.a = 1.0 - alphaSource.b; break; // One minus source blue
default: break;
}
return result;
}
vec4 tevCalculateCombiner(int tev_id) {
vec4 source0 = tevGetColorAndAlphaSource(tev_id, 0);
vec4 source1 = tevGetColorAndAlphaSource(tev_id, 1);
vec4 source2 = tevGetColorAndAlphaSource(tev_id, 2);
uint colorCombine = u_textureEnvCombiner[tev_id] & 15u;
uint alphaCombine = (u_textureEnvCombiner[tev_id] >> 16) & 15u;
vec4 result = vec4(1.0);
// TODO: figure out what the undocumented values do
switch (colorCombine) {
case 0u: result.rgb = source0.rgb; break; // Replace
case 1u: result.rgb = source0.rgb * source1.rgb; break; // Modulate
case 2u: result.rgb = min(vec3(1.0), source0.rgb + source1.rgb); break; // Add
case 3u: result.rgb = clamp(source0.rgb + source1.rgb - 0.5, 0.0, 1.0); break; // Add signed
case 4u: result.rgb = mix(source1.rgb, source0.rgb, source2.rgb); break; // Interpolate
case 5u: result.rgb = max(source0.rgb - source1.rgb, 0.0); break; // Subtract
case 6u: result.rgb = vec3(4.0 * dot(source0.rgb - 0.5 , source1.rgb - 0.5)); break; // Dot3 RGB
case 7u: result = vec4(4.0 * dot(source0.rgb - 0.5 , source1.rgb - 0.5)); break; // Dot3 RGBA
case 8u: result.rgb = min(source0.rgb * source1.rgb + source2.rgb, 1.0); break; // Multiply then add
case 9u: result.rgb = min((source0.rgb + source1.rgb) * source2.rgb, 1.0); break; // Add then multiply
default: break;
}
if (colorCombine != 7u) { // The color combiner also writes the alpha channel in the "Dot3 RGBA" mode.
// TODO: figure out what the undocumented values do
// TODO: test if the alpha combiner supports all the same modes as the color combiner.
switch (alphaCombine) {
case 0u: result.a = source0.a; break; // Replace
case 1u: result.a = source0.a * source1.a; break; // Modulate
case 2u: result.a = min(1.0, source0.a + source1.a); break; // Add
case 3u: result.a = clamp(source0.a + source1.a - 0.5, 0.0, 1.0); break; // Add signed
case 4u: result.a = mix(source1.a, source0.a, source2.a); break; // Interpolate
case 5u: result.a = max(0.0, source0.a - source1.a); break; // Subtract
case 8u: result.a = min(1.0, source0.a * source1.a + source2.a); break; // Multiply then add
case 9u: result.a = min(1.0, (source0.a + source1.a) * source2.a); break; // Add then multiply
default: break;
}
}
result.rgb *= float(1 << (u_textureEnvScale[tev_id] & 3u));
result.a *= float(1 << ((u_textureEnvScale[tev_id] >> 16) & 3u));
return result;
}
#define D0_LUT 0u
#define D1_LUT 1u
#define SP_LUT 2u
#define FR_LUT 3u
#define RB_LUT 4u
#define RG_LUT 5u
#define RR_LUT 6u
float lutLookup(uint lut, uint light, float value){
if(lut>=FR_LUT&&lut<=RR_LUT)lut-=1;
if(lut==SP_LUT)lut=8+light;
return texture(u_tex_lighting_lut,vec2(value,lut)).r;
}
vec3 regToColor(uint reg){
return vec3(
float(bitfieldExtract(reg,20,8))/255.,
float(bitfieldExtract(reg,10,8))/255.,
float(bitfieldExtract(reg,00,8))/255.
);
}
float decodeFP(uint hex, uint E, uint M){
uint width = M + E + 1u;
uint bias = 128u - (1u << (E - 1u));
uint exponent = (hex >> M) & ((1u << E) - 1u);
uint mantissa = hex & ((1u << M) - 1u);
uint sign = (hex >> (E + M)) << 31u;
if ((hex & ((1u << (width - 1u)) - 1u)) != 0) {
if (exponent == (1u << E) - 1u) exponent = 255u;
else exponent += bias;
hex = sign | (mantissa << (23u - M)) | (exponent << 23u);
}else hex = sign;
return uintBitsToFloat(hex);
}
//Implements the following algorthm: https://mathb.in/26766
void calcLighting(out vec4 primary_color, out vec4 secondary_color){
// Quaternions describe a transformation from surface-local space to eye space.
// In surface-local space, by definition (and up to permutation) the normal vector is (0,0,1),
// the tangent vector is (1,0,0), and the bitangent vector is (0,1,0).
vec3 normal = normalize(v_normal );
vec3 tangent = normalize(v_tangent );
vec3 bitangent = normalize(v_bitangent);
vec3 view = normalize(v_view);
uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008F);
if(bitfieldExtract(GPUREG_LIGHTING_ENABLE,0,1)==0){
primary_color = secondary_color = vec4(1.0);
return;
}
uint GPUREG_LIGHTING_AMBIENT = readPicaReg(0x01C0);
uint GPUREG_LIGHTING_NUM_LIGHTS = (readPicaReg(0x01C2)&0x7u)+1;
uint GPUREG_LIGHTING_LIGHT_PERMUTATION = readPicaReg(0x01D9);
primary_color = vec4(vec3(0.0),1.0);
secondary_color = vec4(vec3(0.0),1.0);
primary_color.rgb += regToColor(GPUREG_LIGHTING_AMBIENT);
uint GPUREG_LIGHTING_LUTINPUT_ABS = readPicaReg(0x01D0);
uint GPUREG_LIGHTING_LUTINPUT_SELECT = readPicaReg(0x01D1);
uint GPUREG_LIGHTING_CONFIG0 = readPicaReg(0x01C3);
uint GPUREG_LIGHTING_CONFIG1 = readPicaReg(0x01C4);
uint GPUREG_LIGHTING_LUTINPUT_SCALE = readPicaReg(0x01D2);
float d[7];
bool error_unimpl = false;
for(uint i = 0; i<GPUREG_LIGHTING_NUM_LIGHTS;++i){
uint light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION,int(i*3),3);
uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140+0x10*light_id);
uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141+0x10*light_id);
uint GPUREG_LIGHTi_DIFFUSE = readPicaReg(0x0142+0x10*light_id);
uint GPUREG_LIGHTi_AMBIENT = readPicaReg(0x0143+0x10*light_id);
uint GPUREG_LIGHTi_VECTOR_LOW = readPicaReg(0x0144+0x10*light_id);
uint GPUREG_LIGHTi_VECTOR_HIGH= readPicaReg(0x0145+0x10*light_id);
uint GPUREG_LIGHTi_CONFIG = readPicaReg(0x0149+0x10*light_id);
vec3 light_vector = normalize(vec3(
decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW,0,16),5,10),
decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW,16,16),5,10),
decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_HIGH,0,16),5,10)
));
//Positional Light
if(bitfieldExtract(GPUREG_LIGHTi_CONFIG,0,1)==0)error_unimpl = true;
vec3 half_vector = normalize(normalize(light_vector)+view);
for(int c=0;c<7u;++c){
if(bitfieldExtract(GPUREG_LIGHTING_CONFIG1,16+c,1)==0){
uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE,c*4,3);
float scale = float(1u<<scale_id);
if(scale_id>=6u) scale/=256.0;
uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT,c*4,3);
if(input_id==0u)d[c] = dot(normal,half_vector);
else if(input_id==1u)d[c] = dot(view,half_vector);
else if(input_id==2u)d[c] = dot(normal,view);
else if(input_id==3u)d[c] = dot(light_vector,normal);
else if(input_id==4u){
uint GPUREG_LIGHTi_SPOTDIR_LOW = readPicaReg(0x0146+0x10*light_id);
uint GPUREG_LIGHTi_SPOTDIR_HIGH= readPicaReg(0x0147+0x10*light_id);
vec3 spot_light_vector = normalize(vec3(
decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW,0,16),1,11),
decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW,16,16),1,11),
decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH,0,16),1,11)
));
d[c] = dot(-light_vector,spot_light_vector);// -L . P (aka Spotlight aka SP);
}else if(input_id==5u){
d[c] = 1.0;//TODO: cos <greek symbol> (aka CP);
error_unimpl = true;
}else d[c] = 1.0;
d[c] = lutLookup(c,light_id,d[c]*0.5+0.5)*scale;
if(bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS,2*c,1)!=0u)d[c]=abs(d[c]);
}else d[c]=1.0;
}
uint lookup_config = bitfieldExtract(GPUREG_LIGHTi_CONFIG,4,4);
if(lookup_config==0){
d[D1_LUT] = 0.0;
d[FR_LUT] = 0.0;
d[RG_LUT]=d[RB_LUT]=d[RR_LUT];
}else if(lookup_config==1){
d[D0_LUT] = 0.0;
d[D1_LUT] = 0.0;
d[RG_LUT]=d[RB_LUT]=d[RR_LUT];
}else if(lookup_config==2){
d[FR_LUT] = 0.0;
d[SP_LUT] = 0.0;
d[RG_LUT]=d[RB_LUT]=d[RR_LUT];
}else if(lookup_config==3){
d[SP_LUT] = 0.0;
d[RG_LUT]=d[RB_LUT]=d[RR_LUT]=1.0;
}else if(lookup_config==4)d[FR_LUT] = 0.0;
else if(lookup_config==5)d[D1_LUT] = 0.0;
else if(lookup_config==6)d[RG_LUT]=d[RB_LUT]=d[RR_LUT];
float distance_factor = 1.0; //a
float indirect_factor = 1.0;//fi
float shadow_factor = 1.0; //o
float NdotL = dot(normal,light_vector); //Li*N
//Two sided diffuse
if(bitfieldExtract(GPUREG_LIGHTi_CONFIG,1,1)==0)NdotL=max(0.0,NdotL);
else NdotL=abs(NdotL);
float light_factor = distance_factor*d[SP_LUT]*indirect_factor*shadow_factor;
primary_color.rgb += light_factor*(regToColor(GPUREG_LIGHTi_AMBIENT) + regToColor(GPUREG_LIGHTi_DIFFUSE)*NdotL);
secondary_color.rgb += light_factor*(
regToColor(GPUREG_LIGHTi_SPECULAR0)*d[D0_LUT]+
regToColor(GPUREG_LIGHTi_SPECULAR1)*d[D1_LUT]*vec3(d[RR_LUT],d[RG_LUT],d[RB_LUT])
);
}
uint fresnel_output1 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0,2,1);
uint fresnel_output2 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0,3,1);
if(fresnel_output1==1u) primary_color.a = d[FR_LUT];
if(fresnel_output2==1u) secondary_color.a = d[FR_LUT];
if(error_unimpl){
secondary_color = primary_color = vec4(1.0,0.,1.0,1.0);
}
}
void main() {
vec2 tex2UV = (u_textureConfig & (1u << 13)) != 0u ? v_texcoord1 : v_texcoord2;
// TODO: what do invalid sources and disabled textures read as?
// And what does the "previous combiner" source read initially?
tevSources[0] = v_colour; // Primary/vertex color
calcLighting(tevSources[1],tevSources[2]);
if ((u_textureConfig & 1u) != 0u) tevSources[3] = texture(u_tex0, v_texcoord0.xy);
if ((u_textureConfig & 2u) != 0u) tevSources[4] = texture(u_tex1, v_texcoord1);
if ((u_textureConfig & 4u) != 0u) tevSources[5] = texture(u_tex2, tex2UV);
tevSources[13] = vec4(0.0); // Previous buffer
tevSources[15] = vec4(0.0); // Previous combiner
tevNextPreviousBuffer = v_textureEnvBufferColor;
for (int i = 0; i < 6; i++) {
tevSources[14] = v_textureEnvColor[i]; // Constant color
tevSources[15] = tevCalculateCombiner(i);
tevSources[13] = tevNextPreviousBuffer;
if (i < 4) {
if ((u_textureEnvUpdateBuffer & (0x100u << i)) != 0u) {
tevNextPreviousBuffer.rgb = tevSources[15].rgb;
}
if ((u_textureEnvUpdateBuffer & (0x1000u << i)) != 0u) {
tevNextPreviousBuffer.a = tevSources[15].a;
}
}
}
fragColour = tevSources[15];
if (tevUnimplementedSourceFlag) {
// fragColour = vec4(1.0, 0.0, 1.0, 1.0);
}
//fragColour.rg = texture(u_tex_lighting_lut,vec2(gl_FragCoord.x/200.,float(int(gl_FragCoord.y/2)%24))).rr;
// Get original depth value by converting from [near, far] = [0, 1] to [-1, 1]
// We do this by converting to [0, 2] first and subtracting 1 to go to [-1, 1]
float z_over_w = gl_FragCoord.z * 2.0f - 1.0f;
float depth = z_over_w * u_depthScale + u_depthOffset;
if (!u_depthmapEnable) // Divide z by w if depthmap enable == 0 (ie using W-buffering)
depth /= gl_FragCoord.w;
// Write final fragment depth
gl_FragDepth = depth;
if ((u_alphaControl & 1u) != 0u) { // Check if alpha test is on
uint func = (u_alphaControl >> 4u) & 7u;
float reference = float((u_alphaControl >> 8u) & 0xffu) / 255.0;
float alpha = fragColour.a;
switch (func) {
case 0: discard; // Never pass alpha test
case 1: break; // Always pass alpha test
case 2: // Pass if equal
if (alpha != reference)
discard;
break;
case 3: // Pass if not equal
if (alpha == reference)
discard;
break;
case 4: // Pass if less than
if (alpha >= reference)
discard;
break;
case 5: // Pass if less than or equal
if (alpha > reference)
discard;
break;
case 6: // Pass if greater than
if (alpha <= reference)
discard;
break;
case 7: // Pass if greater than or equal
if (alpha < reference)
discard;
break;
}
}
}
)";
const char* displayVertexShader = R"(
#version 410 core
out vec2 UV;
void main() {
const vec4 positions[4] = vec4[](
vec4(-1.0, 1.0, 1.0, 1.0), // Top-left
vec4(1.0, 1.0, 1.0, 1.0), // Top-right
vec4(-1.0, -1.0, 1.0, 1.0), // Bottom-left
vec4(1.0, -1.0, 1.0, 1.0) // Bottom-right
);
// The 3DS displays both screens' framebuffer rotated 90 deg counter clockwise
// So we adjust our texcoords accordingly
const vec2 texcoords[4] = vec2[](
vec2(1.0, 1.0), // Top-right
vec2(1.0, 0.0), // Bottom-right
vec2(0.0, 1.0), // Top-left
vec2(0.0, 0.0) // Bottom-left
);
gl_Position = positions[gl_VertexID];
UV = texcoords[gl_VertexID];
}
)";
const char* displayFragmentShader = R"(
#version 410 core
in vec2 UV;
out vec4 FragColor;
uniform sampler2D u_texture;
void main() {
FragColor = texture(u_texture, UV);
}
)";
void Renderer::reset() {
depthBufferCache.reset();
colourBufferCache.reset();
textureCache.reset();
// Init the colour/depth buffer settings to some random defaults on reset
colourBufferLoc = 0;
colourBufferFormat = PICA::ColorFmt::RGBA8;
depthBufferLoc = 0;
depthBufferFormat = PICA::DepthFmt::Depth16;
if (triangleProgram.exists()) {
const auto oldProgram = OpenGL::getProgram();
triangleProgram.use();
oldAlphaControl = 0; // Default alpha control to 0
oldTexUnitConfig = 0; // Default tex unit config to 0
oldDepthScale = -1.0; // Default depth scale to -1.0, which is what games typically use
oldDepthOffset = 0.0; // Default depth offset to 0
oldDepthmapEnable = false; // Enable w buffering
glUniform1ui(alphaControlLoc, oldAlphaControl);
glUniform1ui(texUnitConfigLoc, oldTexUnitConfig);
glUniform1f(depthScaleLoc, oldDepthScale);
glUniform1f(depthOffsetLoc, oldDepthOffset);
glUniform1i(depthmapEnableLoc, oldDepthmapEnable);
glUseProgram(oldProgram); // Switch to old GL program
}
}
void Renderer::initGraphicsContext() {
OpenGL::Shader vert(vertexShader, OpenGL::Vertex);
OpenGL::Shader frag(fragmentShader, OpenGL::Fragment);
triangleProgram.create({ vert, frag });
triangleProgram.use();
alphaControlLoc = OpenGL::uniformLocation(triangleProgram, "u_alphaControl");
texUnitConfigLoc = OpenGL::uniformLocation(triangleProgram, "u_textureConfig");
textureEnvSourceLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvSource");
textureEnvOperandLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvOperand");
textureEnvCombinerLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvCombiner");
textureEnvColorLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvColor");
textureEnvScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvScale");
textureEnvUpdateBufferLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvUpdateBuffer");
textureEnvBufferColorLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvBufferColor");
depthScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_depthScale");
depthOffsetLoc = OpenGL::uniformLocation(triangleProgram, "u_depthOffset");
depthmapEnableLoc = OpenGL::uniformLocation(triangleProgram, "u_depthmapEnable");
picaRegLoc = OpenGL::uniformLocation(triangleProgram, "u_picaRegs");
// Init sampler objects
glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex0"), 0);
glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex1"), 1);
glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex2"), 2);
glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex_lighting_lut"), 3);
OpenGL::Shader vertDisplay(displayVertexShader, OpenGL::Vertex);
OpenGL::Shader fragDisplay(displayFragmentShader, OpenGL::Fragment);
displayProgram.create({ vertDisplay, fragDisplay });
displayProgram.use();
glUniform1i(OpenGL::uniformLocation(displayProgram, "u_texture"), 0); // Init sampler object
vbo.createFixedSize(sizeof(PicaVertex) * vertexBufferSize, GL_STREAM_DRAW);
vbo.bind();
vao.create();
vao.bind();
// Position (x, y, z, w) attributes
vao.setAttributeFloat<float>(0, 4, sizeof(PicaVertex), offsetof(PicaVertex, s.positions));
vao.enableAttribute(0);
// Quaternion attribute
vao.setAttributeFloat<float>(1, 4, sizeof(PicaVertex), offsetof(PicaVertex, s.quaternion));
vao.enableAttribute(1);
// Colour attribute
vao.setAttributeFloat<float>(2, 4, sizeof(PicaVertex), offsetof(PicaVertex, s.colour));
vao.enableAttribute(2);
// UV 0 attribute
vao.setAttributeFloat<float>(3, 2, sizeof(PicaVertex), offsetof(PicaVertex, s.texcoord0));
vao.enableAttribute(3);
// UV 1 attribute
vao.setAttributeFloat<float>(4, 2, sizeof(PicaVertex), offsetof(PicaVertex, s.texcoord1));
vao.enableAttribute(4);
// UV 0 W-component attribute
vao.setAttributeFloat<float>(5, 1, sizeof(PicaVertex), offsetof(PicaVertex, s.texcoord0_w));
vao.enableAttribute(5);
// View
vao.setAttributeFloat<float>(6, 3, sizeof(PicaVertex), offsetof(PicaVertex, s.view));
vao.enableAttribute(6);
// UV 2 attribute
vao.setAttributeFloat<float>(7, 2, sizeof(PicaVertex), offsetof(PicaVertex, s.texcoord2));
vao.enableAttribute(7);
dummyVBO.create();
dummyVAO.create();
// Create texture and framebuffer for the 3DS screen
const u32 screenTextureWidth = 2 * 400; // Top screen is 400 pixels wide, bottom is 320
const u32 screenTextureHeight = 2 * 240; // Both screens are 240 pixels tall
glGenTextures(1,&lightLUTTextureArray);
auto prevTexture = OpenGL::getTex2D();
screenTexture.create(screenTextureWidth, screenTextureHeight, GL_RGBA8);
screenTexture.bind();
screenTexture.setMinFilter(OpenGL::Linear);
screenTexture.setMagFilter(OpenGL::Linear);
glBindTexture(GL_TEXTURE_2D, prevTexture);
screenFramebuffer.createWithDrawTexture(screenTexture);
screenFramebuffer.bind(OpenGL::DrawAndReadFramebuffer);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
Helpers::panic("Incomplete framebuffer");
// TODO: This should not clear the framebuffer contents. It should load them from VRAM.
GLint oldViewport[4];
glGetIntegerv(GL_VIEWPORT, oldViewport);
OpenGL::setViewport(screenTextureWidth, screenTextureHeight);
OpenGL::setClearColor(0.0, 0.0, 0.0, 1.0);
OpenGL::clearColor();
OpenGL::setViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]);
reset();
}
// Set up the OpenGL blending context to match the emulated PICA
void Renderer::setupBlending() {
const bool blendingEnabled = (regs[PICA::InternalRegs::ColourOperation] & (1 << 8)) != 0;
// Map of PICA blending equations to OpenGL blending equations. The unused blending equations are equivalent to equation 0 (add)
static constexpr std::array<GLenum, 8> blendingEquations = {
GL_FUNC_ADD, GL_FUNC_SUBTRACT, GL_FUNC_REVERSE_SUBTRACT, GL_MIN, GL_MAX, GL_FUNC_ADD, GL_FUNC_ADD, GL_FUNC_ADD
};
// Map of PICA blending funcs to OpenGL blending funcs. Func = 15 is undocumented and stubbed to GL_ONE for now
static constexpr std::array<GLenum, 16> blendingFuncs = {
GL_ZERO, GL_ONE, GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_DST_COLOR, GL_ONE_MINUS_DST_COLOR, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_CONSTANT_COLOR, GL_ONE_MINUS_CONSTANT_COLOR, GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA,
GL_SRC_ALPHA_SATURATE, GL_ONE
};
if (!blendingEnabled) {
OpenGL::disableBlend();
} else {
OpenGL::enableBlend();
// Get blending equations
const u32 blendControl = regs[PICA::InternalRegs::BlendFunc];
const u32 rgbEquation = blendControl & 0x7;
const u32 alphaEquation = getBits<8, 3>(blendControl);
// Get blending functions
const u32 rgbSourceFunc = getBits<16, 4>(blendControl);
const u32 rgbDestFunc = getBits<20, 4>(blendControl);
const u32 alphaSourceFunc = getBits<24, 4>(blendControl);
const u32 alphaDestFunc = getBits<28, 4>(blendControl);
const u32 constantColor = regs[PICA::InternalRegs::BlendColour];
const u32 r = constantColor & 0xff;
const u32 g = getBits<8, 8>(constantColor);
const u32 b = getBits<16, 8>(constantColor);
const u32 a = getBits<24, 8>(constantColor);
OpenGL::setBlendColor(float(r) / 255.f, float(g) / 255.f, float(b) / 255.f, float(a) / 255.f);
// Translate equations and funcs to their GL equivalents and set them
glBlendEquationSeparate(blendingEquations[rgbEquation], blendingEquations[alphaEquation]);
glBlendFuncSeparate(blendingFuncs[rgbSourceFunc], blendingFuncs[rgbDestFunc], blendingFuncs[alphaSourceFunc], blendingFuncs[alphaDestFunc]);
}
}
void Renderer::setupTextureEnvState() {
// TODO: Only update uniforms when the TEV config changed. Use an UBO potentially.
static constexpr std::array<u32, 6> ioBases = {
PICA::InternalRegs::TexEnv0Source, PICA::InternalRegs::TexEnv1Source,
PICA::InternalRegs::TexEnv2Source, PICA::InternalRegs::TexEnv3Source,
PICA::InternalRegs::TexEnv4Source, PICA::InternalRegs::TexEnv5Source
};
u32 textureEnvSourceRegs[6];
u32 textureEnvOperandRegs[6];
u32 textureEnvCombinerRegs[6];
u32 textureEnvColourRegs[6];
u32 textureEnvScaleRegs[6];
for (int i = 0; i < 6; i++) {
const u32 ioBase = ioBases[i];
textureEnvSourceRegs[i] = regs[ioBase];
textureEnvOperandRegs[i] = regs[ioBase + 1];
textureEnvCombinerRegs[i] = regs[ioBase + 2];
textureEnvColourRegs[i] = regs[ioBase + 3];
textureEnvScaleRegs[i] = regs[ioBase + 4];
}
glUniform1uiv(textureEnvSourceLoc, 6, textureEnvSourceRegs);
glUniform1uiv(textureEnvOperandLoc, 6, textureEnvOperandRegs);
glUniform1uiv(textureEnvCombinerLoc, 6, textureEnvCombinerRegs);
glUniform1uiv(textureEnvColorLoc, 6, textureEnvColourRegs);
glUniform1uiv(textureEnvScaleLoc, 6, textureEnvScaleRegs);
glUniform1ui(textureEnvUpdateBufferLoc, regs[PICA::InternalRegs::TexEnvUpdateBuffer]);
glUniform1ui(textureEnvBufferColorLoc, regs[PICA::InternalRegs::TexEnvBufferColor]);
}
void Renderer::bindTexturesToSlots() {
static constexpr std::array<u32, 3> ioBases = {
PICA::InternalRegs::Tex0BorderColor, PICA::InternalRegs::Tex1BorderColor, PICA::InternalRegs::Tex2BorderColor
};
for (int i = 0; i < 3; i++) {
if ((regs[PICA::InternalRegs::TexUnitCfg] & (1 << i)) == 0) {
continue;
}
const size_t ioBase = ioBases[i];
const u32 dim = regs[ioBase + 1];
const u32 config = regs[ioBase + 2];
const u32 height = dim & 0x7ff;
const u32 width = getBits<16, 11>(dim);
const u32 addr = (regs[ioBase + 4] & 0x0FFFFFFF) << 3;
u32 format = regs[ioBase + (i == 0 ? 13 : 5)] & 0xF;
glActiveTexture(GL_TEXTURE0 + i);
Texture targetTex(addr, static_cast<PICA::TextureFmt>(format), width, height, config);
OpenGL::Texture tex = getTexture(targetTex);
tex.bind();
}
glActiveTexture(GL_TEXTURE0 + 3);
glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray);
glActiveTexture(GL_TEXTURE0);
// Update the texture unit configuration uniform if it changed
const u32 texUnitConfig = regs[PICA::InternalRegs::TexUnitCfg];
if (oldTexUnitConfig != texUnitConfig) {
oldTexUnitConfig = texUnitConfig;
glUniform1ui(texUnitConfigLoc, texUnitConfig);
}
}
void Renderer::updateLightingLUT(){
std::array<u16, GPU::LightingLutSize> u16_lightinglut;
for(int i = 0; i < gpu.lightingLUT.size(); i++){
uint64_t value = gpu.lightingLUT[i] & ((1 << 12) - 1);
u16_lightinglut[i] = value * 65535 / 4095;
}
glActiveTexture(GL_TEXTURE0 + 3);
glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray);
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R16, 256, Lights::LUT_Count, 0, GL_RED, GL_UNSIGNED_SHORT, u16_lightinglut.data());
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glActiveTexture(GL_TEXTURE0);
gpu.lightingLUTDirty = false;
}
void Renderer::drawVertices(PICA::PrimType primType, std::span<const PicaVertex> vertices) {
// The fourth type is meant to be "Geometry primitive". TODO: Find out what that is
static constexpr std::array<OpenGL::Primitives, 4> primTypes = {
OpenGL::Triangle, OpenGL::TriangleStrip, OpenGL::TriangleFan, OpenGL::Triangle
};
const auto primitiveTopology = primTypes[static_cast<usize>(primType)];
// TODO: We should implement a GL state tracker that tracks settings like scissor, blending, bound program, etc
// This way if we attempt to eg do multiple glEnable(GL_BLEND) calls in a row, it will say "Oh blending is already enabled"
// And not actually perform the very expensive driver call for it
OpenGL::disableScissor();
vbo.bind();
vao.bind();
triangleProgram.use();
// Adjust alpha test if necessary
const u32 alphaControl = regs[PICA::InternalRegs::AlphaTestConfig];
if (alphaControl != oldAlphaControl) {
oldAlphaControl = alphaControl;
glUniform1ui(alphaControlLoc, alphaControl);
}
setupBlending();
OpenGL::Framebuffer poop = getColourFBO();
poop.bind(OpenGL::DrawAndReadFramebuffer);
const u32 depthControl = regs[PICA::InternalRegs::DepthAndColorMask];
const bool depthEnable = depthControl & 1;
const bool depthWriteEnable = getBit<12>(depthControl);
const int depthFunc = getBits<4, 3>(depthControl);
const int colourMask = getBits<8, 4>(depthControl);
glColorMask(colourMask & 1, colourMask & 2, colourMask & 4, colourMask & 8);
static constexpr std::array<GLenum, 8> depthModes = {
GL_NEVER, GL_ALWAYS, GL_EQUAL, GL_NOTEQUAL, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL
};
const float depthScale = f24::fromRaw(regs[PICA::InternalRegs::DepthScale] & 0xffffff).toFloat32();
const float depthOffset = f24::fromRaw(regs[PICA::InternalRegs::DepthOffset] & 0xffffff).toFloat32();
const bool depthMapEnable = regs[PICA::InternalRegs::DepthmapEnable] & 1;
// Update depth uniforms
if (oldDepthScale != depthScale) {
oldDepthScale = depthScale;
glUniform1f(depthScaleLoc, depthScale);
}
if (oldDepthOffset != depthOffset) {
oldDepthOffset = depthOffset;
glUniform1f(depthOffsetLoc, depthOffset);
}
if (oldDepthmapEnable != depthMapEnable) {
oldDepthmapEnable = depthMapEnable;
glUniform1i(depthmapEnableLoc, depthMapEnable);
}
setupTextureEnvState();
bindTexturesToSlots();
// Upload PICA Registers as a single uniform. The shader needs access to the rasterizer registers (for depth, starting from index 0x47)
// The texturing and the fragment lighting registers. Therefore we upload them all in one go to avoid multiple slow uniform updates
glUniform1uiv(picaRegLoc, 0x200 - 0x47, &regs[0x47]);
if (gpu.lightingLUTDirty) {
updateLightingLUT();
}
// TODO: Actually use this
float viewportWidth = f24::fromRaw(regs[PICA::InternalRegs::ViewportWidth] & 0xffffff).toFloat32() * 2.0;
float viewportHeight = f24::fromRaw(regs[PICA::InternalRegs::ViewportHeight] & 0xffffff).toFloat32() * 2.0;
OpenGL::setViewport(viewportWidth, viewportHeight);
// Note: The code below must execute after we've bound the colour buffer & its framebuffer
// Because it attaches a depth texture to the aforementioned colour buffer
if (depthEnable) {
OpenGL::enableDepth();
glDepthFunc(depthModes[depthFunc]);
glDepthMask(depthWriteEnable ? GL_TRUE : GL_FALSE);
bindDepthBuffer();
} else {
if (depthWriteEnable) {
OpenGL::enableDepth();
glDepthFunc(GL_ALWAYS);
glDepthMask(GL_TRUE);
bindDepthBuffer();
} else {
OpenGL::disableDepth();
}
}
vbo.bufferVertsSub(vertices);
OpenGL::draw(primitiveTopology, vertices.size());
}
constexpr u32 topScreenBuffer = 0x1f000000;
constexpr u32 bottomScreenBuffer = 0x1f05dc00;
// Quick hack to display top screen for now
void Renderer::display() {
OpenGL::disableScissor();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
screenFramebuffer.bind(OpenGL::ReadFramebuffer);
glBlitFramebuffer(0, 0, 400, 480, 0, 0, 400, 480, GL_COLOR_BUFFER_BIT, GL_LINEAR);
}
void Renderer::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) {
return;
log("GPU: Clear buffer\nStart: %08X End: %08X\nValue: %08X Control: %08X\n", startAddress, endAddress, value, control);
const float r = float(getBits<24, 8>(value)) / 255.0;
const float g = float(getBits<16, 8>(value)) / 255.0;
const float b = float(getBits<8, 8>(value)) / 255.0;
const float a = float(value & 0xff) / 255.0;
if (startAddress == topScreenBuffer) {
log("GPU: Cleared top screen\n");
} else if (startAddress == bottomScreenBuffer) {
log("GPU: Tried to clear bottom screen\n");
return;
} else {
log("GPU: Clearing some unknown buffer\n");
}
OpenGL::setClearColor(r, g, b, a);
OpenGL::clearColor();
}
OpenGL::Framebuffer Renderer::getColourFBO() {
//We construct a colour buffer object and see if our cache has any matching colour buffers in it
// If not, we allocate a texture & FBO for our framebuffer and store it in the cache
ColourBuffer sampleBuffer(colourBufferLoc, colourBufferFormat, fbSize.x(), fbSize.y());
auto buffer = colourBufferCache.find(sampleBuffer);
if (buffer.has_value()) {
return buffer.value().get().fbo;
} else {
return colourBufferCache.add(sampleBuffer).fbo;
}
}
void Renderer::bindDepthBuffer() {
// Similar logic as the getColourFBO function
DepthBuffer sampleBuffer(depthBufferLoc, depthBufferFormat, fbSize.x(), fbSize.y());
auto buffer = depthBufferCache.find(sampleBuffer);
GLuint tex;
if (buffer.has_value()) {
tex = buffer.value().get().texture.m_handle;
} else {
tex = depthBufferCache.add(sampleBuffer).texture.m_handle;
}
if (PICA::DepthFmt::Depth24Stencil8 != depthBufferFormat) {
Helpers::panicDev("TODO: Should we remove stencil attachment?");
}
auto attachment = depthBufferFormat == PICA::DepthFmt::Depth24Stencil8 ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT;
glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, tex, 0);
}
OpenGL::Texture Renderer::getTexture(Texture& tex) {
// Similar logic as the getColourFBO/bindDepthBuffer functions
auto buffer = textureCache.find(tex);
if (buffer.has_value()) {
return buffer.value().get().texture;
} else {
const void* textureData = gpu.getPointerPhys<void*>(tex.location); // Get pointer to the texture data in 3DS memory
Texture& newTex = textureCache.add(tex);
newTex.decodeTexture(textureData);
return newTex.texture;
}
}
void Renderer::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) {
const u32 inputWidth = inputSize & 0xffff;
const u32 inputGap = inputSize >> 16;
const u32 outputWidth = outputSize & 0xffff;
const u32 outputGap = outputSize >> 16;
auto framebuffer = colourBufferCache.findFromAddress(inputAddr);
// If there's a framebuffer at this address, use it. Otherwise go back to our old hack and display framebuffer 0
// Displays are hard I really don't want to try implementing them because getting a fast solution is terrible
OpenGL::Texture& tex = framebuffer.has_value() ? framebuffer.value().get().texture : colourBufferCache[0].texture;
tex.bind();
screenFramebuffer.bind(OpenGL::DrawFramebuffer);
OpenGL::disableBlend();
OpenGL::disableDepth();
OpenGL::disableScissor();
displayProgram.use();
// Hack: Detect whether we are writing to the top or bottom screen by checking output gap and drawing to the proper part of the output texture
// We consider output gap == 320 to mean bottom, and anything else to mean top
if (outputGap == 320) {
OpenGL::setViewport(40, 0, 320, 240); // Bottom screen viewport
} else {
OpenGL::setViewport(0, 240, 400, 240); // Top screen viewport
}
dummyVAO.bind();
OpenGL::draw(OpenGL::TriangleStrip, 4); // Actually draw our 3DS screen
}