Files
goldeneye-pc-port/port/fast3d/gfx_pc.cpp
T
James Dansereau 157a8de59a fix(D172): stop gfx_lod_tile_offset collapsing non-LOD 2-texture combines
ROOT CAUSE FOUND (GE_D172 probe, user Silo firefight capture): particle
draws (G_CC_INTERFERENCE = TEXEL0*TEXEL1) configure tile 0 = IA8 smoke @
tmem 0 and tile 1 = RGBA16 fire @ tmem 0x188, but gfx_lod_tile_offset()
returns 0 unconditionally on the !gfx_detail_textures_enabled path (a D107
blurry-ceiling fix), so texunit 1 samples the smoke texture too. smoke *
smoke -> the magenta/cyan blood/spark colour.

Probe evidence, 40/40 particle tris:
  cfg1[tile=1 tmem=392 fmt=RGBA siz=16b]  SAMPLED1=tile0(tmem=0 fmt=IA)

Fix: only fold texunit i to the base tile when LOD is actually active
(rdp.tex_lod). D107's mip case (tex_lod=1) is unchanged; genuine non-LOD
multitexture now gets tile i. bunker1 verify PASS (worst_cell 11.3, the
D107 repro level, no regression).

Needs an in-game eyeball: blood/sparks should read dark red, not magenta.
2026-09-08 23:30:45 -04:00

3295 lines
130 KiB
C++

