The GS TEX0 register set, and why TBP0 is region-relative
Picking up "the GS register set the level load builds from the texture entry" from level-assets.md, for a renderer that emulates the GS rather than resolving texture addresses on the host. gc_formats::gs gains tex0_of (TBP0, TBW, TW, TH, PSM), texture_slot_bytes and clut_base. Three measurements back them, over all 27 levels and 1,595 textures. Every dimension is a power of two: 1,595 textures, zero exceptions, in four classes -- 128x128 (1,249), 64x64 (303), 256x256 (22), 32x32 (21). The GS addresses textures by exponent, so TW and TH are log2 of the dimensions, and a texture that was not a power of two could not be addressed at all. That the whole population is one is the measurand for the decode. TBP0 is region-relative. 19 of 27 levels have a textures_base only 64-byte aligned -- mod 256 reads 0, 64, 128 or 192 -- while every entry's own data_offset is 256-aligned on all 1,595. A base pointer computed from core_data would therefore not be a whole number of 256-byte blocks on those levels. The first version of tex0_of took textures_base + data_offset and rejected 1,168 of 1,595 textures for exactly this reason, which is how the finding surfaced. The slot is level 0 plus one mip level: the stride between consecutive entries is w*h + (w/2)*(h/2) on 1,560 of the 1,564 ascending gaps -- 32x32 gives 1,280, 64x64 gives 5,120, 128x128 gives 20,480, 256x256 gives 81,920. One byte per texel. `ty` reads 3 on all 1,595, so it is not the stored level count. Four gaps break the rule and four more run backwards. Both are reported as bounded exceptions rather than folded into an average: the four ascending outliers are 712,960 to 1,064,960 bytes, two orders of magnitude past any slot the game allocates, so they are boundaries the slot rule does not describe rather than slots that came out different. The four reverse gaps mean the table is not sorted by data_offset, which a consumer walking the region in table order would trip over. The CLUT address is the established palette * 0x100, restated as clut_base because a GS emulator asks for it as TEXCLUT's CBP. Still open after this: TEXCLUT's own fields, MIPTBP1/2 wiring, and how the ad-gifs' A+D values relate to this base. Suite with the disc: 70 disc tests, 160 gc-formats, 22 gc-trace, 23 gc-engine, 19 gc-game. Clippy clean.
This commit is contained in:
@@ -5051,6 +5051,163 @@ fn occ(hay: &[u8], needle: &[u8]) -> Vec<usize> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every level texture decodes to a GS TEX0 register.
|
||||||
|
///
|
||||||
|
/// The GS addresses textures by exponent, so TW and TH are `log2` of the
|
||||||
|
/// dimensions and a texture that is not a power of two cannot be addressed at
|
||||||
|
/// all. Measured over the whole disc: 1,595 textures, all powers of two, and
|
||||||
|
/// every `data_offset` 256-aligned so the base pointer is a whole number.
|
||||||
|
#[test]
|
||||||
|
fn every_level_texture_decodes_to_a_gs_tex0() {
|
||||||
|
let Some(data) = extracted_root() else {
|
||||||
|
eprintln!("skipped: unpack the disc first");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
require_disc!();
|
||||||
|
|
||||||
|
use gc_formats::gs::{log2_exact, tex0_of, texture_slot_bytes};
|
||||||
|
use gc_formats::level_core::{texture_table, LevelCoreHeader};
|
||||||
|
|
||||||
|
let mut total = 0usize;
|
||||||
|
let mut decoded = 0usize;
|
||||||
|
let mut misaligned = 0usize;
|
||||||
|
let mut nonpow2 = 0usize;
|
||||||
|
let mut slot_exact = 0usize;
|
||||||
|
let mut slot_checked = 0usize;
|
||||||
|
let mut descending = 0usize;
|
||||||
|
let mut oddities: Vec<(usize, i32, i16, i16, i64, i64)> = Vec::new();
|
||||||
|
let mut stride_hist: std::collections::BTreeMap<(i16, i16), usize> = Default::default();
|
||||||
|
|
||||||
|
for lv in 0..27usize {
|
||||||
|
let Ok(idx) = std::fs::read(data.join(format!("level{lv}/core_index.bin"))) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(h) = LevelCoreHeader::parse(&idx) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(entries) = texture_table(&idx, h.tfrag_textures) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// The region's own base is deliberately unused: TBP0 is
|
||||||
|
// region-relative, and 19 of 27 levels are only 64-byte aligned at
|
||||||
|
// `core_data`, so a data-relative base pointer would not be a whole
|
||||||
|
// 256-byte block. Asserted below.
|
||||||
|
let _ = h.textures_base;
|
||||||
|
for (k, e) in entries.iter().enumerate() {
|
||||||
|
if e.width <= 0 || e.height <= 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
total += 1;
|
||||||
|
// The power-of-two requirement, stated before the decode.
|
||||||
|
if log2_exact(e.width).is_none() || log2_exact(e.height).is_none() {
|
||||||
|
nonpow2 += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match tex0_of(e.data_offset, e.width, e.height) {
|
||||||
|
Some(t) => {
|
||||||
|
decoded += 1;
|
||||||
|
let expected_tbw = (e.width as i32) / 64;
|
||||||
|
assert_eq!(
|
||||||
|
t.tbw, expected_tbw,
|
||||||
|
"level{lv} texture {k}: TBW should be w/64"
|
||||||
|
);
|
||||||
|
let _ = t;
|
||||||
|
}
|
||||||
|
None => misaligned += 1,
|
||||||
|
}
|
||||||
|
// The slot size, against the stride to the next entry. Only
|
||||||
|
// ascending strides mean anything: eight entries across the disc
|
||||||
|
// are followed by one that is *earlier* in the region, so the
|
||||||
|
// table is not sorted by `data_offset` and those gaps are not
|
||||||
|
// strides. They are counted, not silently skipped.
|
||||||
|
if k + 1 < entries.len() {
|
||||||
|
let d = entries[k + 1].data_offset as i64 - e.data_offset as i64;
|
||||||
|
if d > 0 {
|
||||||
|
let want = texture_slot_bytes(e.width, e.height) as i64;
|
||||||
|
slot_checked += 1;
|
||||||
|
if d == want {
|
||||||
|
slot_exact += 1;
|
||||||
|
} else if oddities.len() < 12 {
|
||||||
|
oddities.push((lv, e.data_offset, e.width, e.height, d, want));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
descending += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*stride_hist.entry((e.width, e.height)).or_default() += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The level's own `textures_base` is 64-byte aligned on most levels, which
|
||||||
|
// is why TBP0 is region-relative rather than data-relative: a base pointer
|
||||||
|
// taken from `core_data` would not be a whole 256-byte block there.
|
||||||
|
let mut aligned64 = 0usize;
|
||||||
|
let mut levels = 0usize;
|
||||||
|
for lv in 0..27usize {
|
||||||
|
let Ok(idx) = std::fs::read(data.join(format!("level{lv}/core_index.bin"))) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(h) = LevelCoreHeader::parse(&idx) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(entries) = texture_table(&idx, h.tfrag_textures) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if entries.first().is_none_or(|e| e.data_offset < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
levels += 1;
|
||||||
|
if h.textures_base % 256 != 0 {
|
||||||
|
aligned64 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
levels >= 20,
|
||||||
|
"expected most levels to carry textures, got {levels}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
aligned64 * 2 > levels,
|
||||||
|
"the majority of levels should be unaligned at 256, which is why TBP0 is region-relative: \
|
||||||
|
{aligned64} of {levels}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bounds: 1,595 textures, 0 non-power-of-two, 0 unaligned.
|
||||||
|
assert_eq!(total, 1_595, "the disc's level texture count");
|
||||||
|
assert_eq!(nonpow2, 0, "every dimension should be a power of two");
|
||||||
|
assert_eq!(decoded, total, "every texture should decode");
|
||||||
|
assert_eq!(
|
||||||
|
misaligned, 0,
|
||||||
|
"every data_offset should be a 256-byte block, so TBP0 is whole"
|
||||||
|
);
|
||||||
|
for (lv, off, w, h, d, want) in &oddities {
|
||||||
|
eprintln!(" odd stride: level{lv} at {off} {w}x{h}: stride {d} but slot would be {want}");
|
||||||
|
}
|
||||||
|
// The slot is base plus one mip on every ascending consecutive pair bar
|
||||||
|
// four. Those four are gaps of 712,960 to 1,064,960 bytes -- two orders of
|
||||||
|
// magnitude past any slot this game allocates -- so they are not slots that
|
||||||
|
// came out different, they are boundaries the slot rule does not describe.
|
||||||
|
// Reported as a bounded exception rather than folded into an average.
|
||||||
|
assert_eq!(
|
||||||
|
slot_exact + oddities.len(),
|
||||||
|
slot_checked,
|
||||||
|
"every ascending stride should be either a slot or one of the four boundaries"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
oddities.len() <= 4,
|
||||||
|
"more than four non-slot gaps appeared: {:?}",
|
||||||
|
oddities
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
descending, 4,
|
||||||
|
"four reverse gaps are part of the population, not noise"
|
||||||
|
);
|
||||||
|
// And the four dimension classes are the ones measured.
|
||||||
|
assert!(
|
||||||
|
stride_hist.contains_key(&(128, 128)) && stride_hist.contains_key(&(64, 64)),
|
||||||
|
"the dominant classes should be 128 and 64 square"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The tfrag's clip cube lives at `+0x38`, and `+0x2e` is the mini-sphere
|
/// The tfrag's clip cube lives at `+0x38`, and `+0x2e` is the mini-sphere
|
||||||
/// array.
|
/// array.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -180,6 +180,97 @@ impl BlendMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GS TEX0_1, the texture-setup register, decoded from a level texture entry.
|
||||||
|
///
|
||||||
|
/// The GS stores a texture by *exponents*, not by dimensions: TW and TH hold
|
||||||
|
/// `log2(width)` and `log2(height)`, which is why every retail texture in this
|
||||||
|
/// game is a power of two -- **all 1,595 across the 27 levels, measured**. That
|
||||||
|
/// is the measurand behind this struct: `tex0_of` returning a TW that is not an
|
||||||
|
/// exact log2 would mean the entry was not a texture this register can address.
|
||||||
|
///
|
||||||
|
/// TBP0 is a **256-byte** block number, and it is relative to the start of the
|
||||||
|
/// level's texture region, not to the level's data: **19 of the 27 levels have
|
||||||
|
/// a `textures_base` that is only 64-byte aligned** (mod 256 reads 0, 64, 128
|
||||||
|
/// or 192), so a `core_data`-relative base pointer would not be a whole number
|
||||||
|
/// of blocks on those levels. Every entry's own `data_offset` *is* 256-aligned,
|
||||||
|
/// which is what the region-relative reading requires -- the slots advance by
|
||||||
|
/// `w*h + (w/2)*(h/2)` bytes, a multiple of 256 for every power-of-two
|
||||||
|
/// dimension the game uses.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct TEX0 {
|
||||||
|
/// Texture base pointer, in 256-byte blocks.
|
||||||
|
pub tbp0: i32,
|
||||||
|
/// Texture buffer width, in 64-texel blocks.
|
||||||
|
pub tbw: i32,
|
||||||
|
/// Pixel storage method. Every level texture is 8-bit indexed, so this is
|
||||||
|
/// the same constant everywhere; it is a field rather than a constant
|
||||||
|
/// because a renderer that emulates the GS needs it in the register.
|
||||||
|
pub psm: i32,
|
||||||
|
/// `log2(width)`.
|
||||||
|
pub tw: i32,
|
||||||
|
/// `log2(height)`.
|
||||||
|
pub th: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The one PSM this game's level textures use: 8-bit indices, texel-swizzled.
|
||||||
|
pub const PSM_PSMT8: i32 = 0x14;
|
||||||
|
|
||||||
|
/// Build TEX0 for a level texture entry.
|
||||||
|
///
|
||||||
|
/// `data_offset` is the entry's offset **within the texture region**. It is
|
||||||
|
/// passed rather than the region's own base because the base pointer is
|
||||||
|
/// region-relative -- see the note on [`TEX0`]. Returns `None` when the
|
||||||
|
/// dimensions are not powers of two, or when the offset is not a whole
|
||||||
|
/// 256-byte block, which are the two conditions this register cannot express.
|
||||||
|
#[must_use]
|
||||||
|
pub fn tex0_of(data_offset: i32, width: i16, height: i16) -> Option<TEX0> {
|
||||||
|
let tw = log2_exact(width)?;
|
||||||
|
let th = log2_exact(height)?;
|
||||||
|
// TBP0 is a 256-byte block count, so an unaligned base is not a base.
|
||||||
|
if data_offset < 0 || data_offset % 256 != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(TEX0 {
|
||||||
|
tbp0: data_offset / 256,
|
||||||
|
tbw: (width as i32) / 64,
|
||||||
|
psm: PSM_PSMT8,
|
||||||
|
tw,
|
||||||
|
th,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `log2(x)` when `x` is a power of two, else `None`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn log2_exact(x: i16) -> Option<i32> {
|
||||||
|
if x <= 0 || (x & (x - 1)) != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(x.trailing_zeros() as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bytes one level-texture slot occupies: level 0 plus one mip level.
|
||||||
|
///
|
||||||
|
/// Measured over all 1,595 textures on the 27 levels: the stride between
|
||||||
|
/// consecutive entries is `w*h + (w/2)*(h/2)` on every dimension class, where
|
||||||
|
/// the classes are 32, 64, 128 and 256 square. The game stores the base image
|
||||||
|
/// and one mip; `ty` reads 3 on all 1,595, so it is not the stored level count.
|
||||||
|
#[must_use]
|
||||||
|
pub fn texture_slot_bytes(width: i16, height: i16) -> i32 {
|
||||||
|
let w = width as i64;
|
||||||
|
let h = height as i64;
|
||||||
|
(w * h + (w / 2) * (h / 2)) as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The CLUT's byte offset in GS RAM: `palette * 0x100`.
|
||||||
|
///
|
||||||
|
/// The established reading (`gc_formats::texture`) is that the level texture's
|
||||||
|
/// CLUT lives at exactly that address. Restated here because a GS emulator asks
|
||||||
|
/// for it as TEXCLUT's CBP rather than as a lookup address.
|
||||||
|
#[must_use]
|
||||||
|
pub fn clut_base(palette: i16) -> i32 {
|
||||||
|
palette as i32 * 0x100
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -293,6 +384,40 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tex0_needs_powers_of_two() {
|
||||||
|
let t = tex0_of(0, 128, 128).expect("128 is a power of two");
|
||||||
|
assert_eq!(t.tw, 7, "log2(128)");
|
||||||
|
assert_eq!(t.th, 7);
|
||||||
|
assert_eq!(t.tbw, 2, "128 texels is two 64-texel blocks");
|
||||||
|
assert_eq!(t.psm, PSM_PSMT8);
|
||||||
|
assert_eq!(t.tbp0, 0);
|
||||||
|
assert!(tex0_of(0, 96, 128).is_none(), "96 is not a power of two");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tex0_base_pointer_is_256_byte_blocks() {
|
||||||
|
let t = tex0_of(512, 64, 64).expect("aligned");
|
||||||
|
assert_eq!(t.tbp0, 2, "512 bytes is two blocks");
|
||||||
|
assert_eq!(t.tbw, 1);
|
||||||
|
assert!(tex0_of(255, 64, 64).is_none(), "255 is not a block");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn texture_slots_carry_one_mip_level() {
|
||||||
|
// Measured strides: 32 -> 1280, 64 -> 5120, 128 -> 20480, 256 -> 81920.
|
||||||
|
assert_eq!(texture_slot_bytes(32, 32), 1280);
|
||||||
|
assert_eq!(texture_slot_bytes(64, 64), 5120);
|
||||||
|
assert_eq!(texture_slot_bytes(128, 128), 20480);
|
||||||
|
assert_eq!(texture_slot_bytes(256, 256), 81920);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_clut_base_is_the_texture_module_s_address() {
|
||||||
|
assert_eq!(clut_base(8), 2048);
|
||||||
|
assert_eq!(clut_base(0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clamp_keeps_the_far_edge() {
|
fn clamp_keeps_the_far_edge() {
|
||||||
assert_eq!(clamp01(1.0), 1.0, "1.0 is the edge, not a wrap to 0");
|
assert_eq!(clamp01(1.0), 1.0, "1.0 is the edge, not a wrap to 0");
|
||||||
|
|||||||
@@ -2570,7 +2570,46 @@ correct stride, which is not yet known. The LOD1/LOD2-sharing test
|
|||||||
(96.1% same class against 0.4% different) remains the strongest evidence that
|
(96.1% same class against 0.4% different) remains the strongest evidence that
|
||||||
the first slots are positional, and it is insensitive to stride.
|
the first slots are positional, and it is insensitive to stride.
|
||||||
|
|
||||||
## The tfrag clip cube is at `+0x38`, and `+0x2e` is the mini-sphere array
|
## The GS texture register set, and why TBP0 is region-relative
|
||||||
|
|
||||||
|
**Range measured:** all 27 levels, **1,595 level textures**.
|
||||||
|
|
||||||
|
`gc_formats::gs` gains the TEX0 register a GS emulator needs, decoded from a
|
||||||
|
texture entry: `tbp0`, `tbw`, `tw`, `th`, `psm`, plus `texture_slot_bytes` and
|
||||||
|
`clut_base`. Three measurements back the decode.
|
||||||
|
|
||||||
|
**Every dimension is a power of two.** 1,595 textures, **zero** exceptions, in
|
||||||
|
four classes: 128x128 (1,249), 64x64 (303), 256x256 (22), 32x32 (21). The GS
|
||||||
|
addresses textures by exponent, so TW and TH are `log2` of the dimensions and a
|
||||||
|
texture that was not a power of two could not be addressed at all. That the
|
||||||
|
whole population is one is the measurand for the decode.
|
||||||
|
|
||||||
|
**TBP0 is region-relative, not data-relative.** 19 of the 27 levels have a
|
||||||
|
`textures_base` that is only 64-byte aligned -- mod 256 reads 0, 64, 128 or 192
|
||||||
|
-- while every entry's own `data_offset` is 256-aligned on all 1,595. So a base
|
||||||
|
pointer computed from `core_data` would not be a whole number of 256-byte blocks
|
||||||
|
on those levels. The loader uploads the texture region to a GS-aligned base and
|
||||||
|
the pointer counts from there.
|
||||||
|
|
||||||
|
**The slot is level 0 plus one mip level.** The stride between consecutive
|
||||||
|
entries is `w*h + (w/2)*(h/2)` on 1,560 of the 1,564 ascending gaps: 32x32 gives
|
||||||
|
1,280, 64x64 gives 5,120, 128x128 gives 20,480, 256x256 gives 81,920. The
|
||||||
|
game stores the base image and one mip, at one byte per texel. `ty` reads **3 on
|
||||||
|
all 1,595**, so it is not the stored level count -- naming what it is instead of
|
||||||
|
calling it a mip count is the point.
|
||||||
|
|
||||||
|
Four gaps break the rule, and four more run backwards. They are reported as a
|
||||||
|
bounded exception rather than folded into an average: the four ascending
|
||||||
|
outliers are 712,960 to 1,064,960 bytes, two orders of magnitude past any slot
|
||||||
|
the game allocates, so they are boundaries the slot rule does not describe
|
||||||
|
rather than slots that came out different. The four reverse gaps mean the table
|
||||||
|
is not sorted by `data_offset`, which is itself worth knowing -- a consumer that
|
||||||
|
walks the region by table order would visit it out of sequence.
|
||||||
|
|
||||||
|
The CLUT address is the established `palette * 0x100`, restated as
|
||||||
|
`clut_base` because a GS emulator asks for it as TEXCLUT's CBP.
|
||||||
|
|
||||||
|
## The tfrag clip cube is at `+0x38`, and `+0x2e` is the mini-sphere array## The tfrag clip cube is at `+0x38`, and `+0x2e` is the mini-sphere array
|
||||||
|
|
||||||
**Range measured:** level 0, all 833 tfrag headers.
|
**Range measured:** level 0, all 833 tfrag headers.
|
||||||
|
|
||||||
|
|||||||
@@ -709,9 +709,15 @@ the fidelity rule.
|
|||||||
- **Point lights.** The directional pass runs; `point_light_vertex` and its
|
- **Point lights.** The directional pass runs; `point_light_vertex` and its
|
||||||
`r^2 - |p - L|^2` cull test are not written, and they are the one part of
|
`r^2 - |p - L|^2` cull test are not written, and they are the one part of
|
||||||
the lighting that *is* distance-dependent.
|
the lighting that *is* distance-dependent.
|
||||||
- **The GS register set** the level load builds from the texture entry
|
- ~~**The GS register set**~~ **Partly settled 2026-10-14.**
|
||||||
(TBP0/TBP1 and so on), for a renderer that emulates the GS rather than
|
`gc_formats::gs::tex0_of` decodes TEX0 (TBP0, TBW, TW, TH, PSM) from a texture
|
||||||
resolving texture addresses on the host.
|
entry, verified over all 1,595 textures on all 27 levels: every dimension is a
|
||||||
|
power of two, so the exponent fields are exact, and TBP0 is **region-relative**
|
||||||
|
because 19 of 27 levels have a `textures_base` only 64-byte aligned.
|
||||||
|
`texture_slot_bytes` is `w*h + (w/2)*(h/2)` -- level 0 plus one mip -- on
|
||||||
|
1,560 of 1,564 ascending gaps, with four bounded exceptions. Still open: the
|
||||||
|
**TEXCLUT** register (the CLUT's CBP/CSA/CSM), MIPTBP1/2 wiring, and the
|
||||||
|
A+D values the ad-gifs carry relative to this base.
|
||||||
- **The guard-band clip test** that sends patches crossing the guard band to
|
- **The guard-band clip test** that sends patches crossing the guard band to
|
||||||
the clipping program. The geometry side is now in place -- the 8-corner clip
|
the clipping program. The geometry side is now in place -- the 8-corner clip
|
||||||
cube at header `+0x38` is parsed and tested (`core-data-regions.md`, the
|
cube at header `+0x38` is parsed and tested (`core-data-regions.md`, the
|
||||||
|
|||||||
Reference in New Issue
Block a user