/** * Generated data tables for BG1 rotation * * The trickiest issue to address is that, when calculating the rotation indices, at * a 45 degree angle, the mapped address for a fixed rectangle can be outside of the * pixel buffer. To fix this we make a compromise. * * To keep speed, image data is drawn one word at a time, so the effective width of the * rotation is 82 units wide. Since each work contains 4 pixels, we will only rotate * a quarter of the vertical image -- an effective 52 units -- and display the same offset * for four consecutive lines. * * Further, the image data will be the center of the BG1 buffer, so the middle 52 lines. * * When rotating we may still calculate address "outside" of the buffer by a factor of * sqrt(2) (~40%) -- or 32 words horizontally and 21 lines vertically. There is extra * data vertically to fill this and, since the BG1 buffer is stored with a stride of * 256 bytes (128 words) there are an extra 46 words of empty space that can be zeroed * out or filled with content to improve the rotation visuals. */ const fs = require('fs').promises; const process = require('process'); const { Buffer } = require('buffer'); // When calculating addresses, do we use the floor() function or round()? const USE_FLOOR = true; const NUM_ANGLES = 64; const BUFFER_HEIGHT = 208; const BUFFER_WIDTH = 164; // In bytes const BUFFER_STRIDE = 256; // In bytes const TEXTURE_WIDTH = BUFFER_WIDTH / 2; // Full width (in words) const TEXTURE_HEIGHT = BUFFER_HEIGHT / 4; // Quarter height const TEXTURE_STRIDE = BUFFER_STRIDE; const BUFFER_START = 0x1800; const BUFFER_END = BUFFER_START + BUFFER_STRIDE * BUFFER_HEIGHT; console.log(`; The BG1 buffer lives at [${toHex(BUFFER_START)}, ${toHex(BUFFER_END)}]`); // The texture portion of BG starts at the left edge of line 77 and // extends down to line const TEXTURE_START = BUFFER_STRIDE * (BUFFER_HEIGHT - TEXTURE_HEIGHT) / 2; const TEXTURE_END = BUFFER_STRIDE * (BUFFER_HEIGHT + TEXTURE_HEIGHT) / 2; const TEXTURE_CENTER = BUFFER_START + TEXTURE_START + Math.floor(TEXTURE_HEIGHT / 2) * BUFFER_STRIDE + Math.floor(BUFFER_WIDTH / 2); console.log(`; The texture is this range of the BG1 buffer [${toHex(TEXTURE_START)}, ${toHex(TEXTURE_END)}]`); // Define some other constants const x_half = Math.floor(TEXTURE_WIDTH / 2); const y_half = Math.floor(TEXTURE_HEIGHT / 2); // Calculate some bias values to keep everything positive BIAS_X = Math.floor(TEXTURE_CENTER / 2) + 0x200; BIAS_Y = TEXTURE_CENTER - BIAS_X; const angles = Array.from({ length: NUM_ANGLES}).map((x, i) => (i * 2 * Math.PI) / NUM_ANGLES); main(process.argv.slice(2)).then( () => process.exit(0), (e) => { console.error(e); process.exit(1); } ); function toHex(n) { return '$' + n.toString(16).toUpperCase().padStart(4, '0'); } function f_x(x, angle) { // Calculate x in units of bytes // return Math.floor(a(x - x_half, angle)) + x_half + BIAS_X; return a(x - x_half, angle) + BIAS_X; } function f_y(y, angle) { // return Math.floor(b(y - y_half, angle)) + (y_half * TEXTURE_STRIDE) + BIAS_Y; return b(y - y_half, angle) + BIAS_Y; } function check_sample(_a, x, y) { const angle = angles[_a]; const degrees = Math.round(360 * angle / (2 * Math.PI)); const fx = f_x(x, angle); const fy = f_y(y, angle); const ptr = fx + fy; if (fx < 0 || fy < 0 || ptr < 0x1800 || ptr >= 0xE800) { console.log(`(a = ${degrees}, x = ${x}, y = ${y}) : f_x = ${toHex(fx)}, f_y = ${toHex(fy)}, p = ${toHex(ptr)}`); process.exit(); } } async function main(argv) { // Inspired by https://www.youtube.com/watch?v=glWIf0gfWSE&t=1196s // // We will support 64 rotation angles (~5.5 degree increments) which gives nice // power-of-2 values from the common angles or 45, 90, 135, etc. // Do a brute force check to make sure that we can generate addresses that stay within // a proper range for (let a = 0; a < NUM_ANGLES; a += 1) { for (let x = 0; x < TEXTURE_WIDTH; x += 1) { for (let y = 0; y < TEXTURE_HEIGHT; y += 1) { check_sample(a, x, y); } } const degrees = Math.round(360 * angles[a] / (2 * Math.PI)); } // Now generate the tables to stdout as merlin source code const _ = console.log; _("ANGLEBNK\tENT"); _("x_angles\tENT"); for (let a = 0; a < NUM_ANGLES; a += 1) { _(`\tdw\t:x_a_${a}`); } for (let a = 0; a < NUM_ANGLES; a += 1) { const angle = angles[a]; const label = `:x_a_${a}`; const fx = []; for (let x = 0; x < TEXTURE_WIDTH; x += 1) { fx.push(f_x(x, angle)); } const arr = fx.map(toHex).join(','); // Double every array for fast copies _(`${label}\tdw\t${arr}`); _(`\tdw\t${arr}`); } _("y_angles\tENT"); for (let a = 0; a < NUM_ANGLES; a += 1) { _(`\tdw\t:y_a_${a}`); } for (let a = 0; a < NUM_ANGLES; a += 1) { const angle = angles[a]; const label = `:y_a_${a}`; const fy = []; for (let y = 0; y < TEXTURE_HEIGHT; y += 1) { const value = f_y(y, angle); fy.push(value); } const arr = fy.map(toHex).join(','); // Double every array for fast output _(`${label}\tdw\t${arr}`); _(`\tdw\t${arr}`); } } function a(x, angle) { if (USE_FLOOR) { return (Math.floor(2 * x * Math.cos(angle)) & ~1) + Math.floor(x * Math.sin(angle)) * TEXTURE_STRIDE; } return (Math.round(2 * x * Math.cos(angle)) & ~1) + Math.round(x * Math.sin(angle)) * TEXTURE_STRIDE; } function b(y, angle) { if (USE_FLOOR) { return Math.floor(y * Math.cos(angle)) * TEXTURE_STRIDE - (Math.floor(2 * y * Math.sin(angle)) & ~1); } return Math.round(y * Math.cos(angle)) * TEXTURE_STRIDE - (Math.round(2 * y * Math.sin(angle)) & ~1); }