#define NOMINMAX
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <cstdio>
#include <map>
#include <set>
#include <unordered_map>
#include <vector>
#include <list>
#include <stack>
#include <string>
#include <iostream>
#include <memory>
#include <limits>
#ifndef _LANGUAGE_C
#define _LANGUAGE_C
#endif
#include <PR/gbi.h>
#include "gbiex.h" /* GE's G_TRI4 + PD extension opcodes (see header) */
#include "platform.h"
#include "gfx_pc.h"
#include "gfx_cc.h"
#include "gfx_window_manager_api.h"
#include "gfx_rendering_api.h"
#include "gfx_screen_config.h"
uintptr_t gfxFramebuffer;
#define ALIGN(x, a) (((x) + (a - 1)) & ~(a - 1))
#define SUPPORT_CHECK(x) assert(x)
// SCALE_M_N: upscale/downscale M-bit integer to N-bit
#define SCALE_5_8(VAL_) (((VAL_)*0xFF) / 0x1F)
#define SCALE_8_5(VAL_) ((((VAL_) + 4) * 0x1F) / 0xFF)
#define SCALE_4_8(VAL_) ((VAL_)*0x11)
#define SCALE_8_4(VAL_) ((VAL_) / 0x11)
#define SCALE_3_8(VAL_) ((VAL_)*0x24)
#define SCALE_8_3(VAL_) ((VAL_) / 0x24)
// SCREEN_WIDTH and SCREEN_HEIGHT are defined in the headerfile
#define HALF_SCREEN_WIDTH (SCREEN_WIDTH / 2.f)
#define HALF_SCREEN_HEIGHT (SCREEN_HEIGHT / 2.f)
#define RATIO_X (gfx_current_dimensions.width / (float)SCREEN_WIDTH)
#define RATIO_Y (gfx_current_dimensions.height / (float)SCREEN_HEIGHT)
#define MAX_BUFFERED 256
#define MAX_LIGHTS 4
#define MAX_VERTICES 128
#define MAX_VERTEX_COLORS 64
#define TEXTURE_CACHE_MAX_SIZE 1024
#define C0(pos, width) ((cmd->words.w0 >> (pos)) & ((1U << width) - 1))
#define C1(pos, width) ((cmd->words.w1 >> (pos)) & ((1U << width) - 1))
struct RGBA {
uint8_t r, g, b, a;
};
struct NormalColor {
union {
struct { uint8_t r, g, b, a; };
struct { int8_t x, y, z, w; };
};
};
struct LoadedVertex {
float x, y, z, w;
float u, v;
struct RGBA color;
uint8_t fog;
uint8_t clip_rej;
};
static struct {
TextureCacheMap map;
std::list<TextureCacheMapIter> lru;
std::vector<uint32_t> free_texture_ids;
} gfx_texture_cache;
struct ColorCombiner {
uint64_t shader_id0;
uint32_t shader_id1;
bool used_textures[2];
struct ShaderProgram* prg[16];
uint8_t shader_input_mapping[2][7];
};
static std::map<ColorCombinerKey, struct ColorCombiner> color_combiner_pool;
static std::map<ColorCombinerKey, struct ColorCombiner>::iterator prev_combiner = color_combiner_pool.end();
static uint8_t* tex_upload_buffer = nullptr;
static struct RSP {
float modelview_matrix_stack[11][4][4];
uint8_t modelview_matrix_stack_size;
float MP_matrix[4][4];
float P_matrix[4][4];
Light_t lookat[2];
bool lookat_enabled;
Light_t current_lights[MAX_LIGHTS + 1];
float current_lights_coeffs[MAX_LIGHTS][3];
float current_lookat_coeffs[2][3]; // lookat_x, lookat_y
uint8_t current_num_lights; // includes ambient light
bool lights_changed;
uint32_t geometry_mode;
int16_t fog_mul, fog_offset;
uint32_t extra_geometry_mode;
uint32_t aspect_mode;
float aspect_ofs;
float aspect_scale;
struct {
// U0.16
uint16_t s, t;
} texture_scaling_factor;
struct LoadedVertex loaded_vertices[MAX_VERTICES + 4];
const struct NormalColor *vertex_colors; //[MAX_VERTEX_COLORS];
} rsp;
struct RawTexMetadata {
uint16_t width, height;
float h_byte_scale = 1, v_pixel_scale = 1;
};
struct LoadedTexture {
const uint8_t* addr;
uint32_t orig_size_bytes;
uint32_t full_size_bytes; // full_image_line_size_bytes * height
uint32_t size_bytes; // line_size_bytes * height
uint32_t full_image_line_size_bytes;
uint32_t line_size_bytes;
uint32_t tex_flags;
struct RawTexMetadata raw_tex_metadata;
};
static struct RDP {
uint16_t palette[256];
const uint8_t* palette_addrs[2];
uint32_t palette_fmt;
struct {
const uint8_t* addr;
uint8_t siz;
uint32_t width;
uint32_t tex_flags;
struct RawTexMetadata raw_tex_metadata;
} texture_to_load;
struct {
uint8_t fmt;
uint8_t siz;
uint8_t cms, cmt;
uint8_t masks, maskt; /* RC3: N64 tile mask; wrap period = 1<<mask */
uint8_t shifts, shiftt;
uint16_t uls, ult, lrs, lrt; // U10.2
uint16_t width, height; // in texels
uint16_t tmem; // 0-511, in 64-bit word units
uint32_t line_size_bytes;
uint8_t palette;
} texture_tile[8];
LoadedTexture loaded_texture[512]; // for each tmem location
bool textures_changed[2];
uint8_t first_tile_index;
uint8_t tex_min_lod;
uint8_t tex_max_lod;
uint32_t other_mode_l, other_mode_h;
uint64_t combine_mode;
bool grayscale;
bool tex_lod;
bool tex_detail;
uint8_t prim_lod_fraction;
struct RGBA env_color, prim_color, fog_color, fill_color, grayscale_color;
struct XYWidthHeight viewport, scissor;
bool viewport_or_scissor_changed;
void* z_buf_address;
void* color_image_address;
int16_t subpixel_ofs_x;
int16_t subpixel_ofs_y;
} rdp;
static struct RenderingState {
uint8_t depth_mode;
bool alpha_blend;
bool modulate;
struct XYWidthHeight viewport, scissor;
struct ShaderProgram* shader_program;
TextureCacheNode* textures[SHADER_MAX_TEXTURES];
} rendering_state;
struct GfxDimensions gfx_current_window_dimensions;
int32_t gfx_current_window_position_x;
int32_t gfx_current_window_position_y;
struct GfxDimensions gfx_current_dimensions;
static struct GfxDimensions gfx_prev_dimensions;
struct XYWidthHeight gfx_current_game_window_viewport;
struct XYWidthHeight gfx_current_native_viewport;
float gfx_current_native_aspect = 4.f / 3.f;
bool gfx_framebuffers_enabled = true;
bool gfx_detail_textures_enabled = true;
static bool game_renders_to_framebuffer;
static int game_framebuffer;
static int game_framebuffer_msaa_resolved;
uint32_t gfx_msaa_level = 1;
static bool dropped_frame;
static float buf_vbo[MAX_BUFFERED * (32 * 3)]; // 3 vertices in a triangle and 32 floats per vtx
static size_t buf_vbo_len;
static size_t buf_vbo_num_tris;
static struct GfxWindowManagerAPI* gfx_wapi;
static struct GfxRenderingAPI* gfx_rapi;
static uintptr_t segmentPointers[16];
struct FBInfo {
uint32_t orig_width, orig_height;
uint32_t applied_width, applied_height;
bool upscale, autoresize;
};
static bool fbActive = 0;
static std::map<int, FBInfo>::iterator active_fb;
static std::map<int, FBInfo> framebuffers;
static constexpr float clampf(const float x, const float min, const float max) {
return (x < min) ? min : (x > max) ? max : x;
}
static void gfx_flush(void) {
if (buf_vbo_len > 0) {
gfx_rapi->draw_triangles(buf_vbo, buf_vbo_len, buf_vbo_num_tris);
buf_vbo_len = 0;
buf_vbo_num_tris = 0;
}
}
static struct ShaderProgram* gfx_lookup_or_create_shader_program(uint64_t shader_id0, uint32_t shader_id1) {
struct ShaderProgram* prg = gfx_rapi->lookup_shader(shader_id0, shader_id1);
if (prg == NULL) {
gfx_rapi->unload_shader(rendering_state.shader_program);
prg = gfx_rapi->create_and_load_new_shader(shader_id0, shader_id1);
rendering_state.shader_program = prg;
}
return prg;
}
static const char* ccmux_to_string(uint32_t ccmux) {
static const char* const tbl[] = {
"G_CCMUX_COMBINED",
"G_CCMUX_TEXEL0",
"G_CCMUX_TEXEL1",
"G_CCMUX_PRIMITIVE",
"G_CCMUX_SHADE",
"G_CCMUX_ENVIRONMENT",
"G_CCMUX_1",
"G_CCMUX_COMBINED_ALPHA",
"G_CCMUX_TEXEL0_ALPHA",
"G_CCMUX_TEXEL1_ALPHA",
"G_CCMUX_PRIMITIVE_ALPHA",
"G_CCMUX_SHADE_ALPHA",
"G_CCMUX_ENV_ALPHA",
"G_CCMUX_LOD_FRACTION",
"G_CCMUX_PRIM_LOD_FRAC",
"G_CCMUX_K5",
};
if (ccmux > 15) {
return "G_CCMUX_0";
} else {
return tbl[ccmux];
}
}
static const char* acmux_to_string(uint32_t acmux) {
static const char* const tbl[] = {
"G_ACMUX_COMBINED or G_ACMUX_LOD_FRACTION",
"G_ACMUX_TEXEL0",
"G_ACMUX_TEXEL1",
"G_ACMUX_PRIMITIVE",
"G_ACMUX_SHADE",
"G_ACMUX_ENVIRONMENT",
"G_ACMUX_1 or G_ACMUX_PRIM_LOD_FRAC",
"G_ACMUX_0",
};
return tbl[acmux];
}
static void gfx_generate_cc(struct ColorCombiner* comb, const ColorCombinerKey& key) {
bool is_2cyc = (key.options & (uint64_t)SHADER_OPT_2CYC) != 0;
uint8_t c[2][2][4] = { { { 0 } } };
uint64_t shader_id0 = 0;
uint32_t shader_id1 = key.options;
uint8_t shader_input_mapping[2][7] = { { 0 } };
bool used_textures[2] = { false, false };
for (int i = 0; i < 2 && (i == 0 || is_2cyc); i++) {
uint32_t rgb_a = (key.combine_mode >> (i * 28)) & 0xf;
uint32_t rgb_b = (key.combine_mode >> (i * 28 + 4)) & 0xf;
uint32_t rgb_c = (key.combine_mode >> (i * 28 + 8)) & 0x1f;
uint32_t rgb_d = (key.combine_mode >> (i * 28 + 13)) & 7;
uint32_t alpha_a = (key.combine_mode >> (i * 28 + 16)) & 7;
uint32_t alpha_b = (key.combine_mode >> (i * 28 + 16 + 3)) & 7;
uint32_t alpha_c = (key.combine_mode >> (i * 28 + 16 + 6)) & 7;
uint32_t alpha_d = (key.combine_mode >> (i * 28 + 16 + 9)) & 7;
if (rgb_a >= 8) {
rgb_a = G_CCMUX_0;
}
if (rgb_b >= 8) {
rgb_b = G_CCMUX_0;
}
if (rgb_c >= 16) {
rgb_c = G_CCMUX_0;
}
if (rgb_d == 7) {
rgb_d = G_CCMUX_0;
}
if (rgb_a == rgb_b || rgb_c == G_CCMUX_0) {
// Normalize
rgb_a = G_CCMUX_0;
rgb_b = G_CCMUX_0;
rgb_c = G_CCMUX_0;
}
if (alpha_a == alpha_b || alpha_c == G_ACMUX_0) {
// Normalize
alpha_a = G_ACMUX_0;
alpha_b = G_ACMUX_0;
alpha_c = G_ACMUX_0;
}
if (i == 1) {
if (rgb_a != G_CCMUX_COMBINED && rgb_b != G_CCMUX_COMBINED && rgb_c != G_CCMUX_COMBINED &&
rgb_d != G_CCMUX_COMBINED) {
// First cycle RGB not used, so clear it away
c[0][0][0] = c[0][0][1] = c[0][0][2] = c[0][0][3] = G_CCMUX_0;
}
if (rgb_c != G_CCMUX_COMBINED_ALPHA && alpha_a != G_ACMUX_COMBINED && alpha_b != G_ACMUX_COMBINED &&
alpha_d != G_ACMUX_COMBINED) {
// First cycle ALPHA not used, so clear it away
c[0][1][0] = c[0][1][1] = c[0][1][2] = c[0][1][3] = G_ACMUX_0;
}
}
c[i][0][0] = rgb_a;
c[i][0][1] = rgb_b;
c[i][0][2] = rgb_c;
c[i][0][3] = rgb_d;
c[i][1][0] = alpha_a;
c[i][1][1] = alpha_b;
c[i][1][2] = alpha_c;
c[i][1][3] = alpha_d;
}
if (!is_2cyc) {
for (int i = 0; i < 2; i++) {
for (int k = 0; k < 4; k++) {
c[1][i][k] = i == 0 ? G_CCMUX_0 : G_ACMUX_0;
}
}
}
{
uint8_t input_number[32] = { 0 };
int next_input_number = SHADER_INPUT_1;
for (int i = 0; i < 2 && (i == 0 || is_2cyc); i++) {
for (int j = 0; j < 4; j++) {
uint32_t val = 0;
switch (c[i][0][j]) {
case G_CCMUX_0:
val = SHADER_0;
break;
case G_CCMUX_1:
val = SHADER_1;
break;
case G_CCMUX_TEXEL0:
val = SHADER_TEXEL0;
used_textures[0] = true;
break;
case G_CCMUX_TEXEL1:
val = SHADER_TEXEL1;
used_textures[1] = true;
break;
case G_CCMUX_TEXEL0_ALPHA:
val = SHADER_TEXEL0A;
used_textures[0] = true;
break;
case G_CCMUX_TEXEL1_ALPHA:
val = SHADER_TEXEL1A;
used_textures[1] = true;
break;
case G_CCMUX_NOISE:
val = SHADER_NOISE;
break;
case G_CCMUX_PRIMITIVE:
case G_CCMUX_PRIMITIVE_ALPHA:
case G_CCMUX_PRIM_LOD_FRAC:
case G_CCMUX_SHADE:
case G_CCMUX_SHADE_ALPHA:
case G_CCMUX_ENVIRONMENT:
case G_CCMUX_ENV_ALPHA:
case G_CCMUX_LOD_FRACTION:
if (input_number[c[i][0][j]] == 0) {
shader_input_mapping[0][next_input_number - 1] = c[i][0][j];
input_number[c[i][0][j]] = next_input_number++;
}
val = input_number[c[i][0][j]];
break;
case G_CCMUX_COMBINED:
val = SHADER_COMBINED;
break;
default:
sysLogPrintf(LOG_WARNING, "Unsupported ccmux: %d", c[i][0][j]);
break;
}
shader_id0 |= (uint64_t)val << (i * 32 + j * 4);
}
}
}
{
uint8_t input_number[16] = { 0 };
int next_input_number = SHADER_INPUT_1;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 4; j++) {
uint32_t val = 0;
switch (c[i][1][j]) {
case G_ACMUX_0:
val = SHADER_0;
break;
case G_ACMUX_TEXEL0:
val = SHADER_TEXEL0;
used_textures[0] = true;
break;
case G_ACMUX_TEXEL1:
val = SHADER_TEXEL1;
used_textures[1] = true;
break;
case G_ACMUX_LOD_FRACTION:
// case G_ACMUX_COMBINED: same numerical value
if (j != 2) {
val = SHADER_COMBINED;
break;
}
c[i][1][j] = G_CCMUX_LOD_FRACTION;
[[fallthrough]]; // for G_ACMUX_LOD_FRACTION
case G_ACMUX_1:
// case G_ACMUX_PRIM_LOD_FRAC: same numerical value
if (j != 2) {
val = SHADER_1;
break;
}
[[fallthrough]]; // for G_ACMUX_PRIM_LOD_FRAC
case G_ACMUX_PRIMITIVE:
case G_ACMUX_SHADE:
case G_ACMUX_ENVIRONMENT:
if (input_number[c[i][1][j]] == 0) {
shader_input_mapping[1][next_input_number - 1] = c[i][1][j];
input_number[c[i][1][j]] = next_input_number++;
}
val = input_number[c[i][1][j]];
break;
}
shader_id0 |= (uint64_t)val << (i * 32 + 16 + j * 4);
}
}
}
comb->shader_id0 = shader_id0;
comb->shader_id1 = shader_id1;
comb->used_textures[0] = used_textures[0];
comb->used_textures[1] = used_textures[1];
// comb->prg = gfx_lookup_or_create_shader_program(shader_id0, shader_id1);
memcpy(comb->shader_input_mapping, shader_input_mapping, sizeof(shader_input_mapping));
}
static struct ColorCombiner* gfx_lookup_or_create_color_combiner(const ColorCombinerKey& key) {
if (prev_combiner != color_combiner_pool.end() && prev_combiner->first == key) {
return &prev_combiner->second;
}
prev_combiner = color_combiner_pool.find(key);
if (prev_combiner != color_combiner_pool.end()) {
return &prev_combiner->second;
}
gfx_flush();
prev_combiner = color_combiner_pool.insert(std::make_pair(key, ColorCombiner())).first;
gfx_generate_cc(&prev_combiner->second, key);
return &prev_combiner->second;
}
void gfx_texture_cache_clear() {
gfx_flush();
for (const auto& entry : gfx_texture_cache.map) {
gfx_texture_cache.free_texture_ids.push_back(entry.second.texture_id);
}
gfx_texture_cache.map.clear();
gfx_texture_cache.lru.clear();
rdp.textures_changed[0] = rdp.textures_changed[1] = true;
memset(rendering_state.textures, 0, sizeof(rendering_state.textures));
}
static bool gfx_texture_cache_lookup(int i, const TextureCacheKey& key) {
TextureCacheMap::iterator it = gfx_texture_cache.map.find(key);
TextureCacheNode** n = &rendering_state.textures[i];
if (it != gfx_texture_cache.map.end()) {
gfx_rapi->select_texture(i, it->second.texture_id, it->second.linear_filter);
*n = &*it;
gfx_texture_cache.lru.splice(gfx_texture_cache.lru.end(), gfx_texture_cache.lru,
it->second.lru_location); // move to back
return true;
}
if (gfx_texture_cache.map.size() >= TEXTURE_CACHE_MAX_SIZE) {
// Remove the texture that was least recently used
it = gfx_texture_cache.lru.front().it;
gfx_texture_cache.free_texture_ids.push_back(it->second.texture_id);
gfx_texture_cache.map.erase(it);
gfx_texture_cache.lru.pop_front();
}
uint32_t texture_id;
if (!gfx_texture_cache.free_texture_ids.empty()) {
texture_id = gfx_texture_cache.free_texture_ids.back();
gfx_texture_cache.free_texture_ids.pop_back();
} else {
texture_id = gfx_rapi->new_texture();
}
it = gfx_texture_cache.map.insert(std::make_pair(key, TextureCacheValue())).first;
TextureCacheNode* node = &*it;
node->second.texture_id = texture_id;
node->second.lru_location = gfx_texture_cache.lru.insert(gfx_texture_cache.lru.end(), { it });
gfx_rapi->select_texture(i, texture_id, false);
gfx_rapi->set_sampler_parameters(i, false, 0, 0, rdp.tex_lod);
*n = node;
return false;
}
void gfx_texture_cache_delete(const uint8_t* orig_addr) {
gfx_flush();
for (int i = 0; i < 2; ++i) {
if (rendering_state.textures[i] && rendering_state.textures[i]->first.texture_addr == orig_addr) {
rdp.textures_changed[i] = true;
rendering_state.textures[i] = nullptr;
}
}
while (gfx_texture_cache.map.bucket_count() > 0) {
TextureCacheKey key = { orig_addr, { 0 }, 0, 0, 0 }; // bucket index only depends on the address
size_t bucket = gfx_texture_cache.map.bucket(key);
bool again = false;
for (auto it = gfx_texture_cache.map.begin(bucket); it != gfx_texture_cache.map.end(bucket); ++it) {
if (it->first.texture_addr == orig_addr) {
gfx_texture_cache.lru.erase(it->second.lru_location);
gfx_texture_cache.free_texture_ids.push_back(it->second.texture_id);
gfx_texture_cache.map.erase(it->first);
again = true;
break;
}
}
if (!again) {
break;
}
}
}
void gfx_texture_cache_delete_range(const uint8_t* start, const uint8_t* end) {
gfx_flush();
for (int i = 0; i < 2; ++i) {
if (rendering_state.textures[i]
&& rendering_state.textures[i]->first.texture_addr >= start
&& rendering_state.textures[i]->first.texture_addr < end) {
rdp.textures_changed[i] = true;
rendering_state.textures[i] = nullptr;
}
}
for (auto it = gfx_texture_cache.map.begin(); it != gfx_texture_cache.map.end(); ) {
if (it->first.texture_addr >= start && it->first.texture_addr < end) {
gfx_texture_cache.lru.erase(it->second.lru_location);
gfx_texture_cache.free_texture_ids.push_back(it->second.texture_id);
it = gfx_texture_cache.map.erase(it);
} else {
++it;
}
}
}
// D71 (docs/internals.md): texture sources arrive in two byte conventions.
// Raw N64 big-endian byte streams: ROM cart map (0x10xxxxxx), model-sidecar
// blobs (cart extension 0x10Cxxxxx), KSEG0 mirror (0x80xxxxxx) and V1
// dram/BSS/heap buffers (0x70xxxxxx, e.g. tex.c texture pool, rle_expand_8bit
// output). C-compiled u32 arrays in the exe image (.data/.rodata,
// 0x140xxxxxx on MinGW x64) instead store each N64 texel pair as a
// little-endian u32 — e.g. the rarewarelogo.c RGBA16 images — so the N64 byte
// order is recovered by bswap32 of every u32. Without this, the logo's gold
// texels (0xED0F...) decode from the swapped pairs (0x4FCC/0xCC4F) as bright
// green/pink — the garbled Rareware-logo pixels.
static bool gfx_tex_source_is_c_array(const uint8_t* addr) {
const uintptr_t a = (uintptr_t)addr;
if (a >= 0x10000000u && a < 0x20000000u) return false; // cart map + sidecar
if (a >= 0x70000000u && a < 0x90000000u) return false; // V1 dram + KSEG0 mirror
return true; // exe image: C-compiled array
}
static std::map<const uint8_t*, std::vector<uint8_t> > s_c_array_tex_norms;
// Returns a pointer to the source in N64 byte order (the original pointer for
// raw-stream sources, a stable per-source bswapped copy for C arrays).
// extent is the full image size incl. padded rows (ci8 reads up to it).
static const uint8_t* gfx_tex_normalize_source(const uint8_t* addr, uint32_t extent) {
if (!gfx_tex_source_is_c_array(addr)) return addr;
auto it = s_c_array_tex_norms.find(addr);
if (it != s_c_array_tex_norms.end()) return it->second.data();
const uint32_t n = (extent + 3u) & ~3u;
std::vector<uint8_t> buf(n);
const uint32_t* src = (const uint32_t*)addr;
uint32_t* dst = (uint32_t*)buf.data();
for (uint32_t i = 0; i < n / 4; i++)
dst[i] = PD_BE32(src[i]);
if (getenv("GE_D71LOG"))
fprintf(stderr, "[D71] normalized C-array texture source %p (%u bytes)\n",
(const void*)addr, extent);
return s_c_array_tex_norms.emplace(addr, std::move(buf)).first->second.data();
}
static void import_texture_rgba16(int tile, const LoadedTexture& loaded_texture, bool gen_mipmaps) {
const uint8_t* addr = loaded_texture.addr;
const uint32_t size_bytes = loaded_texture.size_bytes;
const uint32_t full_image_line_size_bytes =
loaded_texture.full_image_line_size_bytes;
const uint32_t line_size_bytes = loaded_texture.line_size_bytes;
// SUPPORT_CHECK(full_image_line_size_bytes == line_size_bytes);
// TODO: this trips in some places with a garbage size in full_image_line_size_bytes
// probably wherever framebuffer effects are used
uint8_t *dest = tex_upload_buffer;
for (uint32_t i = 0; i < size_bytes / 2; i++, dest += 4) {
const uint16_t col16 = (addr[2 * i] << 8) | addr[2 * i + 1];
const uint8_t a = col16 & 1;
const uint8_t r = col16 >> 11;
const uint8_t g = (col16 >> 6) & 0x1f;
const uint8_t b = (col16 >> 1) & 0x1f;
dest[0] = SCALE_5_8(r);
dest[1] = SCALE_5_8(g);
dest[2] = SCALE_5_8(b);
dest[3] = a ? 255 : 0;
}
const uint32_t width = rdp.texture_tile[tile].line_size_bytes / 2;
const uint32_t height = size_bytes / rdp.texture_tile[tile].line_size_bytes;
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
// DumpTexture(loaded_texture.otr_path, rgba32_buf, width, height);
}
static void import_texture_rgba32(int tile, const LoadedTexture& loaded_texture, bool gen_mipmaps) {
const RawTexMetadata* metadata = &loaded_texture.raw_tex_metadata;
const uint8_t* addr = loaded_texture.addr;
const uint32_t size_bytes = loaded_texture.size_bytes;
const uint32_t full_image_line_size_bytes =
loaded_texture.full_image_line_size_bytes;
const uint32_t line_size_bytes = loaded_texture.line_size_bytes;
SUPPORT_CHECK(full_image_line_size_bytes == line_size_bytes);
uint32_t *dest = (uint32_t *)tex_upload_buffer;
const uint32_t *src = (const uint32_t *)addr;
for (uint32_t i = 0; i < size_bytes; i += 4, ++dest, ++src) {
*dest = PD_BE32(*src);
}
const uint32_t width = rdp.texture_tile[tile].line_size_bytes / 2;
const uint32_t height = (size_bytes / 2) / rdp.texture_tile[tile].line_size_bytes;
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
// DumpTexture(loaded_texture.otr_path, addr, width, height);
}
static void import_texture_ia4(int tile, const LoadedTexture& loaded_texture, bool gen_mipmaps) {
const RawTexMetadata* metadata = &loaded_texture.raw_tex_metadata;
const uint8_t* addr = loaded_texture.addr;
const uint32_t size_bytes = loaded_texture.size_bytes;
const uint32_t full_image_line_size_bytes =
loaded_texture.full_image_line_size_bytes;
const uint32_t line_size_bytes = loaded_texture.line_size_bytes;
SUPPORT_CHECK(full_image_line_size_bytes == line_size_bytes);
uint8_t *dest = tex_upload_buffer;
for (uint32_t i = 0; i < size_bytes * 2; i++, dest += 4) {
const uint8_t byte = addr[i / 2];
const uint8_t part = (byte >> (4 - (i % 2) * 4)) & 0xf;
const uint8_t intensity = part >> 1;
const uint8_t alpha = part & 1;
const uint8_t c = SCALE_3_8(intensity);
dest[0] = c;
dest[1] = c;
dest[2] = c;
dest[3] = alpha ? 255 : 0;
}
const uint32_t width = rdp.texture_tile[tile].line_size_bytes * 2;
const uint32_t height = size_bytes / rdp.texture_tile[tile].line_size_bytes;
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
// DumpTexture(loaded_texture.otr_path, rgba32_buf, width, height);
}
static void import_texture_ia8(int tile, const LoadedTexture& loaded_texture, bool gen_mipmaps) {
const RawTexMetadata* metadata = &loaded_texture.raw_tex_metadata;
const uint8_t* addr = loaded_texture.addr;
const uint32_t size_bytes = loaded_texture.size_bytes;
const uint32_t full_image_line_size_bytes =
loaded_texture.full_image_line_size_bytes;
const uint32_t line_size_bytes = loaded_texture.line_size_bytes;
SUPPORT_CHECK(full_image_line_size_bytes == line_size_bytes);
uint8_t *dest = tex_upload_buffer;
for (uint32_t i = 0; i < size_bytes; i++, dest += 4) {
const uint8_t intensity = SCALE_4_8(addr[i] >> 4);
const uint8_t alpha = SCALE_4_8(addr[i] & 0xf);
dest[0] = intensity;
dest[1] = intensity;
dest[2] = intensity;
dest[3] = alpha;
}
const uint32_t width = rdp.texture_tile[tile].line_size_bytes;
const uint32_t height = size_bytes / rdp.texture_tile[tile].line_size_bytes;
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
// DumpTexture(loaded_texture.otr_path, rgba32_buf, width, height);
}
static void import_texture_ia16(int tile, const LoadedTexture& loaded_texture, bool gen_mipmaps) {
const RawTexMetadata* metadata = &loaded_texture.raw_tex_metadata;
const uint8_t* addr = loaded_texture.addr;
const uint32_t size_bytes = loaded_texture.size_bytes;
const uint32_t full_image_line_size_bytes =
loaded_texture.full_image_line_size_bytes;
const uint32_t line_size_bytes = loaded_texture.line_size_bytes;
SUPPORT_CHECK(full_image_line_size_bytes == line_size_bytes);
uint8_t *dest = tex_upload_buffer;
for (uint32_t i = 0; i < size_bytes / 2; i++, dest += 4) {
const uint8_t intensity = addr[2 * i];
const uint8_t alpha = addr[2 * i + 1];
dest[0] = intensity;
dest[1] = intensity;
dest[2] = intensity;
dest[3] = alpha;
}
const uint32_t width = rdp.texture_tile[tile].line_size_bytes / 2;
const uint32_t height = size_bytes / rdp.texture_tile[tile].line_size_bytes;
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
// DumpTexture(loaded_texture.otr_path, rgba32_buf, width, height);
}
static void import_texture_i4(int tile, const LoadedTexture& loaded_texture, bool gen_mipmaps) {
const RawTexMetadata* metadata = &loaded_texture.raw_tex_metadata;
const uint8_t* addr = loaded_texture.addr;
const uint32_t size_bytes = loaded_texture.size_bytes;
const uint32_t full_image_line_size_bytes =
loaded_texture.full_image_line_size_bytes;
const uint32_t line_size_bytes = loaded_texture.line_size_bytes;
SUPPORT_CHECK(full_image_line_size_bytes == line_size_bytes);
uint8_t *dest = tex_upload_buffer;
for (uint32_t i = 0; i < size_bytes * 2; i++, dest += 4) {
const uint8_t byte = addr[i / 2];
const uint8_t part = (byte >> (4 - (i % 2) * 4)) & 0xf;
const uint8_t intensity = SCALE_4_8(part);
dest[0] = intensity;
dest[1] = intensity;
dest[2] = intensity;
dest[3] = intensity;
}
const uint32_t width = rdp.texture_tile[tile].line_size_bytes * 2;
const uint32_t height = size_bytes / rdp.texture_tile[tile].line_size_bytes;
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
// DumpTexture(loaded_texture.otr_path, rgba32_buf, width, height);
}
static void import_texture_i8(int tile, const LoadedTexture& loaded_texture, bool gen_mipmaps) {
const RawTexMetadata* metadata = &loaded_texture.raw_tex_metadata;
const uint8_t* addr = loaded_texture.addr;
const uint32_t size_bytes = loaded_texture.size_bytes;
uint32_t full_image_line_size_bytes =
loaded_texture.full_image_line_size_bytes;
const uint32_t line_size_bytes = loaded_texture.line_size_bytes;
SUPPORT_CHECK(full_image_line_size_bytes == line_size_bytes);
uint8_t *dest = tex_upload_buffer;
for (uint32_t i = 0; i < size_bytes; i++, dest += 4) {
const uint8_t intensity = addr[i];
dest[0] = intensity;
dest[1] = intensity;
dest[2] = intensity;
dest[3] = intensity;
}
const uint32_t width = rdp.texture_tile[tile].line_size_bytes;
const uint32_t height = size_bytes / rdp.texture_tile[tile].line_size_bytes;
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
// DumpTexture(loaded_texture.otr_path, rgba32_buf, width, height);
}
static inline void palette_to_rgba32(const uint16_t palentry, uint8_t *rgba32_buf) {
if (rdp.palette_fmt == G_TT_IA16) {
const uint8_t intensity = (palentry & 0xff);
const uint8_t alpha = palentry >> 8;
rgba32_buf[0] = intensity;
rgba32_buf[1] = intensity;
rgba32_buf[2] = intensity;
rgba32_buf[3] = alpha;
} else {
// assume G_TT_RGBA16
const uint8_t a = palentry & 1;
const uint8_t r = palentry >> 11;
const uint8_t g = (palentry >> 6) & 0x1f;
const uint8_t b = (palentry >> 1) & 0x1f;
rgba32_buf[0] = SCALE_5_8(r);
rgba32_buf[1] = SCALE_5_8(g);
rgba32_buf[2] = SCALE_5_8(b);
rgba32_buf[3] = a ? 255 : 0;
}
}
static void import_texture_ci4(int tile, const LoadedTexture& loaded_texture, bool gen_mipmaps) {
const RawTexMetadata* metadata = &loaded_texture.raw_tex_metadata;
const uint8_t* addr = loaded_texture.addr;
const uint32_t size_bytes = loaded_texture.size_bytes;
const uint32_t full_image_line_size_bytes =
loaded_texture.full_image_line_size_bytes;
const uint32_t line_size_bytes = loaded_texture.line_size_bytes;
const uint32_t pal_idx = rdp.texture_tile[tile].palette; // 0-15
const uint16_t* palette = (const uint16_t *)(rdp.palette + pal_idx * 16); // 16 pixel entries, 16 bits each
SUPPORT_CHECK(full_image_line_size_bytes == line_size_bytes);
for (uint32_t i = 0; i < size_bytes * 2; i++) {
const uint8_t byte = addr[i / 2];
const uint8_t idx = (byte >> (4 - (i % 2) * 4)) & 0xf;
palette_to_rgba32(palette[idx], tex_upload_buffer +4 * i);
}
uint32_t result_line_size = rdp.texture_tile[tile].line_size_bytes;
if (metadata->h_byte_scale != 1) {
result_line_size *= metadata->h_byte_scale;
}
const uint32_t width = result_line_size * 2;
const uint32_t height = size_bytes / result_line_size;
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
}
static void import_texture_ci8(int tile, const LoadedTexture& loaded_texture, bool gen_mipmaps) {
const RawTexMetadata* metadata = &loaded_texture.raw_tex_metadata;
const uint8_t* addr = loaded_texture.addr;
const uint32_t size_bytes = loaded_texture.size_bytes;
const uint32_t full_image_line_size_bytes =
loaded_texture.full_image_line_size_bytes;
const uint32_t line_size_bytes = loaded_texture.line_size_bytes;
for (uint32_t i = 0, j = 0; i < size_bytes; j += full_image_line_size_bytes - line_size_bytes) {
for (uint32_t k = 0; k < line_size_bytes; i++, k++, j++) {
const uint8_t idx = addr[j];
palette_to_rgba32(rdp.palette[idx], tex_upload_buffer + 4 * i);
}
}
uint32_t result_line_size = rdp.texture_tile[tile].line_size_bytes;
if (metadata->h_byte_scale != 1) {
result_line_size *= metadata->h_byte_scale;
}
const uint32_t width = result_line_size;
const uint32_t height = size_bytes / result_line_size;
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
}
/* RC2 mip-contamination clamp (see import_texture). Default on;
* Video.FixMipTextures = 0 restores the raw over-tall upload. */
bool g_fix_mip_textures = true;
/* D74 sub-tile UV pre-wrap (see gfx_sp_tri). Opt-in (never-run path); default
* off. Video.WrapFix = 1. */
bool g_wrap_fix = false;
/* D183 source-pitch de-stride (see import_texture). Default on;
* GE_TEXPITCH=0 restores the old flat read for A/B. */
static bool gfx_tex_pitch_fix(void) {
static int cached = -1;
if (cached < 0) {
const char* e = getenv("GE_TEXPITCH");
cached = (e && e[0] == '0') ? 0 : 1;
}
return cached != 0;
}
static void import_texture(int i, int tile, bool importReplacement) {
LoadedTexture& loaded_texture = rdp.loaded_texture[rdp.texture_tile[tile].tmem];
const uint8_t fmt = rdp.texture_tile[tile].fmt;
const uint8_t siz = rdp.texture_tile[tile].siz;
const uint32_t tex_flags = loaded_texture.tex_flags;
const uint8_t palette_index = rdp.texture_tile[tile].palette;
// D74: only fall back when the tmem slot was never written by a load
// command. The old `rdp.tex_lod && tile >= first+detail` branch also
// overwrote valid gDPLoadBlock data with line*tile.height, which (a) dropped
// mip chains and (b) truncated sub-tiled textures to the sub-tile's row
// count (e.g. the Rare-logo D_02005FF0 20x3 tile uploaded as 32x3).
if (!loaded_texture.addr) {
// set up miplevel 0; also acts as a catch-all for when .addr is NULL because my texture loader sucks
loaded_texture.addr = rdp.texture_to_load.addr;
loaded_texture.line_size_bytes = rdp.texture_tile[tile].line_size_bytes;
loaded_texture.full_image_line_size_bytes = rdp.texture_tile[tile].line_size_bytes;
loaded_texture.full_size_bytes = loaded_texture.full_image_line_size_bytes * rdp.texture_tile[tile].height;
loaded_texture.size_bytes = loaded_texture.line_size_bytes * rdp.texture_tile[tile].height;
if (siz == G_IM_SIZ_32b) {
// HACK: fixup 32-bit LODed texture height
loaded_texture.size_bytes <<= 1;
loaded_texture.full_size_bytes <<= 1;
}
loaded_texture.orig_size_bytes = loaded_texture.size_bytes;
}
/* RC2 (docs/dev/TEXTURE-GLITCH-ANALYSIS.md): GE's texGetDepthAndSize() sums the
* base level + every LOD mip into one gDPLoadBlock, so the block byte count
* (-> upload height = size_bytes / row) runs ~1.3x taller than the base
* image and the mip bytes render as garbage rows below it ("interlaced"
* textures on the menu / Depot). Only when LOD is active (rdp.tex_lod) AND
* the load is a plain full-width block (not a windowed gfx_dp_load_tile,
* which legitimately has extra rows -- the D74 sub-tile / Rare-logo case):
* clip to the SETTILESIZE base-tile height. GL then builds correct mips
* from a correct base image. */
if (g_fix_mip_textures && rdp.tex_lod &&
loaded_texture.line_size_bytes == loaded_texture.full_image_line_size_bytes) {
const uint32_t row = rdp.texture_tile[tile].line_size_bytes;
const uint32_t tile_h =
((uint32_t)(rdp.texture_tile[tile].lrt - rdp.texture_tile[tile].ult) >> 2) + 1;
if (row && tile_h > 1) {
uint32_t base_bytes = row * tile_h;
if (siz == G_IM_SIZ_32b)
base_bytes <<= 1; /* mirrors the 32b height fixup above */
if (base_bytes < loaded_texture.size_bytes) {
loaded_texture.size_bytes = base_bytes;
if (loaded_texture.full_size_bytes > base_bytes)
loaded_texture.full_size_bytes = base_bytes;
loaded_texture.orig_size_bytes = base_bytes;
}
}
}
const RawTexMetadata* metadata = &loaded_texture.raw_tex_metadata;
const uint8_t* orig_addr = loaded_texture.addr;
SUPPORT_CHECK(orig_addr);
TextureCacheKey key;
if (fmt == G_IM_FMT_CI) {
key = { orig_addr, { rdp.palette_addrs[0], rdp.palette_addrs[1] }, fmt, siz, palette_index,
loaded_texture.size_bytes };
} else {
key = { orig_addr, {}, fmt, siz, palette_index, loaded_texture.size_bytes };
}
if (gfx_texture_cache_lookup(i, key)) {
return;
}
// D71: importers read raw N64 byte streams; normalize C-array sources.
const uint8_t* saved_addr = loaded_texture.addr;
const uint32_t saved_full_line = loaded_texture.full_image_line_size_bytes;
loaded_texture.addr =
gfx_tex_normalize_source(orig_addr,
loaded_texture.full_size_bytes > loaded_texture.size_bytes
? loaded_texture.full_size_bytes
: loaded_texture.size_bytes);
/* D183: honour the source row pitch. gfx_dp_load_tile can load a
* sub-rectangle of a wider texture image: successive texel rows then sit
* full_image_line_size_bytes apart in RAM while only line_size_bytes of
* each row belong to the tile. Every import_texture_* except the CI8 one
* reads the source flat (they only assert line == full -- and the asserts
* compile out in the release build), so row r starts r*(full-line) bytes
* early -> a progressive diagonal shear that reads as grey static /
* "comb interlacing" (D176(b) Surface cliff walls, D182(2) file-select
* spiral). Compact the strided rows into a contiguous scratch buffer once
* here, so every importer sees line == full and no importer needs to know
* about pitch. No-op when the load was already row-packed (gDPLoadBlock,
* and any full-width gDPLoadTile), which is the overwhelming majority --
* hence golden-safe. */
std::vector<uint8_t> destride_buf;
if (gfx_tex_pitch_fix()) {
const uint32_t src_line = loaded_texture.line_size_bytes;
const uint32_t src_full = loaded_texture.full_image_line_size_bytes;
if (src_line && src_full > src_line && loaded_texture.size_bytes > src_line) {
const uint32_t rows = loaded_texture.size_bytes / src_line;
destride_buf.resize((size_t)rows * src_line);
for (uint32_t r = 0; r < rows; r++) {
memcpy(&destride_buf[(size_t)r * src_line],
loaded_texture.addr + (size_t)r * src_full, src_line);
}
loaded_texture.addr = destride_buf.data();
loaded_texture.full_image_line_size_bytes = src_line;
}
}
/* GE_DTEX: dump the load parameters for the first N textures of a frame so
* RC2 (mip-chain contamination -> over-tall upload) can be told apart from a
* decode/row-swap bug. tile_h = base-tile height from SETTILESIZE; if the
* computed upload height is much larger, the excess rows are LOD mip data.
* docs/dev/TEXTURE-GLITCH-ANALYSIS.md sec 6b. */
if (getenv("GE_DTEX")) {
static int dtexCount = 0;
if (dtexCount < 64) {
const uint32_t row = rdp.texture_tile[tile].line_size_bytes;
const uint32_t up_h = row ? (loaded_texture.size_bytes / row) : 0;
const uint32_t tile_h =
((rdp.texture_tile[tile].lrt - rdp.texture_tile[tile].ult) >> 2) + 1;
const uint32_t tile_w =
((rdp.texture_tile[tile].lrs - rdp.texture_tile[tile].uls) >> 2) + 1;
sysLogPrintf(LOG_NOTE,
"GE_DTEX[%d] addr=%p fmt=%u siz=%u lod=%d tile=%ux%u row=%u "
"line=%u full=%u size=%u -> upload=%ux%u%s%s",
dtexCount++, (void *)orig_addr, fmt, siz, (int)rdp.tex_lod,
tile_w, tile_h, row, loaded_texture.line_size_bytes,
loaded_texture.full_image_line_size_bytes,
loaded_texture.size_bytes,
row ? (row >> (siz ? siz - 1 : 0)) : 0, up_h,
(up_h > tile_h + 1) ? " <-- OVER-TALL (mip contamination?)" : "",
(loaded_texture.full_image_line_size_bytes !=
loaded_texture.line_size_bytes) ? " <-- STRIDED (pitch shear?)" : "");
}
}
if (getenv("GE_TEXDUMP")) {
static int tdc = 0;
const uint16_t* pal = (const uint16_t*)rdp.palette;
sysLogPrintf(LOG_NOTE,
"GE_TEXI[%d] addr=%p fmt=%u siz=%u palfmt=%u palidx=%u size=%u "
"tile=%dx%d pal[0..3]=%04x %04x %04x %04x", tdc++, (void*)orig_addr,
fmt, siz, rdp.palette_fmt, palette_index, loaded_texture.size_bytes,
((rdp.texture_tile[tile].lrs - rdp.texture_tile[tile].uls) >> 2) + 1,
((rdp.texture_tile[tile].lrt - rdp.texture_tile[tile].ult) >> 2) + 1,
pal[0], pal[1], pal[2], pal[3]);
/* GE_TEXRAW=1 additionally writes the raw source bytes handed to the
* importer (texdump/rNNN_f<fmt>_s<siz>_<w>x<h>.bin) -- lets a decode
* bug be told apart from a source-data bug offline (D183). */
if (tdc <= 400 && getenv("GE_TEXRAW")) {
char nm[160];
snprintf(nm, sizeof nm, "texdump/r%03d_f%u_s%u_%ux%u.bin", tdc - 1, fmt, siz,
((rdp.texture_tile[tile].lrs - rdp.texture_tile[tile].uls) >> 2) + 1,
((rdp.texture_tile[tile].lrt - rdp.texture_tile[tile].ult) >> 2) + 1);
FILE* bf = fopen(nm, "wb");
if (bf) { fwrite(loaded_texture.addr, 1, loaded_texture.size_bytes, bf); fclose(bf); }
}
}
/* D161: a CI-format tile drawn with the TLUT disabled (G_TT_NONE) must NOT
* do a palette lookup -- the N64 RDP feeds the raw TMEM texel straight into
* the colour pipe, i.e. it behaves as a plain intensity (I) texture. GE's
* Depot ceiling emits exactly this (CI8 + gsDPSetTextureLUT(G_TT_NONE));
* decoding it against the stale rdp.palette produced the blue-speckle roof
* (docs/dev/TEXTURE-GLITCH-ANALYSIS.md, B2). Route CI4/CI8 -> I4/I8 here. */
uint8_t fmt_eff = fmt;
if (fmt == G_IM_FMT_CI && rdp.palette_fmt == G_TT_NONE) {
fmt_eff = G_IM_FMT_I;
}
if (fmt_eff == G_IM_FMT_RGBA) {
if (siz == G_IM_SIZ_16b) {
import_texture_rgba16(tile, loaded_texture, rdp.tex_lod);
} else if (siz == G_IM_SIZ_32b) {
import_texture_rgba32(tile, loaded_texture, rdp.tex_lod);
} else {
sysFatalError("Bad size for RGBA texture in tile %d: %02x", tile, siz);
}
} else if (fmt_eff == G_IM_FMT_IA) {
if (siz == G_IM_SIZ_4b) {
import_texture_ia4(tile, loaded_texture, rdp.tex_lod);
} else if (siz == G_IM_SIZ_8b) {
import_texture_ia8(tile, loaded_texture, rdp.tex_lod);
} else if (siz == G_IM_SIZ_16b) {
import_texture_ia16(tile, loaded_texture, rdp.tex_lod);
} else {
sysFatalError("Bad size for IA texture in tile %d: %02x", tile, siz);
}
} else if (fmt_eff == G_IM_FMT_CI) {
if (siz == G_IM_SIZ_4b) {
import_texture_ci4(tile, loaded_texture, rdp.tex_lod);
} else if (siz == G_IM_SIZ_8b) {
import_texture_ci8(tile, loaded_texture, rdp.tex_lod);
} else {
sysFatalError("Bad size for CI texture in tile %d: %02x", tile, siz);
}
} else if (fmt_eff == G_IM_FMT_I) {
if (siz == G_IM_SIZ_4b) {
import_texture_i4(tile, loaded_texture, rdp.tex_lod);
} else if (siz == G_IM_SIZ_8b) {
import_texture_i8(tile, loaded_texture, rdp.tex_lod);
} else {
sysFatalError("Bad size for I texture in tile %d: %02x", tile, siz);
}
} else {
sysFatalError("Bad texture format in tile %d: %02x %02x", tile, fmt, siz);
}
loaded_texture.addr = saved_addr;
loaded_texture.full_image_line_size_bytes = saved_full_line;
}
static void gfx_normalize_vector(float v[3]) {
float s = sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
v[0] /= s;
v[1] /= s;
v[2] /= s;
}
static void gfx_transposed_matrix_mul(float res[3], const float a[3], const float b[4][4]) {
res[0] = a[0] * b[0][0] + a[1] * b[0][1] + a[2] * b[0][2];
res[1] = a[0] * b[1][0] + a[1] * b[1][1] + a[2] * b[1][2];
res[2] = a[0] * b[2][0] + a[1] * b[2][1] + a[2] * b[2][2];
}
static void calculate_normal_dir(const Light_t* light, float coeffs[3]) {
const float light_dir[3] = { light->dir[0] / 127.f, light->dir[1] / 127.f, light->dir[2] / 127.f };
gfx_transposed_matrix_mul(coeffs, light_dir, rsp.modelview_matrix_stack[rsp.modelview_matrix_stack_size - 1]);
gfx_normalize_vector(coeffs);
}
static void calculate_normal_dir(const struct NormalColor *vcn, float coeffs[3]) {
const float light_dir[3] = { vcn->x / 127.f, vcn->y / 127.f, vcn->z / 127.f };
gfx_transposed_matrix_mul(coeffs, light_dir, rsp.modelview_matrix_stack[rsp.modelview_matrix_stack_size - 1]);
gfx_normalize_vector(coeffs);
}
static void gfx_matrix_mul(float res[4][4], const float a[4][4], const float b[4][4]) {
float tmp[4][4];
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
tmp[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j] + a[i][3] * b[3][j];
}
}
memcpy(res, tmp, sizeof(tmp));
}
static void gfx_sp_matrix(uint8_t parameters, const int32_t* addr) {
float matrix[4][4];
/* D144: a front-end 3D model (MISSION COMPLETE dossier, mode-select
* wallets - the D75 family) can emit a gSPMatrix whose pointer field was
* never resolved: w1 comes through as 0xFFFF... or another non-canonical
* value, and seg_addr() then hands us a wild pointer -> AV reading addr[].
* A real N64 matrix pointer always lands in mapped memory. Rather than
* crash the menu transition, substitute identity for an unmapped source
* (the model draws with the wrong transform - already a parked D75
* cosmetic - but the screen and its "Next" -> menu path work). */
const bool addr_bad = ((uintptr_t)addr < 0x10000 ||
(uintptr_t)addr >= 0x0000800000000000ULL);
if (addr_bad) {
memset(matrix, 0, sizeof(matrix));
matrix[0][0] = matrix[1][1] = matrix[2][2] = matrix[3][3] = 1.0f;
} else
#ifndef GBI_FLOATS
// Original GBI where fixed point matrices are used
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j += 2) {
int32_t int_part = addr[i * 2 + j / 2];
uint32_t frac_part = addr[8 + i * 2 + j / 2];
matrix[i][j] = (int32_t)((int_part & 0xffff0000) | (frac_part >> 16)) / 65536.0f;
matrix[i][j + 1] = (int32_t)((int_part << 16) | (frac_part & 0xffff)) / 65536.0f;
}
}
#else
// For a modified GBI where fixed point values are replaced with floats
memcpy(matrix, addr, sizeof(matrix));
#endif
if (parameters & G_MTX_PROJECTION) {
if (parameters & G_MTX_LOAD) {
memcpy(rsp.P_matrix, matrix, sizeof(matrix));
} else {
gfx_matrix_mul(rsp.P_matrix, matrix, rsp.P_matrix);
}
} else { // G_MTX_MODELVIEW
if ((parameters & G_MTX_PUSH) && rsp.modelview_matrix_stack_size < 11) {
++rsp.modelview_matrix_stack_size;
memcpy(rsp.modelview_matrix_stack[rsp.modelview_matrix_stack_size - 1],
rsp.modelview_matrix_stack[rsp.modelview_matrix_stack_size - 2], sizeof(matrix));
}
if (parameters & G_MTX_LOAD) {
memcpy(rsp.modelview_matrix_stack[rsp.modelview_matrix_stack_size - 1], matrix, sizeof(matrix));
} else {
gfx_matrix_mul(rsp.modelview_matrix_stack[rsp.modelview_matrix_stack_size - 1], matrix,
rsp.modelview_matrix_stack[rsp.modelview_matrix_stack_size - 1]);
}
rsp.lights_changed = 1;
}
gfx_matrix_mul(rsp.MP_matrix, rsp.modelview_matrix_stack[rsp.modelview_matrix_stack_size - 1], rsp.P_matrix);
}
static void gfx_sp_pop_matrix(uint32_t count) {
while (count--) {
if (rsp.modelview_matrix_stack_size > 0) {
--rsp.modelview_matrix_stack_size;
if (rsp.modelview_matrix_stack_size > 0) {
gfx_matrix_mul(rsp.MP_matrix, rsp.modelview_matrix_stack[rsp.modelview_matrix_stack_size - 1],
rsp.P_matrix);
}
}
}
}
static float gfx_adjust_x_for_aspect_ratio(float x, float w = 1.f) {
if (fbActive) {
return x;
} else {
return (rsp.aspect_ofs * w + x) * rsp.aspect_scale / gfx_current_dimensions.aspect_ratio;
}
}
static void gfx_adjust_width_height_for_scale(uint32_t& width, uint32_t& height) {
width = std::round(width * RATIO_Y);
height = std::round(height * RATIO_Y);
if (width == 0) {
width = 1;
}
if (height == 0) {
height = 1;
}
}
/* D146: a valid mapped-memory pointer check. A desynced / corrupt front-end
* DL (D75 family) hands wild pointers to the RSP command handlers; abort()
* over one bad menu model is the wrong trade for a breadth-first port. */
static inline bool fast3d_ptr_ok(const void *p) {
uintptr_t v = (uintptr_t)p;
return v >= 0x10000 && v < 0x0000800000000000ULL;
}
static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* vertices) {
SUPPORT_CHECK(n_vertices <= MAX_VERTICES);
if (!fast3d_ptr_ok(vertices) || dest_index + n_vertices > MAX_VERTICES) {
return; /* D146 */
}
for (size_t i = 0; i < n_vertices; i++, dest_index++) {
const Vtx* v = &vertices[i];
struct LoadedVertex* d = &rsp.loaded_vertices[dest_index];
/* GE's Vtx layout (include/PR/gbi.h): ob[] position, tc[] texture
* coords, and the last 4 bytes are either RGBA (Vtx_t.cn) or a
* normal + alpha (Vtx_tn.n/a). Unlike PD there is no G_COL colour
* table: colours/normals live in the vertex itself. */
float x = v->v.ob[0] * rsp.MP_matrix[0][0] + v->v.ob[1] * rsp.MP_matrix[1][0] + v->v.ob[2] * rsp.MP_matrix[2][0] + rsp.MP_matrix[3][0];
float y = v->v.ob[0] * rsp.MP_matrix[0][1] + v->v.ob[1] * rsp.MP_matrix[1][1] + v->v.ob[2] * rsp.MP_matrix[2][1] + rsp.MP_matrix[3][1];
float z = v->v.ob[0] * rsp.MP_matrix[0][2] + v->v.ob[1] * rsp.MP_matrix[1][2] + v->v.ob[2] * rsp.MP_matrix[2][2] + rsp.MP_matrix[3][2];
float w = v->v.ob[0] * rsp.MP_matrix[0][3] + v->v.ob[1] * rsp.MP_matrix[1][3] + v->v.ob[2] * rsp.MP_matrix[2][3] + rsp.MP_matrix[3][3];
x = gfx_adjust_x_for_aspect_ratio(x, w);
short U = v->v.tc[0] * rsp.texture_scaling_factor.s >> 16;
short V = v->v.tc[1] * rsp.texture_scaling_factor.t >> 16;
struct NormalColor vcn_data;
if (rsp.geometry_mode & G_LIGHTING) {
/* Lit vertices carry their normal in the last 3 bytes. */
vcn_data.x = (int8_t)v->n.n[0];
vcn_data.y = (int8_t)v->n.n[1];
vcn_data.z = (int8_t)v->n.n[2];
vcn_data.a = v->n.a;
} else {
vcn_data.r = v->v.cn[0];
vcn_data.g = v->v.cn[1];
vcn_data.b = v->v.cn[2];
vcn_data.a = v->v.cn[3];
}
const struct NormalColor *vcn = &vcn_data;
if (rsp.geometry_mode & G_LIGHTING) {
if (rsp.lights_changed) {
for (int i = 0; i < rsp.current_num_lights - 1; i++) {
calculate_normal_dir(&rsp.current_lights[i], rsp.current_lights_coeffs[i]);
}
if (rsp.lookat_enabled) {
calculate_normal_dir(&rsp.lookat[0], rsp.current_lookat_coeffs[0]);
calculate_normal_dir(&rsp.lookat[1], rsp.current_lookat_coeffs[1]);
}
rsp.lights_changed = false;
}
int r = rsp.current_lights[rsp.current_num_lights - 1].col[0];
int g = rsp.current_lights[rsp.current_num_lights - 1].col[1];
int b = rsp.current_lights[rsp.current_num_lights - 1].col[2];
for (int i = 0; i < rsp.current_num_lights - 1; i++) {
float intensity = 0;
intensity += vcn->x * rsp.current_lights_coeffs[i][0];
intensity += vcn->y * rsp.current_lights_coeffs[i][1];
intensity += vcn->z * rsp.current_lights_coeffs[i][2];
intensity /= 127.0f;
if (intensity > 0.0f) {
r += intensity * rsp.current_lights[i].col[0];
g += intensity * rsp.current_lights[i].col[1];
b += intensity * rsp.current_lights[i].col[2];
}
}
d->color.r = r > 255 ? 255 : r;
d->color.g = g > 255 ? 255 : g;
d->color.b = b > 255 ? 255 : b;
/* D72 (docs/internals.md): unlike the PD port, GE never
* generates texture coordinates from vertex normals. This PD-
* inherited block overwrote the authored tc[] UVs on every
* lit+textured surface (lookat_enabled defaults to true here),
* which smeared the Rareware-logo quads into a diagonal gold
* wedge: their corner UVs (0x0010..0x03F0 = full 32x32 coverage)
* were replaced by normal-derived values. GE's ground truth:
* every DL that could env-map sets gsDPSetTextureLUT(G_TT_NONE),
* stage rendering always has a camera lookat yet stage textures
* are surface-fixed on hardware, and the logo vertex batches
* carry authored atlas-slice UVs. So U/V stay as computed from
* tc[] above. (gSPLookAt state is still tracked — it may matter
* for lighting fidelity later.) */
} else {
d->color.r = vcn->r;
d->color.g = vcn->g;
d->color.b = vcn->b;
}
d->u = U;
d->v = V;
// trivial clip rejection
d->clip_rej = 0;
if (x < -w) {
d->clip_rej |= 1; // CLIP_LEFT
}
if (x > w) {
d->clip_rej |= 2; // CLIP_RIGHT
}
if (y < -w) {
d->clip_rej |= 4; // CLIP_BOTTOM
}
if (y > w) {
d->clip_rej |= 8; // CLIP_TOP
}
// if (z < -w) d->clip_rej |= 16; // CLIP_NEAR
if (z > w) {
d->clip_rej |= 32; // CLIP_FAR
}
d->x = x;
d->y = y;
d->z = z;
d->w = w;
if (rsp.geometry_mode & G_FOG) {
if (fabsf(w) < 0.001f) {
// To avoid division by zero
w = 0.001f;
}
float winv = 1.0f / w;
if (winv < 0.0f) {
winv = std::numeric_limits<int16_t>::max();
}
float fog_z = z * winv * rsp.fog_mul + rsp.fog_offset;
d->fog = clampf(fog_z, 0.f, 255.f);
} else {
d->fog = rdp.fog_color.a;
}
d->color.a = vcn->a; // can be required for SHADE_ALPHA even if fog is enabled
}
}
static void gfx_sp_modify_vertex(uint16_t vtx_idx, uint8_t where, uint32_t val) {
SUPPORT_CHECK(where == G_MWO_POINT_ST);
int16_t s = (int16_t)(val >> 16);
int16_t t = (int16_t)val;
struct LoadedVertex* v = &rsp.loaded_vertices[vtx_idx];
v->u = s;
v->v = t;
}
static inline int gfx_lod_tile_offset(const int i) {
if (gfx_detail_textures_enabled)
return ((rdp.tex_lod && !rdp.tex_detail) ? 0 : i);
// D107: GE has no true detail textures (gfx_detail_textures_enabled is
// false), but its room GDLs still emit G_TL_LOD + G_TD_DETAIL for
// mip-mapped textures. The old `rdp.tex_lod ? rdp.tex_detail : i` then
// returned tex_detail (1) for every texel -> fast3d sampled GE's first
// mip (tile 1), whose single-LOADBLOCK TMEM slot fast3d never registers
// -> a magnified crop of the base image (the "blurry blob" ceilings /
// wall panels in BUNKER1). GE loads the whole mip chain at TMEM 0, so
// for an LOD texture the base render tile is the only correctly-loaded
// level: use it.
//
// D172: but this must NOT collapse a genuine non-LOD two-texture combine.
// The explosion/blood/spark particle records (assets/oddtextures.c
// globalDL_0x078..) set G_TL_TILE (no LOD) + G_CC_INTERFERENCE
// (TEXEL0 * TEXEL1) and bind tile 0 = IA8 smoke @ TMEM 0, tile 1 = RGBA16
// fire @ TMEM 0x188. Returning 0 here fed TEXEL1 the smoke texture too
// (smoke * smoke) -> the magenta/cyan particle colour. Only fold to the
// base tile when LOD is actually active.
return rdp.tex_lod ? 0 : i;
}
static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bool is_rect) {
struct LoadedVertex* v1 = &rsp.loaded_vertices[vtx1_idx];
struct LoadedVertex* v2 = &rsp.loaded_vertices[vtx2_idx];
struct LoadedVertex* v3 = &rsp.loaded_vertices[vtx3_idx];
struct LoadedVertex* v_arr[3] = { v1, v2, v3 };
if ((rsp.extra_geometry_mode & G_NO_CLIPPING_EXT) == 0) {
if (v1->clip_rej & v2->clip_rej & v3->clip_rej) {
// The whole triangle lies outside the visible area
return;
}
}
if ((rsp.geometry_mode & G_CULL_BOTH) != 0) {
float dx1 = v1->x / (v1->w) - v2->x / (v2->w);
float dy1 = v1->y / (v1->w) - v2->y / (v2->w);
float dx2 = v3->x / (v3->w) - v2->x / (v2->w);
float dy2 = v3->y / (v3->w) - v2->y / (v2->w);
float cross = dx1 * dy2 - dy1 * dx2;
if ((v1->w < 0) ^ (v2->w < 0) ^ (v3->w < 0)) {
// If one vertex lies behind the eye, negating cross will give the correct result.
// If all vertices lie behind the eye, the triangle will be rejected anyway.
cross = -cross;
}
// If inverted culling is requested, negate the cross
// if ((rsp.extra_geometry_mode & G_EX_INVERT_CULLING) == 1) {
// cross = -cross;
// }
switch (rsp.geometry_mode & G_CULL_BOTH) {
case G_CULL_FRONT:
if (cross <= 0) {
return;
}
break;
case G_CULL_BACK:
if (cross >= 0) {
return;
}
break;
case G_CULL_BOTH:
// Why is this even an option?
return;
}
}
bool depth_test = ((rsp.geometry_mode & G_ZBUFFER) == G_ZBUFFER || (rdp.other_mode_l & G_ZS_PRIM) == G_ZS_PRIM) &&
((rdp.other_mode_h & G_CYC_1CYCLE) == G_CYC_1CYCLE || (rdp.other_mode_h & G_CYC_2CYCLE) == G_CYC_2CYCLE);
bool depth_update = (rdp.other_mode_l & Z_UPD) == Z_UPD;
bool depth_compare = (rdp.other_mode_l & Z_CMP) == Z_CMP;
bool depth_source_prim = (rdp.other_mode_l & G_ZS_PRIM) == G_ZS_PRIM /* && gDP.primDepth.z == 1.0f */;
uint16_t zmode = rdp.other_mode_l & ZMODE_DEC;
uint8_t depth_mode = (depth_test ? 1 : 0) | (depth_update ? 2 : 0) | (depth_compare ? 4 : 0) | (depth_source_prim ? 8 : 0) | (zmode >> 6);
if (depth_mode != rendering_state.depth_mode) {
gfx_flush();
gfx_rapi->set_depth_mode(depth_test, depth_update, depth_compare, depth_source_prim, zmode);
rendering_state.depth_mode = depth_mode;
}
if (rdp.viewport_or_scissor_changed) {
if (memcmp(&rdp.viewport, &rendering_state.viewport, sizeof(rdp.viewport)) != 0) {
gfx_flush();
gfx_rapi->set_viewport(rdp.viewport.x, rdp.viewport.y, rdp.viewport.width, rdp.viewport.height);
rendering_state.viewport = rdp.viewport;
}
if (memcmp(&rdp.scissor, &rendering_state.scissor, sizeof(rdp.scissor)) != 0) {
gfx_flush();
gfx_rapi->set_scissor(rdp.scissor.x, rdp.scissor.y, rdp.scissor.width, rdp.scissor.height);
rendering_state.scissor = rdp.scissor;
}
rdp.viewport_or_scissor_changed = false;
}
uint64_t cc_options = 0;
bool use_alpha =
(rdp.other_mode_l & (3 << 20)) == (G_BL_CLR_MEM << 20) && (rdp.other_mode_l & (3 << 16)) == (G_BL_1MA << 16);
const bool use_fog = ((rdp.other_mode_l >> 30) == G_BL_CLR_FOG) || ((rdp.other_mode_l >> 26) == G_BL_A_FOG);
const bool texture_edge = (rdp.other_mode_l & CVG_X_ALPHA) == CVG_X_ALPHA;
const bool use_noise = (rdp.other_mode_l & (3U << G_MDSFT_ALPHACOMPARE)) == G_AC_DITHER;
const bool use_2cyc = (rdp.other_mode_h & (3U << G_MDSFT_CYCLETYPE)) == G_CYC_2CYCLE;
const bool alpha_threshold = (rdp.other_mode_l & (3U << G_MDSFT_ALPHACOMPARE)) == G_AC_THRESHOLD;
const bool invisible = (rdp.other_mode_l & (3 << 24)) == (G_BL_0 << 24) && (rdp.other_mode_l & (3 << 20)) == (G_BL_CLR_MEM << 20);
const bool use_grayscale = rdp.grayscale;
const bool use_modulate = use_alpha && (rsp.extra_geometry_mode & G_MODULATE_EXT) != 0;
const bool use_blur = (rdp.other_mode_h & (3U << G_MDSFT_TEXTFILT)) == G_TF_BLUR_EXT;
if (texture_edge) {
use_alpha = true;
}
if (use_alpha) {
cc_options |= (uint64_t)SHADER_OPT_ALPHA;
}
if (use_fog) {
cc_options |= (uint64_t)SHADER_OPT_FOG;
}
if (texture_edge) {
cc_options |= (uint64_t)SHADER_OPT_TEXTURE_EDGE;
}
if (use_noise) {
cc_options |= (uint64_t)SHADER_OPT_NOISE;
}
if (use_2cyc) {
cc_options |= (uint64_t)SHADER_OPT_2CYC;
}
if (alpha_threshold) {
cc_options |= (uint64_t)SHADER_OPT_ALPHA_THRESHOLD;
}
if (invisible) {
cc_options |= (uint64_t)SHADER_OPT_INVISIBLE;
}
if (use_grayscale) {
cc_options |= (uint64_t)SHADER_OPT_GRAYSCALE;
}
if (use_blur) {
cc_options |= (uint64_t)SHADER_OPT_BLUR;
}
// If we are not using alpha, clear the alpha components of the combiner as they have no effect
if (!use_alpha) {
cc_options &= ~((0xfff << 16) | ((uint64_t)0xfff << 44));
}
ColorCombinerKey key;
key.combine_mode = rdp.combine_mode;
key.options = cc_options;
ColorCombiner* comb = gfx_lookup_or_create_color_combiner(key);
uint32_t tm = 0;
uint32_t tex_width[2], tex_height[2], tex_width2[2], tex_height2[2];
/* D74 (Video.WrapFix): per-texunit pre-wrap window. N64 wraps a render
* tile's UVs at the TILE period (uls/ult + lrs/lrt window) when the tile
* is a sub-region of the uploaded image; GL wraps at the full image size.
* Computed once per texunit here (needs the ORIGINAL cms/cmt, before the
* CLAMP-clearing below) and applied per vertex in the loop. Opt-in --
* this path never ran before (the old guard was `cms & G_TX_WRAP` ==
* `& 0`), so it is new behaviour. */
bool wrap_s[2] = { false, false }, wrap_t[2] = { false, false };
float wrap_tw[2] = { 0, 0 }, wrap_th[2] = { 0, 0 };
float wrap_uls[2] = { 0, 0 }, wrap_ult[2] = { 0, 0 };
for (int i = 0; i < 2; i++) {
// TODO: fix this; for now just ignore smaller mips
const uint32_t tile = rdp.first_tile_index + gfx_lod_tile_offset(i);
if (comb->used_textures[i]) {
if (rdp.textures_changed[i]) {
gfx_flush();
import_texture(i, tile, false);
rdp.textures_changed[i] = false;
}
uint8_t cms = rdp.texture_tile[tile].cms;
uint8_t cmt = rdp.texture_tile[tile].cmt;
uint32_t tex_size_bytes = rdp.loaded_texture[rdp.texture_tile[tile].tmem].orig_size_bytes;
uint32_t line_size = rdp.texture_tile[tile].line_size_bytes;
if (line_size == 0) {
line_size = 1;
}
tex_height[i] = tex_size_bytes / line_size;
switch (rdp.texture_tile[tile].siz) {
case G_IM_SIZ_4b:
line_size <<= 1;
break;
case G_IM_SIZ_8b:
break;
case G_IM_SIZ_16b:
line_size /= G_IM_SIZ_16b_LINE_BYTES;
break;
case G_IM_SIZ_32b:
line_size /= G_IM_SIZ_32b_LINE_BYTES; // this is 2!
tex_height[i] /= 2;
break;
}
tex_width[i] = line_size;
tex_width2[i] = (rdp.texture_tile[tile].lrs - rdp.texture_tile[tile].uls + 4) / 4;
tex_height2[i] = (rdp.texture_tile[tile].lrt - rdp.texture_tile[tile].ult + 4) / 4;
uint32_t tex_width1 = tex_width[i] << (cms & G_TX_MIRROR);
uint32_t tex_height1 = tex_height[i] << (cmt & G_TX_MIRROR);
if (g_wrap_fix && !(cms & G_TX_MIRROR)) {
/* (a) sub-tile window: wrap tile (clamp bit not set) whose
* uls..lrs window is smaller than the uploaded image -> pre-fmod
* the UVs at the window size. */
if (!(cms & G_TX_CLAMP) && tex_width2[i] > 0 && tex_width2[i] < tex_width[i]) {
wrap_s[i] = true;
wrap_tw[i] = (float)tex_width2[i];
wrap_uls[i] = rdp.texture_tile[tile].uls / 4.0f;
}
if (!(cmt & G_TX_CLAMP) && tex_height2[i] > 0 && tex_height2[i] < tex_height[i]) {
wrap_t[i] = true;
wrap_th[i] = (float)tex_height2[i];
wrap_ult[i] = rdp.texture_tile[tile].ult / 4.0f;
}
/* (b) RC3 non-power-of-two wrap period: the N64 RDP masks the
* texel coordinate at 1<<mask (GE sets mask = ceil(log2(dim)),
* texDimensionToMask), so a non-PoT tile repeats at the NEXT
* power of two, not at its image size the way GL GL_REPEAT does.
* Fold the UV at the N64 period; the [dim, 1<<mask) overflow
* band (TMEM smear on console) is clamped to the last texel so
* it reads as an edge streak instead of a bogus early repeat. */
{
uint8_t mks = rdp.texture_tile[tile].masks;
uint8_t mkt = rdp.texture_tile[tile].maskt;
if (!wrap_s[i] && !(cms & G_TX_CLAMP) && mks >= 1 && mks <= 14) {
float period = (float)(1u << mks);
if (period != (float)tex_width[i] && tex_width[i] > 0) {
wrap_s[i] = true;
wrap_tw[i] = period;
wrap_uls[i] = 0.0f;
}
}
if (!wrap_t[i] && !(cmt & G_TX_CLAMP) && mkt >= 1 && mkt <= 14) {
float period = (float)(1u << mkt);
if (period != (float)tex_height[i] && tex_height[i] > 0) {
wrap_t[i] = true;
wrap_th[i] = period;
wrap_ult[i] = 0.0f;
}
}
}
}
if ((cms & G_TX_CLAMP) && ((cms & G_TX_MIRROR) || tex_width1 != tex_width2[i])) {
tm |= 1 << 2 * i;
cms &= ~G_TX_CLAMP;
}
if ((cmt & G_TX_CLAMP) && ((cmt & G_TX_MIRROR) || tex_height1 != tex_height2[i])) {
tm |= 1 << (2 * i + 1);
cmt &= ~G_TX_CLAMP;
}
if (rendering_state.textures[i]) {
bool linear_filter = (rdp.other_mode_h & (3U << G_MDSFT_TEXTFILT)) != G_TF_POINT;
if (linear_filter != rendering_state.textures[i]->second.linear_filter ||
cms != rendering_state.textures[i]->second.cms || cmt != rendering_state.textures[i]->second.cmt) {
gfx_flush();
gfx_rapi->set_sampler_parameters(i, linear_filter, cms, cmt, rdp.tex_lod);
rendering_state.textures[i]->second.linear_filter = linear_filter;
rendering_state.textures[i]->second.cms = cms;
rendering_state.textures[i]->second.cmt = cmt;
}
}
}
}
#ifdef PORT
/* D172 probe (env-gated, inert): the magenta/cyan blood/spark bug. The
* particle records (assets/oddtextures.c globalDL_0x078..) draw a 2-cycle
* G_CC_INTERFERENCE combine (TEXEL0*TEXEL1) binding two tiles of two
* formats - tile 0 IA8 smoke, tile 1 RGBA16 fire @ tmem 0x188. Dump both
* texunits' tile state whenever a 2-cycle tri actually consumes TEXEL1, so
* one Silo capture pins whether TEXEL1 resolves to the right tmem/format. */
if (getenv("GE_D172") && use_2cyc && comb->used_textures[1] && !rdp.tex_lod) {
const uint32_t fi = rdp.first_tile_index;
/* the tile fast3d will actually SAMPLE for each texunit */
const uint32_t s0 = fi + gfx_lod_tile_offset(0);
const uint32_t s1 = fi + gfx_lod_tile_offset(1);
/* the tile the DL actually CONFIGURED for texunit 1 */
const uint32_t c1 = fi + 1;
/* only interesting when the DL set up a genuinely distinct 2nd tile
* (different tmem) -- filters out mip/LOD-bilerp false positives */
if (rdp.texture_tile[c1].tmem != rdp.texture_tile[fi].tmem) {
static int d172x = 0;
if (d172x < 40) {
d172x++;
sysLogPrintf(LOG_NOTE,
"D172: multitex tri combine=%llx tex_lod=%d first=%u | "
"cfg1[tile=%u tmem=%u fmt=%u siz=%u] | "
"SAMPLED0=tile%u(tmem=%u) SAMPLED1=tile%u(tmem=%u fmt=%u)%s",
(unsigned long long)rdp.combine_mode, (int)rdp.tex_lod, fi,
c1, rdp.texture_tile[c1].tmem, rdp.texture_tile[c1].fmt, rdp.texture_tile[c1].siz,
s0, rdp.texture_tile[s0].tmem,
s1, rdp.texture_tile[s1].tmem, rdp.texture_tile[s1].fmt,
(s1 == s0) ? " <-- TEXEL1 == TEXEL0 (BUG)" : "");
}
}
}
#endif
struct ShaderProgram* prg = comb->prg[tm];
if (prg == NULL) {
comb->prg[tm] = prg =
gfx_lookup_or_create_shader_program(comb->shader_id0, comb->shader_id1 | (tm * SHADER_OPT_TEXEL0_CLAMP_S));
}
if (prg != rendering_state.shader_program) {
gfx_flush();
gfx_rapi->unload_shader(rendering_state.shader_program);
gfx_rapi->load_shader(prg);
rendering_state.shader_program = prg;
}
if (use_alpha != rendering_state.alpha_blend || use_modulate != rendering_state.modulate) {
gfx_flush();
gfx_rapi->set_use_alpha(use_alpha, use_modulate);
rendering_state.alpha_blend = use_alpha;
rendering_state.modulate = use_modulate;
}
uint8_t num_inputs;
bool used_textures[2];
gfx_rapi->shader_get_info(prg, &num_inputs, used_textures);
struct GfxClipParameters clip_parameters = gfx_rapi->get_clip_parameters();
for (int i = 0; i < 3; i++) {
float z = v_arr[i]->z, w = v_arr[i]->w;
if (clip_parameters.z_is_from_0_to_1) {
z = (z + w) / 2.0f;
}
buf_vbo[buf_vbo_len++] = v_arr[i]->x;
buf_vbo[buf_vbo_len++] = clip_parameters.invert_y ? -v_arr[i]->y : v_arr[i]->y;
buf_vbo[buf_vbo_len++] = z;
buf_vbo[buf_vbo_len++] = w;
for (int t = 0; t < 2; t++) {
if (!used_textures[t]) {
continue;
}
// TODO: fix this; for now just ignore smaller mips
const uint32_t tile = gfx_lod_tile_offset(t);
float u = v_arr[i]->u / 32.0f;
float v = v_arr[i]->v / 32.0f;
int shifts = rdp.texture_tile[rdp.first_tile_index + tile].shifts;
int shiftt = rdp.texture_tile[rdp.first_tile_index + tile].shiftt;
if (shifts != 0) {
if (shifts <= 10) {
u /= 1 << shifts;
} else {
u *= 1 << (16 - shifts);
}
}
if (shiftt != 0) {
if (shiftt <= 10) {
v /= 1 << shiftt;
} else {
v *= 1 << (16 - shiftt);
}
}
u -= rdp.texture_tile[rdp.first_tile_index + tile].uls / 4.0f;
v -= rdp.texture_tile[rdp.first_tile_index + tile].ult / 4.0f;
// D74 (Video.WrapFix, opt-in): pre-wrap UVs at the tile-window
// period when the render tile is a sub-region of the uploaded
// image. Flags/sizes are precomputed per texunit above (indexed by
// `t`, not the vertex `i`). The old in-place version was inert
// (guard `cms & G_TX_WRAP` == `& 0`) and OOB (`tex_width2[i]` with
// `i` = vertex). See SMALL-FIXES B1.
if (wrap_s[t]) {
u = fmodf(u, wrap_tw[t]);
if (u < 0.0f) u += wrap_tw[t];
u += wrap_uls[t];
/* RC3: when the N64 wrap period (wrap_tw) exceeds the uploaded
* image width, the [width, period) band has no real texels --
* clamp it to the edge rather than let GL_REPEAT restart the
* image early. Never triggers for the sub-tile-window case
* (wrap_tw + wrap_uls stay within tex_width). */
if (tex_width[t] > 0 && u > (float)tex_width[t]) u = (float)tex_width[t] - 0.5f;
}
if (wrap_t[t]) {
v = fmodf(v, wrap_th[t]);
if (v < 0.0f) v += wrap_th[t];
v += wrap_ult[t];
if (tex_height[t] > 0 && v > (float)tex_height[t]) v = (float)tex_height[t] - 0.5f;
}
if (!is_rect) {
if (!(rdp.other_mode_h & G_TP_PERSP)) {
u *= 0.5f;
v *= 0.5f;
}
if ((rdp.other_mode_h & (3U << G_MDSFT_TEXTFILT)) != G_TF_POINT) {
// Linear filter adds 0.5f to the coordinates
u += 0.5f;
v += 0.5f;
}
}
buf_vbo[buf_vbo_len++] = u / tex_width[t];
buf_vbo[buf_vbo_len++] = v / tex_height[t];
#ifdef PORT
{
static int ge_d116 = -1;
if (ge_d116 < 0) ge_d116 = getenv("GE_D116") ? 1 : 0;
if (ge_d116 && is_rect && t == 0 && tex_width[t] > 0 && tex_width[t] <= 32) {
fprintf(stderr,
"[D116/vbo] rect vtx%d x=%.4f y=%.4f u=%.4f v=%.4f "
"(raw v->u=%.1f texw=%.0f texw2=%u tile.uls=%d lrs=%d cms=%d)\n",
i, v_arr[i]->x, v_arr[i]->y, u / tex_width[t], v / tex_height[t],
v_arr[i]->u, tex_width[t], tex_width2[i],
rdp.texture_tile[rdp.first_tile_index + tile].uls,
rdp.texture_tile[rdp.first_tile_index + tile].lrs,
rdp.texture_tile[rdp.first_tile_index + tile].cms);
}
}
#endif
bool clampS = tm & (1 << 2 * t);
bool clampT = tm & (1 << (2 * t + 1));
if (clampS) {
buf_vbo[buf_vbo_len++] = (tex_width2[t] - 0.5f) / tex_width[t];
}
if (clampT) {
buf_vbo[buf_vbo_len++] = (tex_height2[t] - 0.5f) / tex_height[t];
}
}
if (use_fog) {
buf_vbo[buf_vbo_len++] = rdp.fog_color.r / 255.0f;
buf_vbo[buf_vbo_len++] = rdp.fog_color.g / 255.0f;
buf_vbo[buf_vbo_len++] = rdp.fog_color.b / 255.0f;
buf_vbo[buf_vbo_len++] = v_arr[i]->fog / 255.0f; // fog factor
}
if (use_grayscale) {
buf_vbo[buf_vbo_len++] = rdp.grayscale_color.r / 255.0f;
buf_vbo[buf_vbo_len++] = rdp.grayscale_color.g / 255.0f;
buf_vbo[buf_vbo_len++] = rdp.grayscale_color.b / 255.0f;
buf_vbo[buf_vbo_len++] = rdp.grayscale_color.a / 255.0f; // lerp interpolation factor (not alpha)
}
for (int j = 0; j < num_inputs; j++) {
struct RGBA* color = 0;
struct RGBA tmp = { 0 };
for (int k = 0; k < 1 + (use_alpha ? 1 : 0); k++) {
switch (comb->shader_input_mapping[k][j]) {
// Note: CCMUX constants and ACMUX constants used here have same value, which is why this works
// (except LOD fraction).
case G_CCMUX_PRIMITIVE:
color = &rdp.prim_color;
break;
case G_CCMUX_SHADE:
color = &v_arr[i]->color;
break;
case G_CCMUX_SHADE_ALPHA:
tmp.r = tmp.g = tmp.b = v_arr[i]->color.a;
color = &tmp;
break;
case G_CCMUX_ENVIRONMENT:
color = &rdp.env_color;
break;
case G_CCMUX_PRIMITIVE_ALPHA: {
tmp.r = tmp.g = tmp.b = rdp.prim_color.a;
color = &tmp;
break;
}
case G_CCMUX_ENV_ALPHA: {
tmp.r = tmp.g = tmp.b = rdp.env_color.a;
color = &tmp;
break;
}
case G_CCMUX_PRIM_LOD_FRAC: {
tmp.r = tmp.g = tmp.b = rdp.prim_lod_fraction;
color = &tmp;
break;
}
case G_CCMUX_LOD_FRACTION: {
if (rdp.other_mode_h & G_TL_LOD) {
// HACK: very roughly eyeballed based on the carpets in Defection
// this is actually supposed to be calculated per pixel
const float distance_frac = std::max(0.f, std::min(w / 1024.f, 1.f));
tmp.r = tmp.g = tmp.b = tmp.a = (0.7f + distance_frac * 0.3f) * 255.f;
} else {
tmp.r = tmp.g = tmp.b = tmp.a = 255;
}
color = &tmp;
break;
}
case G_ACMUX_PRIM_LOD_FRAC:
tmp.a = rdp.prim_lod_fraction;
color = &tmp;
break;
default:
memset(&tmp, 0, sizeof(tmp));
color = &tmp;
break;
}
if (k == 0) {
buf_vbo[buf_vbo_len++] = color->r / 255.0f;
buf_vbo[buf_vbo_len++] = color->g / 255.0f;
buf_vbo[buf_vbo_len++] = color->b / 255.0f;
} else {
buf_vbo[buf_vbo_len++] = color->a / 255.0f;
}
}
}
}
if (++buf_vbo_num_tris == MAX_BUFFERED) {
gfx_flush();
}
}
static inline void gfx_sp_tri4(Gfx *cmd) {
// the game issues gSPTri2 for quads, which uses G_TRI4 with 2 empty triangles
uint8_t x = C1(0, 4);
uint8_t y = C1(4, 4);
uint8_t z = C0(0, 4);
if(x || y || z) {
gfx_sp_tri1(x, y, z, false);
}
x = C1(8, 4);
y = C1(12, 4);
z = C0(4, 4);
if (x || y || z) {
gfx_sp_tri1(x, y, z, false);
}
x = C1(16, 4);
y = C1(20, 4);
z = C0(8, 4);
if (x || y || z) {
gfx_sp_tri1(x, y, z, false);
}
x = C1(24, 4);
y = C1(28, 4);
z = C0(12, 4);
if (x || y || z) {
gfx_sp_tri1(x, y, z, false);
}
}
static void gfx_sp_geometry_mode(uint32_t clear, uint32_t set) {
rsp.geometry_mode &= ~clear;
rsp.geometry_mode |= set;
}
static inline void gfx_update_aspect_mode(void) {
const uint32_t side = rsp.aspect_mode & G_ASPECT_CENTER_EXT;
rsp.aspect_scale = rsp.aspect_mode ? gfx_current_native_aspect : gfx_current_window_dimensions.aspect_ratio;
if (side == G_ASPECT_LEFT_EXT) {
rsp.aspect_ofs = 1.f - gfx_current_dimensions.aspect_ratio / gfx_current_native_aspect;
} else if (side == G_ASPECT_RIGHT_EXT) {
rsp.aspect_ofs = gfx_current_dimensions.aspect_ratio / gfx_current_native_aspect - 1.f;
} else {
rsp.aspect_ofs = 0.f;
}
if (side && (rsp.aspect_mode & G_ASPECT_WIDE_EXT)) {
constexpr float c = 16.f / 9.f;
if (gfx_current_dimensions.aspect_ratio > c) {
rsp.aspect_ofs *= c / gfx_current_dimensions.aspect_ratio;
}
}
}
static void gfx_sp_extra_geometry_mode(uint32_t clear, uint32_t set) {
rsp.extra_geometry_mode &= ~clear;
rsp.extra_geometry_mode |= set;
rsp.aspect_mode = (rsp.extra_geometry_mode & G_ASPECT_MODE_EXT);
gfx_update_aspect_mode();
}
static void gfx_adjust_viewport_or_scissor(XYWidthHeight* area, bool preserve_aspect = false) {
// HACK: assume all target framebuffers have the same aspect
// Use floor/ceil to ensure scissor fully contains the logical region
// and prevents sub-pixel gaps at viewport edges
float x1 = area->x * RATIO_X;
float y1 = (SCREEN_HEIGHT - area->y) * RATIO_Y;
float x2 = (area->x + area->width) * RATIO_X;
float y2 = (SCREEN_HEIGHT - area->y + area->height) * RATIO_Y;
area->x = std::floor(x1);
area->y = std::floor(y1);
area->width = std::ceil(x2) - area->x;
area->height = std::ceil(y2) - area->y;
if (preserve_aspect) {
// preserve native aspect ratio
const float ratio = gfx_current_native_aspect / gfx_current_dimensions.aspect_ratio;
const float midx = gfx_current_dimensions.width * 0.5f;
area->x = midx + (area->x - midx) * ratio;
area->x += rsp.aspect_ofs * gfx_current_dimensions.width * 0.5f;
area->width *= ratio;
}
if (!game_renders_to_framebuffer ||
(gfx_msaa_level > 1 && gfx_current_dimensions.width == gfx_current_game_window_viewport.width &&
gfx_current_dimensions.height == gfx_current_game_window_viewport.height)) {
area->x += gfx_current_game_window_viewport.x;
area->y += gfx_current_window_dimensions.height -
(gfx_current_game_window_viewport.y + gfx_current_game_window_viewport.height);
}
}
static void gfx_calc_and_set_viewport(const Vp_t* viewport) {
// 2 bits fraction
float width = 2.0f * viewport->vscale[0] / 4.0f;
float height = 2.0f * viewport->vscale[1] / 4.0f;
float x = (viewport->vtrans[0] / 4.0f) - width / 2.0f;
float y = ((viewport->vtrans[1] / 4.0f) + height / 2.0f);
rdp.viewport.x = x;
rdp.viewport.y = y;
rdp.viewport.width = width;
rdp.viewport.height = height;
gfx_adjust_viewport_or_scissor(&rdp.viewport);
rdp.viewport_or_scissor_changed = true;
}
static void gfx_sp_movemem(uint8_t index, uint8_t offset, const void* data) {
if (!fast3d_ptr_ok(data)) {
return; /* D146: corrupt DL -> wild pointer */
}
switch (index) {
case G_MV_VIEWPORT:
gfx_calc_and_set_viewport((const Vp_t*)data);
break;
case G_MV_LOOKATY:
case G_MV_LOOKATX:
// I think this is only really used for guLookAtReflect
index = !((index - G_MV_LOOKATY) / 2);
rsp.lookat[index] = ((const Light *)data)->l;
rsp.lookat_enabled = (index == 0) || (rsp.lookat[1].dir[0] || rsp.lookat[1].dir[1]);
rsp.lights_changed = true;
break;
case G_MV_L0:
case G_MV_L1:
case G_MV_L2:
// NOTE: reads out of bounds if it is an ambient light
memcpy(rsp.current_lights + (index - G_MV_L0) / 2, data, sizeof(Light_t));
break;
}
}
static void gfx_sp_moveword(uint8_t index, uint16_t offset, uintptr_t data) {
switch (index) {
case G_MW_NUMLIGHT:
// Ambient light is included
// The 31th bit is a flag that lights should be recalculated
rsp.current_num_lights = (data - 0x80000000U) / 32;
rsp.lights_changed = 1;
break;
case G_MW_FOG:
rsp.fog_mul = (int16_t)(data >> 16);
rsp.fog_offset = (int16_t)data;
break;
case G_MW_SEGMENT:
// GE registers segment bases as OS_K0_TO_PHYSICAL(ptr); store the
// live host pointer so seg_addr() resolves seg+offset correctly.
segmentPointers[(offset >> 2) & 0xff] = (data < 0x800000) ? (data + 0x80000000) : data;
break;
}
}
static void gfx_sp_texture(uint16_t sc, uint16_t tc, uint8_t level, uint8_t tile, uint8_t on) {
rsp.texture_scaling_factor.s = sc;
rsp.texture_scaling_factor.t = tc;
rdp.tex_max_lod = level;
if (rdp.first_tile_index != tile) {
rdp.textures_changed[0] = true;
rdp.textures_changed[1] = true;
rdp.first_tile_index = tile;
}
}
static void gfx_dp_set_scissor(uint32_t mode, uint32_t ulx, uint32_t uly, uint32_t lrx, uint32_t lry) {
float x = ulx / 4.0f;
float y = lry / 4.0f;
float width = (lrx - ulx) / 4.0f;
float height = (lry - uly) / 4.0f;
rdp.scissor.x = x;
rdp.scissor.y = y;
rdp.scissor.width = width;
rdp.scissor.height = height;
gfx_adjust_viewport_or_scissor(&rdp.scissor, rsp.aspect_mode != 0);
rdp.viewport_or_scissor_changed = true;
}
static void gfx_dp_set_texture_image(uint32_t format, uint32_t size, uint32_t width, uint32_t tex_flags, const void* addr) {
rdp.texture_to_load.addr = (const uint8_t*)addr;
rdp.texture_to_load.siz = size;
rdp.texture_to_load.width = width;
rdp.texture_to_load.tex_flags = tex_flags;
}
static void gfx_dp_set_tile(uint8_t fmt, uint32_t siz, uint32_t line, uint32_t tmem, uint8_t tile, uint32_t palette,
uint32_t cmt, uint32_t maskt, uint32_t shiftt, uint32_t cms, uint32_t masks,
uint32_t shifts) {
// OTRTODO:
// SUPPORT_CHECK(tmem == 0 || tmem == 256);
static uint32_t max_tmem = 0;
if (cms == G_TX_WRAP && masks == G_TX_NOMASK) {
cms = G_TX_CLAMP;
}
if (cmt == G_TX_WRAP && maskt == G_TX_NOMASK) {
cmt = G_TX_CLAMP;
}
if (fmt == G_IM_FMT_RGBA && siz < G_IM_SIZ_16b) {
// HACK: sometimes the game will submit G_IM_FMT_RGBA, G_IM_SIZ_8b/4b, intending it to read as CI8/CI4 with RGBA16 palette
fmt = G_IM_FMT_CI;
} else if (fmt == G_IM_FMT_IA && siz == G_IM_SIZ_32b) {
// HACK: ... and sometimes it submits this, apparently intending it to be I8
fmt = G_IM_FMT_I;
siz = G_IM_SIZ_8b;
}
rdp.texture_tile[tile].palette = palette; // palette should set upper 4 bits of color index in 4b mode
rdp.texture_tile[tile].fmt = fmt;
rdp.texture_tile[tile].siz = siz;
rdp.texture_tile[tile].cms = cms;
rdp.texture_tile[tile].cmt = cmt;
rdp.texture_tile[tile].masks = masks; /* RC3 */
rdp.texture_tile[tile].maskt = maskt; /* RC3 */
rdp.texture_tile[tile].shifts = shifts;
rdp.texture_tile[tile].shiftt = shiftt;
rdp.texture_tile[tile].line_size_bytes = line * 8;
rdp.texture_tile[tile].tmem = tmem;
rdp.textures_changed[0] = true;
rdp.textures_changed[1] = true;
}
static void gfx_dp_set_tile_size(uint8_t tile, uint16_t uls, uint16_t ult, uint16_t lrs, uint16_t lrt) {
rdp.texture_tile[tile].uls = uls;
rdp.texture_tile[tile].ult = ult;
rdp.texture_tile[tile].lrs = lrs;
rdp.texture_tile[tile].lrt = lrt;
rdp.texture_tile[tile].width = (lrs - uls + 4) / 4;
rdp.texture_tile[tile].height = (lrt - ult + 4) / 4;
rdp.textures_changed[0] = true;
rdp.textures_changed[1] = true;
}
static void gfx_dp_load_tlut(uint8_t tile, uint32_t uls, uint32_t ult, uint32_t lrs, uint32_t lrt) {
// SUPPORT_CHECK(tile == G_TX_LOADTILE);
SUPPORT_CHECK(rdp.texture_to_load.siz == G_IM_SIZ_16b);
SUPPORT_CHECK(rdp.texture_tile[tile].tmem >= 256);
rdp.texture_tile[tile].uls = uls;
rdp.texture_tile[tile].ult = ult;
rdp.texture_tile[tile].lrs = lrs;
rdp.texture_tile[tile].lrt = lrt;
const uint32_t width = (lrs - uls + 1);
const uint32_t height = (lrt - ult + 1);
const uint32_t pitch = rdp.texture_to_load.width + 1;
const uint32_t count = width * height;
const uint16_t *base = (const uint16_t *)rdp.texture_to_load.addr + pitch * ult + uls;
if (rdp.texture_tile[tile].tmem == 256) {
rdp.palette_addrs[0] = (const uint8_t *)base;
if (count >= 256) {
rdp.palette_addrs[1] = (const uint8_t *)(base + 128);
}
} else {
rdp.palette_addrs[1] = (const uint8_t *)base;
}
const uint32_t palofs = rdp.texture_tile[tile].tmem - 256;
SUPPORT_CHECK(palofs + count <= 256);
const uint16_t *src = base;
uint16_t *dst = rdp.palette + palofs;
for (uint32_t i = 0; i < count; ++i) {
*dst++ = PD_BE16(*src++);
}
rdp.textures_changed[0] = rdp.textures_changed[1] = true;
}
static void gfx_dp_load_block(uint8_t tile, uint32_t uls, uint32_t ult, uint32_t lrs, uint32_t dxt) {
// SUPPORT_CHECK(tile == G_TX_LOADTILE);
SUPPORT_CHECK(uls == 0);
SUPPORT_CHECK(ult == 0);
// The lrs field rather seems to be number of pixels to load
uint32_t orig_size_bytes = (lrs + 1) << rdp.texture_to_load.siz >> 1;
uint32_t size_bytes = orig_size_bytes;
if (rdp.texture_to_load.raw_tex_metadata.h_byte_scale != 1 ||
rdp.texture_to_load.raw_tex_metadata.v_pixel_scale != 1) {
size_bytes *= rdp.texture_to_load.raw_tex_metadata.h_byte_scale;
size_bytes *= rdp.texture_to_load.raw_tex_metadata.v_pixel_scale;
}
LoadedTexture& loaded_texture = rdp.loaded_texture[rdp.texture_tile[tile].tmem];
loaded_texture.orig_size_bytes = orig_size_bytes;
loaded_texture.size_bytes = size_bytes;
loaded_texture.full_size_bytes = size_bytes;
loaded_texture.line_size_bytes = size_bytes;
loaded_texture.full_image_line_size_bytes = size_bytes;
loaded_texture.tex_flags = rdp.texture_to_load.tex_flags;
loaded_texture.raw_tex_metadata = rdp.texture_to_load.raw_tex_metadata;
loaded_texture.addr = rdp.texture_to_load.addr;
rdp.textures_changed[0] = rdp.textures_changed[1] = true;
}
static void gfx_dp_load_tile(uint8_t tile, uint32_t uls, uint32_t ult, uint32_t lrs, uint32_t lrt) {
SUPPORT_CHECK(tile == G_TX_LOADTILE);
uint32_t offset_x = uls >> G_TEXTURE_IMAGE_FRAC;
uint32_t offset_y = ult >> G_TEXTURE_IMAGE_FRAC;
uint32_t tile_width = ((lrs - uls) >> G_TEXTURE_IMAGE_FRAC) + 1;
uint32_t tile_height = ((lrt - ult) >> G_TEXTURE_IMAGE_FRAC) + 1;
uint32_t full_image_width = rdp.texture_to_load.width + 1;
uint32_t offset_x_in_bytes = offset_x << rdp.texture_to_load.siz >> 1;
uint32_t tile_line_size_bytes = tile_width << rdp.texture_to_load.siz >> 1;
uint32_t full_image_line_size_bytes = full_image_width << rdp.texture_to_load.siz >> 1;
uint32_t orig_size_bytes = tile_line_size_bytes * tile_height;
uint32_t size_bytes = orig_size_bytes;
uint32_t start_offset_bytes = full_image_line_size_bytes * offset_y + offset_x_in_bytes;
float h_byte_scale = rdp.texture_to_load.raw_tex_metadata.h_byte_scale;
float v_pixel_scale = rdp.texture_to_load.raw_tex_metadata.v_pixel_scale;
if (h_byte_scale != 1 || v_pixel_scale != 1) {
start_offset_bytes = h_byte_scale * (v_pixel_scale * offset_y * full_image_line_size_bytes + offset_x_in_bytes);
size_bytes *= h_byte_scale * v_pixel_scale;
full_image_line_size_bytes *= h_byte_scale;
tile_line_size_bytes *= h_byte_scale;
}
LoadedTexture& loaded_texture = rdp.loaded_texture[rdp.texture_tile[tile].tmem];
loaded_texture.orig_size_bytes = orig_size_bytes;
loaded_texture.size_bytes = size_bytes;
loaded_texture.full_size_bytes = full_image_line_size_bytes * tile_height;
loaded_texture.full_image_line_size_bytes = full_image_line_size_bytes;
loaded_texture.line_size_bytes = tile_line_size_bytes;
loaded_texture.tex_flags = rdp.texture_to_load.tex_flags;
loaded_texture.raw_tex_metadata = rdp.texture_to_load.raw_tex_metadata;
loaded_texture.addr = rdp.texture_to_load.addr + start_offset_bytes;
rdp.texture_tile[tile].uls = uls;
rdp.texture_tile[tile].ult = ult;
rdp.texture_tile[tile].lrs = lrs;
rdp.texture_tile[tile].lrt = lrt;
rdp.texture_tile[tile].width = ((lrs - uls) >> G_TEXTURE_IMAGE_FRAC) + 1;
rdp.texture_tile[tile].height = ((lrt - ult) >> G_TEXTURE_IMAGE_FRAC) + 1;
rdp.textures_changed[0] = rdp.textures_changed[1] = true;
}
static void gfx_dp_set_combine_mode(uint32_t rgb, uint32_t alpha, uint32_t rgb_cyc2, uint32_t alpha_cyc2) {
rdp.combine_mode = rgb | (alpha << 16) | ((uint64_t)rgb_cyc2 << 28) | ((uint64_t)alpha_cyc2 << 44);
}
static inline uint32_t color_comb(uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
return (a & 0xf) | ((b & 0xf) << 4) | ((c & 0x1f) << 8) | ((d & 7) << 13);
}
static inline uint32_t alpha_comb(uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
return (a & 7) | ((b & 7) << 3) | ((c & 7) << 6) | ((d & 7) << 9);
}
static void gfx_dp_set_grayscale_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
rdp.grayscale_color.r = r;
rdp.grayscale_color.g = g;
rdp.grayscale_color.b = b;
rdp.grayscale_color.a = a;
}
static void gfx_dp_set_env_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
rdp.env_color.r = r;
rdp.env_color.g = g;
rdp.env_color.b = b;
rdp.env_color.a = a;
}
static void gfx_dp_set_prim_color(uint8_t m, uint8_t l, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
rdp.prim_lod_fraction = l;
rdp.prim_color.r = r;
rdp.prim_color.g = g;
rdp.prim_color.b = b;
rdp.prim_color.a = a;
rdp.fill_color.r = r;
rdp.fill_color.g = g;
rdp.fill_color.b = b;
rdp.fill_color.a = a;
rdp.tex_min_lod = m;
}
static void gfx_dp_set_fog_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
rdp.fog_color.r = r;
rdp.fog_color.g = g;
rdp.fog_color.b = b;
rdp.fog_color.a = a;
}
static void gfx_dp_set_fill_color(uint32_t packed_color) {
uint16_t col16 = (uint16_t)packed_color;
uint32_t r = col16 >> 11;
uint32_t g = (col16 >> 6) & 0x1f;
uint32_t b = (col16 >> 1) & 0x1f;
uint32_t a = col16 & 1;
rdp.fill_color.r = SCALE_5_8(r);
rdp.fill_color.g = SCALE_5_8(g);
rdp.fill_color.b = SCALE_5_8(b);
rdp.fill_color.a = a * 255;
}
static void gfx_dp_set_subpixel_offset(int16_t x, int16_t y) {
rdp.subpixel_ofs_x = x;
rdp.subpixel_ofs_y = y;
}
static void gfx_draw_rectangle(int32_t ulx, int32_t uly, int32_t lrx, int32_t lry) {
uint32_t saved_other_mode_h = rdp.other_mode_h;
uint32_t cycle_type = (rdp.other_mode_h & (3U << G_MDSFT_CYCLETYPE));
if (cycle_type == G_CYC_COPY) {
rdp.other_mode_h = (rdp.other_mode_h & ~(3U << G_MDSFT_TEXTFILT)) | G_TF_POINT;
}
ulx += rdp.subpixel_ofs_x;
lrx += rdp.subpixel_ofs_x;
uly += rdp.subpixel_ofs_y;
lry += rdp.subpixel_ofs_y;
// U10.2 coordinates
float ulxf = ulx;
float ulyf = uly;
float lrxf = lrx;
float lryf = lry;
ulxf = ulxf / (4.0f * HALF_SCREEN_WIDTH) - 1.0f;
ulyf = -(ulyf / (4.0f * HALF_SCREEN_HEIGHT)) + 1.0f;
lrxf = lrxf / (4.0f * HALF_SCREEN_WIDTH) - 1.0f;
lryf = -(lryf / (4.0f * HALF_SCREEN_HEIGHT)) + 1.0f;
ulxf = gfx_adjust_x_for_aspect_ratio(ulxf);
lrxf = gfx_adjust_x_for_aspect_ratio(lrxf);
struct LoadedVertex* ul = &rsp.loaded_vertices[MAX_VERTICES + 0];
struct LoadedVertex* ll = &rsp.loaded_vertices[MAX_VERTICES + 1];
struct LoadedVertex* lr = &rsp.loaded_vertices[MAX_VERTICES + 2];
struct LoadedVertex* ur = &rsp.loaded_vertices[MAX_VERTICES + 3];
ul->x = ulxf;
ul->y = ulyf;
ul->z = -1.0f;
ul->w = 1.0f;
ll->x = ulxf;
ll->y = lryf;
ll->z = -1.0f;
ll->w = 1.0f;
lr->x = lrxf;
lr->y = lryf;
lr->z = -1.0f;
lr->w = 1.0f;
ur->x = lrxf;
ur->y = ulyf;
ur->z = -1.0f;
ur->w = 1.0f;
// The coordinates for texture rectangle shall bypass the viewport setting
struct XYWidthHeight default_viewport = { 0, (int16_t)SCREEN_HEIGHT, (uint32_t)SCREEN_WIDTH, (uint32_t)SCREEN_HEIGHT };
struct XYWidthHeight viewport_saved = rdp.viewport;
uint32_t geometry_mode_saved = rsp.geometry_mode;
gfx_adjust_viewport_or_scissor(&default_viewport);
rdp.viewport = default_viewport;
rdp.viewport_or_scissor_changed = true;
rsp.geometry_mode = 0;
gfx_sp_tri1(MAX_VERTICES + 0, MAX_VERTICES + 1, MAX_VERTICES + 3, true);
gfx_sp_tri1(MAX_VERTICES + 1, MAX_VERTICES + 2, MAX_VERTICES + 3, true);
rsp.geometry_mode = geometry_mode_saved;
rdp.viewport = viewport_saved;
rdp.viewport_or_scissor_changed = true;
if (cycle_type == G_CYC_COPY) {
rdp.other_mode_h = saved_other_mode_h;
}
}
static void gfx_dp_texture_rectangle(int32_t ulx, int32_t uly, int32_t lrx, int32_t lry, uint8_t tile, int16_t uls,
int16_t ult, int16_t dsdx, int16_t dtdy, bool flip) {
uint64_t saved_combine_mode = rdp.combine_mode;
if ((rdp.other_mode_h & (3U << G_MDSFT_CYCLETYPE)) == G_CYC_COPY) {
// Per RDP Command Summary Set Tile's shift s and this dsdx should be set to 4 texels
// Divide by 4 to get 1 instead
dsdx >>= 2;
// Color combiner is turned off in copy mode
gfx_dp_set_combine_mode(color_comb(0, 0, 0, G_CCMUX_TEXEL0), alpha_comb(0, 0, 0, G_ACMUX_TEXEL0), 0, 0);
// Per documentation one extra pixel is added in this modes to each edge
lrx += 1 << 2;
lry += 1 << 2;
}
// uls and ult are S10.5
// dsdx and dtdy are S5.10
// lrx, lry, ulx, uly are U10.2
// lrs, lrt are S10.5
const int16_t width = flip ? lry - uly : lrx - ulx;
const int16_t height = flip ? lrx - ulx : lry - uly;
const float lrs = ((uls << 7) + dsdx * width) >> 7;
const float lrt = ((ult << 7) + dtdy * height) >> 7;
struct LoadedVertex* ul = &rsp.loaded_vertices[MAX_VERTICES + 0];
struct LoadedVertex* ll = &rsp.loaded_vertices[MAX_VERTICES + 1];
struct LoadedVertex* lr = &rsp.loaded_vertices[MAX_VERTICES + 2];
struct LoadedVertex* ur = &rsp.loaded_vertices[MAX_VERTICES + 3];
ul->u = uls;
ul->v = ult;
lr->u = lrs;
lr->v = lrt;
if (!flip) {
ll->u = uls;
ll->v = lrt;
ur->u = lrs;
ur->v = ult;
} else {
ll->u = lrs;
ll->v = ult;
ur->u = uls;
ur->v = lrt;
}
{
static int ge_d116 = -1;
if (ge_d116 < 0) ge_d116 = getenv("GE_D116") ? 1 : 0;
if (ge_d116) {
const auto& tt = rdp.texture_tile[tile];
fprintf(stderr,
"[D116/f3d] tile=%d flip=%d ul(%d,%d) lr(%d,%d) uls=%d ult=%d dsdx=%d dtdy=%d "
"-> lrs=%.2f lrt=%.2f ul.u=%.2f lr.u=%.2f | TILE siz=%d fmt=%d line_bytes=%d "
"uls=%d lrs=%d width=%d height=%d cms=%d\n",
tile, (int)flip, ulx, uly, lrx, lry, (int)uls, (int)ult, (int)dsdx, (int)dtdy,
lrs, lrt, ul->u, lr->u,
tt.siz, tt.fmt, tt.line_size_bytes, tt.uls, tt.lrs, tt.width, tt.height, tt.cms);
}
}
uint8_t saved_tile = rdp.first_tile_index;
if (saved_tile != tile) {
rdp.textures_changed[0] = true;
rdp.textures_changed[1] = true;
}
rdp.first_tile_index = tile;
gfx_draw_rectangle(ulx, uly, lrx, lry);
if (saved_tile != tile) {
rdp.textures_changed[0] = true;
rdp.textures_changed[1] = true;
}
rdp.first_tile_index = saved_tile;
rdp.combine_mode = saved_combine_mode;
}
static void gfx_dp_image_rectangle(int32_t tile, int32_t w, int32_t h,
int32_t ulx, int32_t uly, int16_t uls, int16_t ult,
int32_t lrx, int32_t lry, int16_t lrs, int16_t lrt) {
uint64_t saved_combine_mode = rdp.combine_mode;
struct LoadedVertex* ul = &rsp.loaded_vertices[MAX_VERTICES + 0];
struct LoadedVertex* ll = &rsp.loaded_vertices[MAX_VERTICES + 1];
struct LoadedVertex* lr = &rsp.loaded_vertices[MAX_VERTICES + 2];
struct LoadedVertex* ur = &rsp.loaded_vertices[MAX_VERTICES + 3];
ul->u = uls * 32;
ul->v = ult * 32;
lr->u = lrs * 32;
lr->v = lrt * 32;
ll->u = uls * 32;
ll->v = lrt * 32;
ur->u = lrs * 32;
ur->v = ult * 32;
// ensure we have the correct texture size
rdp.texture_tile[tile].line_size_bytes = w << rdp.texture_tile[tile].siz >> 1;
rdp.texture_tile[tile].width = w;
rdp.texture_tile[tile].height = h;
rdp.texture_tile[tile].cms = 0;
rdp.texture_tile[tile].cmt = 0;
rdp.texture_tile[tile].shifts = 0;
rdp.texture_tile[tile].shiftt = 0;
auto& loadtex = rdp.loaded_texture[rdp.texture_tile[tile].tmem];
loadtex.full_image_line_size_bytes = loadtex.line_size_bytes = rdp.texture_tile[tile].line_size_bytes;
loadtex.size_bytes = loadtex.orig_size_bytes = loadtex.full_size_bytes = loadtex.line_size_bytes * h;
uint8_t saved_tile = rdp.first_tile_index;
if (saved_tile != tile) {
rdp.textures_changed[0] = true;
rdp.textures_changed[1] = true;
}
rdp.first_tile_index = tile;
gfx_draw_rectangle(ulx, uly, lrx, lry);
if (saved_tile != tile) {
rdp.textures_changed[0] = true;
rdp.textures_changed[1] = true;
}
rdp.first_tile_index = saved_tile;
rdp.combine_mode = saved_combine_mode;
}
static void gfx_dp_fill_rectangle(int32_t ulx, int32_t uly, int32_t lrx, int32_t lry) {
if (rdp.color_image_address == rdp.z_buf_address) {
// Don't clear Z buffer here since we already did it with glClear
return;
}
uint32_t mode = (rdp.other_mode_h & (3U << G_MDSFT_CYCLETYPE));
// OTRTODO: This is a bit of a hack for widescreen screen fades, but it'll work for now...
if (ulx == 0 && uly == 0 && lrx == 319 * 4 && lry == 239 * 4) {
ulx = -1024;
uly = -1024;
lrx = 2048;
lry = 2048;
}
if (mode == G_CYC_COPY || mode == G_CYC_FILL) {
// Per documentation one extra pixel is added in this modes to each edge
lrx += 1 << 2;
lry += 1 << 2;
}
for (int i = MAX_VERTICES; i < MAX_VERTICES + 4; i++) {
struct LoadedVertex* v = &rsp.loaded_vertices[i];
v->color = rdp.fill_color;
}
uint64_t saved_combine_mode = rdp.combine_mode;
if (mode == G_CYC_FILL) {
gfx_dp_set_combine_mode(color_comb(0, 0, 0, G_CCMUX_SHADE), alpha_comb(0, 0, 0, G_ACMUX_SHADE), 0, 0);
}
gfx_draw_rectangle(ulx, uly, lrx, lry);
rdp.combine_mode = saved_combine_mode;
}
static void gfx_dp_set_z_image(void* z_buf_address) {
rdp.z_buf_address = z_buf_address;
}
static void gfx_dp_set_color_image(uint32_t format, uint32_t size, uint32_t width, void* address) {
rdp.color_image_address = address;
}
static void gfx_sp_set_other_mode(uint32_t shift, uint32_t num_bits, uint64_t mode) {
uint64_t mask = (((uint64_t)1 << num_bits) - 1) << shift;
uint64_t om = rdp.other_mode_l | ((uint64_t)rdp.other_mode_h << 32);
om = (om & ~mask) | mode;
rdp.other_mode_l = (uint32_t)om;
rdp.other_mode_h = (uint32_t)(om >> 32);
rdp.palette_fmt = rdp.other_mode_h & (3U << G_MDSFT_TEXTLUT);
rdp.tex_lod = (rdp.other_mode_h & G_TL_LOD) != 0;
rdp.tex_detail = (rdp.other_mode_h & (2U << G_MDSFT_TEXTDETAIL)) == G_TD_DETAIL;
}
static void gfx_sp_set_vertex_colors(uint32_t count, const struct NormalColor *vcn) {
// common sense dictates that we should copy the colors as the command is supposed to do,
// but it actually doesn't seem to matter
// SUPPORT_CHECK(count <= sizeof(rsp.vertex_colors) / sizeof(rsp.vertex_colors[0]));
// for (uint32_t i = 0; i < count; ++i) {
// rsp.vertex_colors[i] = vcn[i];
// }
if (fast3d_ptr_ok(vcn)) { /* D146: ignore a wild pointer from a corrupt DL */
rsp.vertex_colors = vcn;
}
}
static void gfx_dp_set_other_mode(uint32_t h, uint32_t l) {
rdp.other_mode_h = h;
rdp.other_mode_l = l;
}
static inline void *seg_addr(uintptr_t w1) {
// GE model files reference GDLs (gSPDisplayList) and vertex arrays by raw
// VMA 0x05xxxxxx WITHOUT the LSB set; segment 5 is set per-render to the
// live host file base by the game's gSPSegment. Resolve it explicitly
// before the segmented-address path below (a converter-remapped seg-5 w1
// already carries the LSB and takes that path).
if ((w1 & 0xFF000000) == 0x05000000 && segmentPointers[5]) {
return (void *)(segmentPointers[5] + (w1 & 0x00FFFFFF));
}
// all segmented addresses have the least significant bit set
if (w1 & 1) {
// seg 0 is reserved and doesn't count here
const uintptr_t seg = (w1 & 0x0f000000) >> 24;
if (seg && segmentPointers[seg]) {
const uintptr_t addr = (w1 & 0x00fffffe);
return (void *)(segmentPointers[seg] + addr);
}
}
// GE's ROM GDLs also carry UNMARKED segmented refs with the LSB clear:
// G_MTX w1=0x03xxxxxx (seg 3 = render_pos), G_VTX w1=0x04/0x05xxxxxx
// (seg 4 = runtime vtx buffer, seg 5 = file base), G_SETTIMG w1=0x05xxxxxx
// (embedded image blob). Convention: segment in bits 24-27, 24-bit offset.
// Runtime pointers never land here: DRAM lives at >= 0x70000000 and
// K0-physical values are < 0x800000 (nibble 24 == 0).
if (w1 < 0x10000000 && ((w1 >> 24) & 0xf)) {
const uintptr_t seg = (w1 >> 24) & 0xf;
if (segmentPointers[seg]) {
return (void *)(segmentPointers[seg] + (w1 & 0x00FFFFFF));
}
}
// GE passes OS_K0_TO_PHYSICAL(ptr) == ptr - 0x80000000 for RAM that lives
// in the reserved N64-DRAM region (port/src/dram.c); map it back. The
// region is 8 MB, so any offset below 0x800000 came from there.
if (w1 < 0x800000) {
return (void *)(w1 + 0x80000000);
}
// D131: a GBI DL built by game code can reference a COMPILED symbol via
// osVirtualToPhysical() (a u32-returning shim), which truncates the
// module's 0x1_00000000 high word. Seen in explosionRenderPropSmoke:
// gSPMatrix(gdl++, osVirtualToPhysical((void*)&dword_CODE_bss_8007A100),
// ..MODELVIEW) -> w1 == 0x40xxxxxx -> wild deref in gfx_sp_matrix. The
// module is based at 0x140000000 (fixed, no ASLR), and nothing legit
// reaches this fallthrough with a value in [0x40000000, DRAM_V1): DRAM
// (>=0x70000000), KSEG0 (>=0x80000000), segmented addrs and sub-0x800000
// physical offsets are all handled above. Restore the high word from
// this TU's own load address.
{
static const uintptr_t mod_hi =
((uintptr_t)(void *)&segmentPointers[0]) & 0xffffffff00000000ULL;
if (mod_hi && w1 >= 0x40000000 && w1 < 0x70000000) {
return (void *)(mod_hi | w1);
}
}
return (void *)w1;
}
uintptr_t clearMtx;
static void gfx_run_dl(Gfx* cmd) {
// puts("dl");
int dummy = 0;
char dlName[128];
const char* fileName;
Gfx* dListStart = cmd;
uint64_t ourHash = -1;
for (;;) {
uint32_t opcode = cmd->words.w0 >> 24;
// gfx_print_cmd(cmd);
switch (opcode) {
// RSP commands:
case G_NOOP: /* 0xc0. GE's gbi_extension.h also names this slot
G_SETTEX (gsSPUseTexture); the game never emits it
(finding B1), so treating it as a no-op is safe. */
break;
case G_MTX: {
gfx_sp_matrix(C0(16, 8), (const int32_t*)seg_addr(cmd->words.w1));
break;
}
case (uint8_t)G_POPMTX:
gfx_sp_pop_matrix(1);
break;
case G_MOVEMEM:
gfx_sp_movemem(C0(16, 8), 0, seg_addr(cmd->words.w1));
break;
case (uint8_t)G_MOVEWORD:
gfx_sp_moveword(C0(0, 8), C0(8, 16), cmd->words.w1);
break;
case (uint8_t)G_TEXTURE:
gfx_sp_texture(C1(16, 16), C1(0, 16), C0(11, 3), C0(8, 3), C0(0, 8));
break;
case G_VTX:
gfx_sp_vertex(C0(0, 16) / sizeof(Vtx), C0(16, 4), (const Vtx*)seg_addr(cmd->words.w1));
break;
case G_DL: {
if (C0(16, 1) == 0) {
// Push return address
Gfx* subGFX = (Gfx*)seg_addr(cmd->words.w1);
if (subGFX != nullptr) {
gfx_run_dl(subGFX);
}
} else {
cmd = (Gfx*)seg_addr(cmd->words.w1);
--cmd; // increase after break
}
break;
}
case (uint8_t)G_ENDDL:
return;
case (uint8_t)G_SETGEOMETRYMODE:
gfx_sp_geometry_mode(0, cmd->words.w1);
break;
case (uint8_t)G_CLEARGEOMETRYMODE:
gfx_sp_geometry_mode(cmd->words.w1, 0);
break;
case G_EXTRAGEOMETRYMODE_EXT:
gfx_sp_extra_geometry_mode(~C0(0, 24), cmd->words.w1);
break;
case (uint8_t)G_TRI1:
gfx_sp_tri1(C1(16, 8) / 10, C1(8, 8) / 10, C1(0, 8) / 10, false);
break;
case (uint8_t)G_TRI4:
gfx_sp_tri4(cmd);
break;
case (uint8_t)G_SETOTHERMODE_L:
gfx_sp_set_other_mode(C0(8, 8), C0(0, 8), cmd->words.w1);
break;
case (uint8_t)G_SETOTHERMODE_H:
gfx_sp_set_other_mode(C0(8, 8) + 32, C0(0, 8), (uint64_t)cmd->words.w1 << 32);
break;
case G_COL:
gfx_sp_set_vertex_colors(C0(0, 16) / 4, (NormalColor *)seg_addr(cmd->words.w1));
break;
// RDP Commands:
case G_SETTIMG: {
gfx_dp_set_texture_image(C0(21, 3), C0(19, 2), C0(0, 10), 0, seg_addr(cmd->words.w1));
break;
}
case G_SETTIMG_FB_EXT:
gfx_flush();
gfx_rapi->select_texture_fb(cmd->words.w1);
rdp.textures_changed[0] = false;
rdp.textures_changed[1] = false;
break;
case G_SETGRAYSCALE_EXT:
rdp.grayscale = cmd->words.w1;
break;
case G_LOADBLOCK:
gfx_dp_load_block(C1(24, 3), C0(12, 12), C0(0, 12), C1(12, 12), C1(0, 12));
break;
case G_LOADTILE:
gfx_dp_load_tile(C1(24, 3), C0(12, 12), C0(0, 12), C1(12, 12), C1(0, 12));
break;
case G_SETTILE:
gfx_dp_set_tile(C0(21, 3), C0(19, 2), C0(9, 9), C0(0, 9), C1(24, 3), C1(20, 4), C1(18, 2), C1(14, 4),
C1(10, 4), C1(8, 2), C1(4, 4), C1(0, 4));
break;
case G_SETTILESIZE:
gfx_dp_set_tile_size(C1(24, 3), C0(12, 12), C0(0, 12), C1(12, 12), C1(0, 12));
break;
case G_LOADTLUT:
gfx_dp_load_tlut(C1(24, 3), C0(14, 10), C0(2, 10), C1(14, 10), C1(2, 10));
break;
case G_SETENVCOLOR:
gfx_dp_set_env_color(C1(24, 8), C1(16, 8), C1(8, 8), C1(0, 8));
break;
case G_SETPRIMCOLOR:
gfx_dp_set_prim_color(C0(8, 8), C0(0, 8), C1(24, 8), C1(16, 8), C1(8, 8), C1(0, 8));
break;
case G_SETFOGCOLOR:
gfx_dp_set_fog_color(C1(24, 8), C1(16, 8), C1(8, 8), C1(0, 8));
break;
case G_SETFILLCOLOR:
gfx_dp_set_fill_color(cmd->words.w1);
break;
case G_SETINTENSITY_EXT:
gfx_dp_set_grayscale_color(C1(24, 8), C1(16, 8), C1(8, 8), C1(0, 8));
break;
case G_SETCOMBINE:
#ifdef PORT
/* D172 probe (env-gated, inert): log every SETCOMBINE with the
* cycle-type active at that moment. Particle records
* (explosion.c g_ExplosionDisplayLists[]) set a 2-cycle
* combine but never set cycletype - this tells us what
* cycletype fast3d has when they replay. */
if (getenv("GE_D172")) {
static uint64_t d172seen[64];
static int d172cnt = 0;
uint64_t key = ((uint64_t)(uint32_t)cmd->words.w0 << 32) | (uint32_t)cmd->words.w1;
bool dup = false;
for (int k = 0; k < d172cnt; k++) if (d172seen[k] == key) { dup = true; break; }
if (!dup && d172cnt < 64) {
d172seen[d172cnt++] = key;
uint32_t ct = (rdp.other_mode_h >> G_MDSFT_CYCLETYPE) & 3;
sysLogPrintf(LOG_NOTE,
"D172: SETCOMBINE w0=%08x w1=%08x cycletype=%u (%s)",
(uint32_t)cmd->words.w0, (uint32_t)cmd->words.w1,
ct, ct == 0 ? "1CYC" : ct == 1 ? "2CYC" : ct == 2 ? "COPY" : "FILL");
}
}
#endif
gfx_dp_set_combine_mode(color_comb(C0(20, 4), C1(28, 4), C0(15, 5), C1(15, 3)),
alpha_comb(C0(12, 3), C1(12, 3), C0(9, 3), C1(9, 3)),
color_comb(C0(5, 4), C1(24, 4), C0(0, 5), C1(6, 3)),
alpha_comb(C1(21, 3), C1(3, 3), C1(18, 3), C1(0, 3)));
break;
// G_SETPRIMCOLOR, G_CCMUX_PRIMITIVE, G_ACMUX_PRIMITIVE, is used by Goddard
// G_CCMUX_TEXEL1, LOD_FRACTION is used in Bowser room 1
case G_SETSUBPIXELOFFSET_EXT: {
gfx_dp_set_subpixel_offset(C0(0, 16), C1(0, 16));
break;
}
case G_TEXRECT:
case G_TEXRECTFLIP: {
int32_t lrx, lry, tile, ulx, uly;
uint32_t uls, ult, dsdx, dtdy;
lrx = C0(12, 12);
lry = C0(0, 12);
tile = C1(24, 3);
ulx = C1(12, 12);
uly = C1(0, 12);
++cmd;
uls = C1(16, 16);
ult = C1(0, 16);
++cmd;
dsdx = C1(16, 16);
dtdy = C1(0, 16);
gfx_dp_texture_rectangle(ulx, uly, lrx, lry, tile, uls, ult, dsdx, dtdy, opcode == G_TEXRECTFLIP);
break;
}
case G_FILLRECT:
gfx_dp_fill_rectangle(C1(12, 12), C1(0, 12), C0(12, 12), C0(0, 12));
break;
case G_FILLRECT_WIDE_EXT: {
int32_t lrx, lry, ulx, uly;
lrx = (int32_t)(C0(0, 24) << 8) >> 8;
lry = (int32_t)(C1(0, 24) << 8) >> 8;
++cmd;
ulx = (int32_t)(C0(0, 24) << 8) >> 8;
uly = (int32_t)(C1(0, 24) << 8) >> 8;
gfx_dp_fill_rectangle(ulx, uly, lrx, lry);
break;
}
case G_TEXRECT_WIDE_EXT: {
int32_t lrx, lry, tile, ulx, uly;
uint32_t uls, ult, dsdx, dtdy;
bool flip;
lrx = (int32_t)((C0(0, 24) << 8)) >> 8;
lry = (int32_t)((C1(0, 24) << 8)) >> 8;
tile = C1(24, 3);
flip = C1(27, 1);
++cmd;
ulx = (int32_t)((C0(0, 24) << 8)) >> 8;
uly = (int32_t)((C1(0, 24) << 8)) >> 8;
++cmd;
uls = C0(16, 16);
ult = C0(0, 16);
dsdx = C1(16, 16);
dtdy = C1(0, 16);
gfx_dp_texture_rectangle(ulx, uly, lrx, lry, tile, uls, ult, dsdx, dtdy, flip);
break;
}
case G_IMAGERECT_EXT: {
int16_t tile, iw, ih;
int16_t x0, y0, s0, t0;
int16_t x1, y1, s1, t1;
tile = C0(0, 3);
iw = C1(16, 16);
ih = C1(0, 16);
++cmd;
x0 = C0(16, 16);
y0 = C0(0, 16);
s0 = C1(16, 16);
t0 = C1(0, 16);
++cmd;
x1 = C0(16, 16);
y1 = C0(0, 16);
s1 = C1(16, 16);
t1 = C1(0, 16);
gfx_dp_image_rectangle(tile, iw, ih, x0, y0, s0, t0, x1, y1, s1, t1);
break;
}
case G_SETSCISSOR:
gfx_dp_set_scissor(C1(24, 2), C0(12, 12), C0(0, 12), C1(12, 12), C1(0, 12));
break;
case G_SETZIMG:
gfx_dp_set_z_image(seg_addr(cmd->words.w1));
break;
case G_SETCIMG:
gfx_dp_set_color_image(C0(21, 3), C0(19, 2), C0(0, 11), seg_addr(cmd->words.w1));
break;
case G_SETFB_EXT:
gfx_flush();
if (cmd->words.w1) {
// don't care about noise here
gfx_set_framebuffer(cmd->words.w1, 1.f);
fbActive = true;
} else {
gfx_reset_framebuffer();
fbActive = false;
}
break;
case G_COPYFB_EXT:
gfx_copy_framebuffer(C0(11, 11), C0(0, 11), (int16_t)C1(16, 16), (int16_t)C1(0, 16), C0(22, 1));
break;
case G_RDPSETOTHERMODE:
gfx_dp_set_other_mode(C0(0, 24), cmd->words.w1);
break;
case G_INVALTEXCACHE_EXT:
if (cmd->words.w1) {
gfx_texture_cache_delete((const uint8_t *)seg_addr(cmd->words.w1));
} else {
gfx_texture_cache_clear();
}
break;
case (uint8_t)G_RDPHALF_1:
case (uint8_t)G_RDPHALF_2:
case (uint8_t)G_RDPHALF_CONT:
// on N64 skyRender uses these to render some types of skies and skybox water
// by issuing low-level ucode commands G_TRI_FILL and G_TRI_SHADE_TXTR
// the port renders the sky in a different manner
break;
case G_RDPFLUSH_EXT:
gfx_flush();
break;
case G_CLEAR_DEPTH_EXT:
gfx_flush();
gfx_rapi->clear_framebuffer(false, true);
break;
case G_RDPPIPESYNC:
case G_RDPFULLSYNC:
case G_RDPLOADSYNC:
case G_RDPTILESYNC:
break;
default: {
(void)dListStart;
/* D146: an unknown opcode means the DL walk has desynced or
* this DL is garbage (a front-end 3D model whose display list
* was never built - the D75 / D144 family - lives at a valid
* DRAM address full of junk). Aborting the whole process over
* one bad menu model is the wrong trade for a breadth-first
* port: end this (sub-)DL and let the frame finish. Still
* logged loudly, rate-limited, so a real level-render desync
* is not hidden. */
{
static int warned = 0;
if (warned < 20) {
warned++;
sysLogPrintf(LOG_ERROR,
"D146: unknown GBI opcode 0x%02x at %p (w0=%llx w1=%llx) - ending DL",
opcode, (void *)cmd,
(unsigned long long)cmd->words.w0,
(unsigned long long)cmd->words.w1);
}
}
return;
}
}
++cmd;
}
}
static void gfx_sp_reset() {
rsp.modelview_matrix_stack_size = 1;
rsp.current_num_lights = 2;
rsp.lights_changed = true;
}
extern "C" void gfx_get_dimensions(uint32_t* width, uint32_t* height, int32_t* posX, int32_t* posY) {
gfx_wapi->get_dimensions(width, height, posX, posY);
}
extern "C" void gfx_init(const GfxInitSettings *settings) {
gfx_wapi = settings->wapi;
gfx_rapi = settings->rapi;
gfx_wapi->init(&settings->window_settings);
gfx_rapi->init();
gfx_rapi->update_framebuffer_parameters(0, settings->window_settings.width, settings->window_settings.height, 1, false, true, true, true);
gfx_current_dimensions.internal_mul = 1;
gfx_current_game_window_viewport.width = gfx_current_dimensions.width = settings->window_settings.width;
gfx_current_game_window_viewport.height = gfx_current_dimensions.height = settings->window_settings.height;
game_framebuffer = gfx_rapi->create_framebuffer();
game_framebuffer_msaa_resolved = gfx_rapi->create_framebuffer();
if (gfx_msaa_level > 1 && !gfx_framebuffers_enabled) {
sysLogPrintf(LOG_WARNING, "F3D: MSAA set to %d, but framebuffers are not available; disabling", gfx_msaa_level);
gfx_msaa_level = 1;
}
for (int i = 0; i < 16; i++) {
segmentPointers[i] = 0;
}
if (tex_upload_buffer == nullptr) {
// We cap texture max to 8k, because why would you need more?
int max_tex_size = std::min(8192, gfx_rapi->get_max_texture_size());
tex_upload_buffer = (uint8_t*)malloc(max_tex_size * max_tex_size * 4);
}
/* D72: N64 boots with RSP memory zeroed — no lookat until gSPLookAt. */
rsp.lookat_enabled = false;
}
extern "C" void gfx_destroy(void) {
// TODO: should also destroy rapi and wapi, and any other resources acquired in fast3d
// Texture cache and loaded textures store references to Resources which need to be unreferenced.
gfx_texture_cache_clear();
}
extern "C" struct GfxRenderingAPI* gfx_get_current_rendering_api(void) {
return gfx_rapi;
}
extern "C" void gfx_start_frame(void) {
gfx_wapi->handle_events();
gfx_wapi->get_dimensions(&gfx_current_window_dimensions.width, &gfx_current_window_dimensions.height,
&gfx_current_window_position_x, &gfx_current_window_position_y);
if (gfx_current_window_dimensions.height == 0) {
// Avoid division by zero
gfx_current_window_dimensions.height = 1;
}
gfx_current_window_dimensions.aspect_ratio = (float)gfx_current_window_dimensions.width / gfx_current_window_dimensions.height;
gfx_current_dimensions = gfx_current_window_dimensions;
gfx_current_game_window_viewport.width = gfx_current_dimensions.width;
gfx_current_game_window_viewport.height = gfx_current_dimensions.height;
if (gfx_current_dimensions.height != gfx_prev_dimensions.height) {
for (auto& fb : framebuffers) {
uint32_t width, height, msaa;
if (fb.second.autoresize) {
if (fb.second.upscale) {
width = fb.second.orig_width;
height = fb.second.orig_height;
gfx_adjust_width_height_for_scale(width, height);
} else {
// assume this is a fullscreen fb
width = gfx_current_dimensions.width;
height = gfx_current_dimensions.height;
}
if (width != fb.second.applied_width || height != fb.second.applied_height) {
gfx_rapi->update_framebuffer_parameters(fb.first, width, height, 1, true, true, true, true);
fb.second.applied_width = width;
fb.second.applied_height = height;
}
}
}
}
gfx_prev_dimensions = gfx_current_dimensions;
bool different_size = gfx_current_dimensions.width != gfx_current_game_window_viewport.width ||
gfx_current_dimensions.height != gfx_current_game_window_viewport.height;
if (gfx_framebuffers_enabled && (different_size || gfx_msaa_level > 1)) {
game_renders_to_framebuffer = true;
if (different_size) {
gfx_rapi->update_framebuffer_parameters(game_framebuffer, gfx_current_dimensions.width,
gfx_current_dimensions.height, gfx_msaa_level, true, true, true,
true);
} else {
// MSAA framebuffer needs to be resolved to an equally sized target when complete, which must therefore
// match the window size
gfx_rapi->update_framebuffer_parameters(game_framebuffer, gfx_current_window_dimensions.width,
gfx_current_window_dimensions.height, gfx_msaa_level, false, true,
true, true);
}
if (gfx_msaa_level > 1 && different_size) {
gfx_rapi->update_framebuffer_parameters(game_framebuffer_msaa_resolved, gfx_current_dimensions.width,
gfx_current_dimensions.height, 1, false, false, false, false);
}
} else {
game_renders_to_framebuffer = false;
}
fbActive = 0;
// update aspect scale and offset
gfx_update_aspect_mode();
}
uint32_t num_dls = 0;
/* F10 port-layer options overlay (port/src/optionsoverlay.c). Returns a
* self-contained 2D display list to draw on top of the game's frame, or NULL
* when the overlay is closed -- in which case nothing is appended and the
* frame is byte-identical to before (golden dumps unaffected). */
extern "C" Gfx* optionsOverlayEmit(void);
extern "C" void gfx_run(Gfx* commands) {
++num_dls;
gfx_sp_reset();
// puts("New frame");
if (!gfx_wapi->start_frame()) {
dropped_frame = true;
return;
}
dropped_frame = false;
gfx_rapi->update_framebuffer_parameters(0, gfx_current_window_dimensions.width,
gfx_current_window_dimensions.height, 1, false, true, true,
!game_renders_to_framebuffer);
gfx_rapi->start_frame();
gfx_rapi->start_draw_to_framebuffer(game_renders_to_framebuffer ? game_framebuffer : 0,
(float)gfx_current_dimensions.height / SCREEN_HEIGHT);
gfx_rapi->clear_framebuffer(true, false);
rdp.viewport_or_scissor_changed = true;
rendering_state.viewport = {};
rendering_state.scissor = {};
gfx_run_dl(commands);
{
Gfx* overlay = optionsOverlayEmit();
if (overlay != nullptr) {
gfx_run_dl(overlay);
}
}
gfx_flush();
gfxFramebuffer = 0;
if (game_renders_to_framebuffer) {
gfx_rapi->start_draw_to_framebuffer(0, 1);
gfx_rapi->clear_framebuffer(true, true);
if (gfx_msaa_level > 1) {
bool different_size = gfx_current_dimensions.width != gfx_current_game_window_viewport.width ||
gfx_current_dimensions.height != gfx_current_game_window_viewport.height;
if (different_size) {
gfx_rapi->resolve_msaa_color_buffer(game_framebuffer_msaa_resolved, game_framebuffer);
gfxFramebuffer = (uintptr_t)gfx_rapi->get_framebuffer_texture_id(game_framebuffer_msaa_resolved);
} else {
gfx_rapi->resolve_msaa_color_buffer(0, game_framebuffer);
}
} else {
gfxFramebuffer = (uintptr_t)gfx_rapi->get_framebuffer_texture_id(game_framebuffer);
}
}
gfx_rapi->end_frame();
gfx_wapi->swap_buffers_begin();
}
extern "C" void gfx_end_frame(void) {
if (!dropped_frame) {
gfx_rapi->finish_render();
gfx_wapi->swap_buffers_end();
}
}
extern "C" void gfx_set_target_fps(int fps) {
gfx_wapi->set_target_fps(fps);
}
extern "C" void reset_texture_state() {
gfx_texture_cache_clear();
if (rendering_state.shader_program) {
gfx_rapi->unload_shader(rendering_state.shader_program);
rendering_state.shader_program = nullptr;
}
gfx_rapi->clear_shaders();
color_combiner_pool.clear();
prev_combiner = color_combiner_pool.end();
}
extern "C" void gfx_set_texture_filter(enum FilteringMode mode) {
reset_texture_state();
gfx_rapi->set_texture_filter(mode);
}
extern "C" void gfx_set_mipmap_filter(enum MipmapFilteringMode mode) {
reset_texture_state();
gfx_rapi->set_mipmap_filter(mode);
}
extern "C" void gfx_set_fix_mip_textures(int on) { g_fix_mip_textures = !!on; }
extern "C" void gfx_set_wrap_fix(int on) {
const char* e = getenv("GE_WRAPFIX"); /* RC3 test override */
g_wrap_fix = e ? (atoi(e) != 0) : !!on;
}
extern "C" int gfx_create_framebuffer(uint32_t width, uint32_t height, int upscale, int autoresize) {
int fb = gfx_rapi->create_framebuffer();
gfx_resize_framebuffer(fb, width, height, upscale, autoresize);
return fb;
}
extern "C" void gfx_resize_framebuffer(int fb, uint32_t width, uint32_t height, int upscale, int autoresize) {
uint32_t orig_width, orig_height;
if (width && height) {
// user-specified size
orig_width = width;
orig_height = height;
if (upscale) {
gfx_adjust_width_height_for_scale(width, height);
}
gfx_rapi->update_framebuffer_parameters(fb, width, height, 1, true, true, true, true);
} else {
// same size as main fb
orig_width = width = gfx_current_dimensions.width;
orig_height = height = gfx_current_dimensions.height;
upscale = false;
autoresize = true;
gfx_rapi->update_framebuffer_parameters(fb, width, height, 1, true, true, true, true);
}
framebuffers[fb] = { orig_width, orig_height, width, height, (bool)upscale, (bool)autoresize };
}
extern "C" void gfx_set_framebuffer(int fb, float noise_scale) {
gfx_rapi->start_draw_to_framebuffer(fb, noise_scale);
gfx_rapi->clear_framebuffer(true, true);
active_fb = framebuffers.find(fb);
}
extern "C" void gfx_copy_framebuffer(int fb_dst, int fb_src, int left, int top, int use_back) {
const bool is_main_fb = (fb_src == 0);
if (is_main_fb) {
if (left > 0 && top > 0) {
// upscale the position
left = left * gfx_current_dimensions.width / gfx_current_native_viewport.width;
top = top * gfx_current_dimensions.height / gfx_current_native_viewport.height;
// flip Y
top = gfx_current_dimensions.height - top - 1;
}
if (use_back && gfx_msaa_level > 1) {
// read from the framebuffer we've been rendering to
fb_src = game_framebuffer;
}
}
gfx_rapi->copy_framebuffer(fb_dst, fb_src, left, top, is_main_fb, (bool)use_back);
}
extern "C" void gfx_reset_framebuffer(void) {
gfx_rapi->start_draw_to_framebuffer(0, (float)gfx_current_dimensions.height / SCREEN_HEIGHT);
active_fb = framebuffers.end();
}