ref:e85f7bdd44c921da140d451852ba90ec8f60a2cd

Fix transparent background: revert Rust to RGBA byte order for memcpy

The ABGR byte order caused transparent backgrounds because NativeImage stores ints in little-endian: the ABGR int 0xFFBBGGRR becomes bytes [RR,GG,BB,FF] in memory (RGBA). The memcpy copied ABGR bytes directly, putting alpha=255 in the R position and R in the A position. Fix: Rust outputs RGBA bytes (the original format). This matches NativeImage's actual memory layout on little-endian systems, so memcpy produces correct results. The fallback per-pixel path swaps RGBA→ABGR for setPixelRGBA(). Also fixed: Matrix rain pixel order (RGBA), all Rust tests updated for RGBA byte positions. 52 Rust tests + 46 GameTests + visual test, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SHA: e85f7bdd44c921da140d451852ba90ec8f60a2cd
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-03-20 17:54
Parents: 7a2a4ef
5 files changed +60 -51
Type
rust/src/renderer.rs +37 −37
@@ -1,9 +1,9 @@
//! Terminal renderer module
//!
//! Renders the terminal grid to an ABGR pixel buffer using cached glyphs
//! Renders the terminal grid to an RGBA pixel buffer using cached glyphs
//! and proper ANSI color support from alacritty_terminal.
//!
//! The pixel buffer uses RGBA byte order (A at offset 0, B at 1, G at 2, R at 3)
//! The pixel buffer uses ABGR byte order (A at offset 0, B at 1, G at 2, R at 3)
//! to match Minecraft's NativeImage internal format, avoiding per-pixel conversion
//! on the Java side.
//!
@@ -12,7 +12,7 @@
use crate::glyph_cache::{GlyphCache, GlyphStyle, SubpixelOrder};
/// Color with f32 components in 0.0..1.0 (stored as r, g, b, a internally;
/// converted to ABGR byte order when written to the pixel buffer)
/// converted to RGBA byte order when written to the pixel buffer)
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: f32,
@@ -114,7 +114,7 @@
glyph_cache: GlyphCache,
/// Color palette
palette: ColorPalette,
/// Image buffer (RGBA pixels)
/// Image buffer (ABGR pixels)
pixel_buffer: Vec<u8>,
/// Image dimensions
width: u32,
@@ -429,7 +429,7 @@
}
}
/// Fill background with terminal background color (ABGR byte order)
/// Fill background with terminal background color (RGBA byte order for memcpy to NativeImage)
fn fill_background(&mut self) {
let bg = self.palette.background;
let r = (bg.r * 255.0) as u8;
@@ -437,14 +437,14 @@
let b = (bg.b * 255.0) as u8;
for i in (0..self.pixel_buffer.len()).step_by(4) {
self.pixel_buffer[i] = 255; // A
self.pixel_buffer[i] = r;
self.pixel_buffer[i + 1] = g;
self.pixel_buffer[i + 2] = b;
self.pixel_buffer[i + 3] = 255;
self.pixel_buffer[i + 1] = b; // B
self.pixel_buffer[i + 2] = g; // G
self.pixel_buffer[i + 3] = r; // R
}
}
/// Fill a rectangle with a color (RGBA byte order)
/// Fill a rectangle with a color (ABGR byte order)
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
let r = (color.r * 255.0) as u8;
let g = (color.g * 255.0) as u8;
@@ -454,10 +454,10 @@
for px in x.max(0)..(x + w).min(self.width as i32) {
let idx = ((py as u32 * self.width + px as u32) * 4) as usize;
if idx + 3 < self.pixel_buffer.len() {
self.pixel_buffer[idx] = r;
self.pixel_buffer[idx + 1] = g;
self.pixel_buffer[idx + 2] = b;
self.pixel_buffer[idx + 3] = 255;
self.pixel_buffer[idx] = 255; // A
self.pixel_buffer[idx + 1] = b; // B
self.pixel_buffer[idx + 2] = g; // G
self.pixel_buffer[idx + 3] = r; // R
}
}
}
@@ -478,7 +478,7 @@
}
}
/// Draw a grayscale anti-aliased glyph (ABGR byte order)
/// Draw a grayscale anti-aliased glyph (RGBA byte order)
fn draw_glyph_grayscale(
&mut self,
glyph: &crate::glyph_cache::RasterizedGlyph,
@@ -514,22 +514,22 @@
continue;
}
// Alpha blending (RGBA: idx=R, idx+1=G, idx+2=B, idx+3=A)
// Alpha blending (ABGR: idx=A, idx+1=B, idx+2=G, idx+3=R)
let alpha_f = alpha as f32 / 255.0;
let inv_alpha = 1.0 - alpha_f;
self.pixel_buffer[idx] =
self.pixel_buffer[idx] = 255; // A
(r as f32 * alpha_f + self.pixel_buffer[idx] as f32 * inv_alpha) as u8;
self.pixel_buffer[idx + 1] =
(b as f32 * alpha_f + self.pixel_buffer[idx + 1] as f32 * inv_alpha) as u8;
(g as f32 * alpha_f + self.pixel_buffer[idx + 1] as f32 * inv_alpha) as u8;
self.pixel_buffer[idx + 2] =
(g as f32 * alpha_f + self.pixel_buffer[idx + 2] as f32 * inv_alpha) as u8;
self.pixel_buffer[idx + 3] =
(r as f32 * alpha_f + self.pixel_buffer[idx + 3] as f32 * inv_alpha) as u8;
(b as f32 * alpha_f + self.pixel_buffer[idx + 2] as f32 * inv_alpha) as u8;
self.pixel_buffer[idx + 3] = 255;
}
}
}
/// Draw a subpixel anti-aliased glyph (ABGR byte order)
/// Draw a subpixel anti-aliased glyph (RGBA byte order)
fn draw_glyph_subpixel(
&mut self,
glyph: &crate::glyph_cache::RasterizedGlyph,
@@ -589,22 +589,22 @@
continue;
}
// RGBA: idx=R, idx+1=G, idx+2=B, idx+3=A
let bg_r = self.pixel_buffer[idx];
let bg_g = self.pixel_buffer[idx + 1];
// ABGR: idx=A, idx+1=B, idx+2=G, idx+3=R
let bg_b = self.pixel_buffer[idx + 1];
let bg_g = self.pixel_buffer[idx + 2];
let bg_r = self.pixel_buffer[idx + 3];
let bg_b = self.pixel_buffer[idx + 2];
let alpha_r = cov_r as f32 / 255.0;
let alpha_g = cov_g as f32 / 255.0;
let alpha_b = cov_b as f32 / 255.0;
self.pixel_buffer[idx] = 255; // A
self.pixel_buffer[idx] =
(fg_r as f32 * alpha_r + bg_r as f32 * (1.0 - alpha_r)) as u8;
self.pixel_buffer[idx + 1] =
(fg_g as f32 * alpha_g + bg_g as f32 * (1.0 - alpha_g)) as u8;
self.pixel_buffer[idx + 2] =
(fg_b as f32 * alpha_b + bg_b as f32 * (1.0 - alpha_b)) as u8;
self.pixel_buffer[idx + 3] = 255;
self.pixel_buffer[idx + 2] =
(fg_g as f32 * alpha_g + bg_g as f32 * (1.0 - alpha_g)) as u8;
self.pixel_buffer[idx + 3] =
(fg_r as f32 * alpha_r + bg_r as f32 * (1.0 - alpha_r)) as u8;
}
}
}
@@ -776,7 +776,7 @@
}
#[test]
fn test_pixel_buffer_abgr_format() {
fn test_pixel_buffer_rgba_format() {
let mut renderer = TerminalRenderer::new(10, 5, 14.0).unwrap();
// Fill a rect with a known color: R=255, G=0, B=128
@@ -785,10 +785,10 @@
let pixels = renderer.pixel_buffer();
// ABGR byte order: [A, B, G, R]
assert_eq!(pixels[0], 255, "offset 0 should be Alpha=255");
assert_eq!(pixels[1], 127, "offset 1 should be Blue=127 (0.5*255.0 truncated)");
assert_eq!(pixels[2], 0, "offset 2 should be Green=0");
assert_eq!(pixels[3], 255, "offset 3 should be Red=255");
// RGBA byte order (matches NativeImage little-endian memory layout)
assert_eq!(pixels[0], 255, "offset 0 should be Red=255");
assert_eq!(pixels[1], 0, "offset 1 should be Green=0");
assert_eq!(pixels[2], 127, "offset 2 should be Blue=127");
assert_eq!(pixels[3], 255, "offset 3 should be Alpha=255");
}
}
rust/src/terminal.rs +9 −9
@@ -808,7 +808,7 @@
// ABGR format: [A, B, G, R]. Background is ~(25, 25, 30), foreground text is ~(230, 230, 230)
let fg_pixel_count = pixels
.chunks(4)
.filter(|p| p[1] > 100 || p[2] > 100 || p[3] > 100)
.filter(|p| p[0] > 100 || p[1] > 100 || p[2] > 100)
.count();
assert!(
@@ -829,10 +829,10 @@
let pixels = state.pixel_buffer();
// Look for red-ish pixels in ABGR format: [A, B, G, R]
// RGBA: high R (p[0]), low G (p[1]), low B (p[2])
// high R (p[3]), low G (p[2]), low B (p[1])
let red_pixels = pixels
.chunks(4)
.filter(|p| p[0] > 150 && p[1] < 50 && p[2] < 50)
.filter(|p| p[3] > 150 && p[2] < 50 && p[1] < 50)
.count();
assert!(
@@ -854,13 +854,13 @@
// Cursor is at (0, 0) — check the first cell area for cursor-colored pixels
// Cursor color is (0, 255, 127) = bright green
// ABGR format: [A, B, G, R] — green channel is at idx+2
// RGBA format: [R, G, B, A] — green channel is at idx+1
let mut cursor_pixels = 0;
for y in 0..cell_h {
for x in 0..cell_w {
let idx = (y * dims[0] as usize + x) * 4;
if idx + 3 < pixels.len() {
let g = pixels[idx + 2];
let g = pixels[idx + 1]; // RGBA: G at offset 1
// Cursor is green-ish (0, 255, 127)
if g > 200 {
cursor_pixels += 1;
@@ -889,7 +889,7 @@
let cell_h = dims[3] as usize;
// Check that rows 0, 1, 2 all have foreground content
// RGBA format: [R, G, B, A] — check R channel (idx+0) for brightness
// ABGR format: [A, B, G, R] — check R channel (idx+3) for brightness
for row in 0..3 {
let row_start_y = row * cell_h;
let row_end_y = (row + 1) * cell_h;
@@ -897,7 +897,7 @@
for y in row_start_y..row_end_y {
for x in 0..dims[0] as usize {
let idx = (y * dims[0] as usize + x) * 4;
if idx + 3 < pixels.len() && pixels[idx + 3] > 100 {
if idx + 3 < pixels.len() && pixels[idx] > 100 {
row_fg += 1;
}
}
@@ -944,8 +944,8 @@
// In ABGR format, byte 0 of each pixel is alpha = 255 (fully opaque)
for (i, chunk) in pixels.chunks(4).enumerate() {
assert_eq!(
chunk[0], 255,
chunk[3], 255, // RGBA: alpha at offset 3
"Pixel {} alpha (ABGR byte 0) should be 255 (got {})",
i, chunk[0]
i, chunk[3]
);
}