MCRcortex / MCRcortex/voxy

Contribution: static block-entity body baking for chests, signs, beds and shulker boxes (26.1.2 reference implementation)

Open
#680 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
1.2k
Forks
1.1k
PR merge metrics
No merged PRs in 30d

Description

Hi! We developed a static block-entity body baking adapter while working on a Cubic Chunks port, and would like to contribute the implementation for Voxy's consideration. It covers chests, sign bodies, beds and shulker boxes. These are general Voxy rendering capabilities rather than cubic-world functionality, so we have removed the experimental production hook from our CC project and are handing the work upstream.

This is a reference implementation with limited runtime verification, not a merge-ready patch or a claim of complete block-entity support. The complete four implementation files and the focused check helper are included below so this is a concrete code handoff. This submission was prepared using Codex with the project owner's authorization.

Target and scope

Developed against Minecraft 26.1.2, Java 25, Voxy 0.2.18-beta at commit 4643445ea36efe4069a8839037ce40f435d88766. Compatibility with current upstream HEAD has not been established.

  • Chests: ordinary/trapped/ender/copper closed bodies, facing and double-chest halves.
  • Signs: standing/wall/ceiling-hanging/wall-hanging bodies, native wood textures and attachment geometry; no text.
  • Beds: native head/foot geometry, orientation and 16 colors.
  • Shulker boxes: closed body, six orientations, undyed and 16 colors.
Implementation

On the client/render thread after atlas setup, create unregistered, level-less native BE prototypes, extract native render states and immediately capture their submitted model vertices. Restore chest facing/type overwritten by the no-world preview, fix lids/progress closed and remove only the isolated sign render state's text. Copy atlas pixels and geometry before publishing to the model worker; do not retain live BEs, native models, sprites or GL objects.

Bake the snapshots using the installed Voxy SoftwareRasterizer/ReuseVertexConsumer and its six views, then continue through Voxy's existing model analysis/upload/LOD pipeline. Keep the ordinary block/fluid path for unsupported states. Normalize waterlogged body states and bed occupancy. Preserve solid versus cutout coverage, handle double-sided sign geometry and restore pixel-pack/PBO state after readback.

The three helper classes have no CC world/storage/network dependencies; their package/logger names are historical. The fourth file supplies the original setupTexture/renderToOutput/free mixin hooks. No Voxy implementation source, Minecraft model definitions, game assets or binaries are included.

Existing verification (2026-09-17)

Local archived source hashes match the files included here. Existing compilation and menu-only client checks passed:

  • Initial chest/sign implementation: 796 normalized states, 2 atlases, 17 samples plus STONE before/after; capture measured 93 ms in that run.
  • Bed/shulker extension: 1026 normalized states, 4 atlases, 9 new samples plus STONE before/after; capture measured 111 ms in that run. The previous 17 samples were not rerun in the extension check.
  • Actual six-face color/depth output was checked for model differences, facing, double-chest internal faces, bed/shulker material flags and unchanged STONE pixels/depth/flags.
  • Snapshot baking ran on the Fabric test thread, with capture and ordinary STONE baseline on the client thread. These were real native renderer/atlas and Voxy rasterizer checks, not mocked images.
  • Timing is a single-run observation, not a benchmark. These checks were not rerun for this handoff.
Limitations and integration notes

No real-world Voxy-only distant-view acceptance test, resource-pack reload test, complete lifecycle/stress test or cross-version validation has been completed. Text, decorated-pot patterns, player skins, inventory contents and animations are not implemented. Thin/overhanging geometry is limited by the existing six 16x16 faces. Unknown renderer/material/animated-sprite paths currently fail explicitly, so upstream may want a different fallback policy. Eager capture and OpenGL direct-state readback requirements also warrant upstream review.

For an upstream integration, relocate helpers as appropriate and either wire the three lifecycle hooks directly into the bakery or register the supplied mixin in a client mixin configuration. The mixin's comment refers to our old version-gating plugin; that CC plugin is not supplied or required by the helper logic. Do not enable CC's unrelated extended-coordinate/cache changes for this feature.

The check helper is a reference Fabric client GameTest helper, not a standalone test mod. Register a client GameTest entrypoint that calls its run(context) method, enable the bakery hook, and replace/remove its historical cubicchunks26.voxy.extendedCoordinates guard. Run once with cc26.test.voxyBedsShulkersOnly=false and once with true. It writes diagnostic face sheets. A standalone upstream build/test harness is not included.

If this direction is useful, the implementation below is available for review/adaptation. There is no expectation that it be accepted as-is.

Source snapshot

Paths identify the original files. SHA-256 values refer to the archived file bytes; copying through Markdown may normalize line endings.

NativeStaticBlockEntityModels.java

Original path: src/main/java/dev/muffin/cubicchunks/compat/voxy/staticmodel/NativeStaticBlockEntityModels.java

SHA-256: 3ecdc4b4973216e16f8eae10811ccd65edd857e7d1b79c5c2f3c1f08e4fc7df2

package dev.muffin.cubicchunks.compat.voxy.staticmodel;

import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.blaze3d.vertex.VertexFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.Model;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.SubmitNodeCollection;
import net.minecraft.client.renderer.SubmitNodeStorage;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BedRenderer;
import net.minecraft.client.renderer.blockentity.ChestRenderer;
import net.minecraft.client.renderer.blockentity.HangingSignRenderer;
import net.minecraft.client.renderer.blockentity.StandingSignRenderer;
import net.minecraft.client.renderer.blockentity.ShulkerBoxRenderer;
import net.minecraft.client.renderer.blockentity.state.BedRenderState;
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
import net.minecraft.client.renderer.blockentity.state.ChestRenderState;
import net.minecraft.client.renderer.blockentity.state.SignRenderState;
import net.minecraft.client.renderer.blockentity.state.ShulkerBoxRenderState;
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.BedBlock;
import net.minecraft.world.level.block.CeilingHangingSignBlock;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.CopperChestBlock;
import net.minecraft.world.level.block.EnderChestBlock;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.StandingSignBlock;
import net.minecraft.world.level.block.ShulkerBoxBlock;
import net.minecraft.world.level.block.TrappedChestBlock;
import net.minecraft.world.level.block.WallHangingSignBlock;
import net.minecraft.world.level.block.WallSignBlock;
import net.minecraft.world.level.block.WeatheringCopperChestBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.ChestType;
import net.minecraft.world.phys.Vec3;

/**
 * Client-thread adapter to Minecraft 26.1.2's audited native static BER submissions.
 * The old CC 1.12 cube tile-entity ownership and update-tag path remain unchanged:
 * these are unregistered, level-less prototypes, never entities from a live cube.
 * Geometry, materials and transforms come from the installed native renderer;
 * neither Minecraft model definitions nor Voxy implementation code are copied.
 */
public final class NativeStaticBlockEntityModels {
    private static final int FULL_BRIGHT = 0x00F000F0;
    private static final int MAX_VERTICES_PER_MODEL = 65_536;
    private static final Identifier CHEST_ATLAS = Identifier.withDefaultNamespace("textures/atlas/chest.png");
    private static final Identifier SIGN_ATLAS = Identifier.withDefaultNamespace("textures/atlas/signs.png");
    private static final Identifier BED_ATLAS = Identifier.withDefaultNamespace("textures/atlas/beds.png");
    private static final Identifier SHULKER_ATLAS = Identifier.withDefaultNamespace("textures/atlas/shulker_boxes.png");

    private NativeStaticBlockEntityModels() {}

    /** Only the audited native block implementations; unknown subclasses need their own audit. */
    public static boolean supports(BlockState state) {
        Class<?> type = Objects.requireNonNull(state, "state").getBlock().getClass();
        return type == ChestBlock.class || type == TrappedChestBlock.class
                || type == EnderChestBlock.class || type == CopperChestBlock.class
                || type == WeatheringCopperChestBlock.class || type == StandingSignBlock.class
                || type == WallSignBlock.class || type == CeilingHangingSignBlock.class
                || type == WallHangingSignBlock.class || type == BedBlock.class || type == ShulkerBoxBlock.class;
    }

    /** Water belongs to the existing fluid bake; it does not change these native body models. */
    public static BlockState normalize(BlockState state) {
        Objects.requireNonNull(state, "state");
        if (!supports(state)) return state;
        if (state.hasProperty(BlockStateProperties.WATERLOGGED))
            state = state.setValue(BlockStateProperties.WATERLOGGED, false);
        // Occupancy is gameplay state and does not change the native bed body.
        if (state.getBlock().getClass() == BedBlock.class) state = state.setValue(BedBlock.OCCUPIED, false);
        return state;
    }

    /** Deterministic unique body states; callers may isolate/report individual capture failures. */
    public static List<BlockState> supportedStates() {
        LinkedHashSet<BlockState> states = new LinkedHashSet<>();
        for (Block block : BuiltInRegistries.BLOCK) {
            if (supports(block.defaultBlockState())) {
                for (BlockState state : block.getStateDefinition().getPossibleStates()) states.add(normalize(state));
            }
        }
        return List.copyOf(states);
    }

    /**
     * Capture immediately after renderer/atlas reload, on the client thread. No native renderer,
     * model, sprite, prototype, pose or mutable render state escapes in the returned value.
     * Unsupported material/effects are explicit failures and must not be cached as empty models.
     */
    public static Mesh capture(Minecraft client, BlockState requestedState) {
        Objects.requireNonNull(client, "client");
        Objects.requireNonNull(requestedState, "state");
        if (!client.isSameThread()) throw failure(requestedState, "capture requires the client thread");
        if (!supports(requestedState)) throw failure(requestedState, "block implementation is not audited");
        BlockState state = normalize(requestedState);
        try {
            BlockEntity prototype = ((EntityBlock) state.getBlock()).newBlockEntity(BlockPos.ZERO, state);
            if (prototype == null || prototype.hasLevel()) throw failure(state, "no isolated native prototype");
            BlockEntityRenderer<BlockEntity, BlockEntityRenderState> renderer =
                    client.getBlockEntityRenderDispatcher().getRenderer(prototype);
            if (renderer == null) throw failure(state, "native renderer is unavailable after resource reload");
            boolean chestBody = state.getBlock() instanceof ChestBlock || state.getBlock() instanceof EnderChestBlock;
            boolean bedBody = state.getBlock() instanceof BedBlock;
            boolean shulkerBody = state.getBlock() instanceof ShulkerBoxBlock;
            Class<?> expectedRenderer = chestBody ? ChestRenderer.class
                    : bedBody ? BedRenderer.class : shulkerBody ? ShulkerBoxRenderer.class
                    : state.getBlock() instanceof CeilingHangingSignBlock || state.getBlock() instanceof WallHangingSignBlock
                    ? HangingSignRenderer.class : StandingSignRenderer.class;
            if (renderer.getClass() != expectedRenderer) {
                throw failure(state, "renderer implementation is not audited: " + renderer.getClass().getName());
            }
            BlockEntityRenderState renderState = renderer.createRenderState();
            renderer.extractRenderState(prototype, renderState, 0, Vec3.ZERO, null);
            if (chestBody && renderState instanceof ChestRenderState chest) {
                // The level-less native preview forces SOUTH/SINGLE. Restore the actual block state.
                chest.facing = state.getValue(ChestBlock.FACING);
                chest.type = state.hasProperty(ChestBlock.TYPE) ? state.getValue(ChestBlock.TYPE) : ChestType.SINGLE;
                chest.open = 0;
            } else if (bedBody && renderState instanceof BedRenderState) {
                // Native extraction already selects this half, facing and the block's dye color.
            } else if (shulkerBody && renderState instanceof ShulkerBoxRenderState shulker) {
                shulker.progress = 0;
            } else if (!chestBody && !bedBody && !shulkerBody && renderState instanceof SignRenderState sign) {
                // Text is instance data, not a state-wide body material. Do not mutate the prototype.
                sign.frontText = null;
                sign.backText = null;
            } else {
                throw failure(state, "native renderer returned an unexpected render-state type");
            }
            if (renderState.breakProgress != null || renderState.lightCoords != FULL_BRIGHT) {
                throw failure(state, "prototype extraction introduced world lighting or a breaking effect");
            }
            CaptureCollector collector = new CaptureCollector(state, chestBody ? CHEST_ATLAS
                    : bedBody ? BED_ATLAS : shulkerBody ? SHULKER_ATLAS : SIGN_ATLAS);
            renderer.submit(renderState, new PoseStack(), collector, new CameraRenderState());
            if (prototype.hasLevel()) throw failure(state, "native prototype acquired a live level during capture");
            return collector.finish();
        } catch (CaptureException ex) {
            throw ex;
        } catch (RuntimeException ex) {
            throw new CaptureException(state, "native model capture failed: " + ex.getClass().getSimpleName()
                    + ": " + ex.getMessage(), ex);
        }
    }

    /** Audited native material semantics; all use white vertex color and directional lighting. */
    public enum Material {
        ENTITY_SOLID, ENTITY_CUTOUT_CULL, ENTITY_CUTOUT;

        public boolean cutout() { return this != ENTITY_SOLID; }
        public float alphaCutoff() { return cutout() ? 0.1F : 0; }
        public boolean doubleSided() { return this == ENTITY_CUTOUT; }
        public boolean perFaceLighting() { return this == ENTITY_CUTOUT; }
    }

    /** Four consecutive vertices make one native quad; each vertex is x,y,z,u,v. */
    public record Layer(Identifier atlas, Material material, float[] xyzuv) {
        public Layer {
            Objects.requireNonNull(atlas, "atlas");
            Objects.requireNonNull(material, "material");
            Objects.requireNonNull(xyzuv, "xyzuv");
            if (xyzuv.length == 0 || xyzuv.length % 20 != 0) throw new IllegalArgumentException("Expected complete nonempty quads");
            xyzuv = xyzuv.clone();
            for (float value : xyzuv) {
                if (!Float.isFinite(value)) throw new IllegalArgumentException("Nonfinite model vertex");
            }
        }

        @Override public float[] xyzuv() { return xyzuv.clone(); }
        public int vertexCount() { return xyzuv.length / 5; }
        public boolean doubleSided() { return material.doubleSided(); }
    }

    public record Mesh(List<Layer> layers) {
        public Mesh {
            layers = List.copyOf(layers);
            if (layers.isEmpty()) throw new IllegalArgumentException("A captured native body must contain geometry");
        }

        public int vertexCount() { return layers.stream().mapToInt(Layer::vertexCount).sum(); }
    }

    public static final class CaptureException extends IllegalStateException {
        private final BlockState state;

        private CaptureException(BlockState state, String reason, Throwable cause) {
            super("Cannot capture static native body for " + state + ": " + reason, cause);
            this.state = state;
        }

        public BlockState state() { return state; }
    }

    private static CaptureException failure(BlockState state, String reason) {
        return new CaptureException(state, reason, null);
    }

    private static final class CaptureCollector extends SubmitNodeStorage {
        private final BlockState state;
        private final Identifier expectedAtlas;
        private final List<Layer> layers = new ArrayList<>();
        private int vertexCount;

        private CaptureCollector(BlockState state, Identifier expectedAtlas) {
            this.state = state;
            this.expectedAtlas = expectedAtlas;
        }

        // Every inherited non-model submission goes through order(). Reject, never silently queue it.
        @Override public SubmitNodeCollection order(int order) {
            throw failure(state, "renderer submitted unsupported ordered/text/item/custom geometry");
        }

        @Override public <S> void submitModel(Model<? super S> model, S modelState, PoseStack pose,
                RenderType renderType, int light, int overlay, int tintedColor, TextureAtlasSprite sprite,
                int outlineColor, ModelFeatureRenderer.CrumblingOverlay crumbling) {
            if (sprite == null || tintedColor != -1 || outlineColor != 0 || crumbling != null
                    || overlay != OverlayTexture.NO_OVERLAY || light != FULL_BRIGHT) {
                throw failure(state, "unsupported model color, lighting, overlay, outline, crumbling or missing sprite");
            }
            Identifier atlas = sprite.atlasLocation();
            if (!expectedAtlas.equals(atlas)) throw failure(state, "unexpected material atlas: " + atlas);
            if (sprite.contents().isAnimated()) throw failure(state, "animated body sprite requires an animation adapter");
            Material material;
            // Exact memoized RenderType identity also verifies the texture, target and transforms;
            // pipeline identity alone would incorrectly admit other samplers/texture transforms.
            if (renderType == RenderTypes.entitySolid(atlas)
                    && renderType.pipeline() == RenderPipelines.ENTITY_SOLID
                    && renderType.pipeline().isCull()) {
                material = Material.ENTITY_SOLID;
            } else if (renderType == RenderTypes.entityCutoutCull(atlas)
                    && renderType.pipeline() == RenderPipelines.ENTITY_CUTOUT_CULL
                    && renderType.pipeline().isCull()) {
                material = Material.ENTITY_CUTOUT_CULL;
            } else if (renderType == RenderTypes.entityCutout(atlas)
                    && renderType.pipeline() == RenderPipelines.ENTITY_CUTOUT
                    && !renderType.pipeline().isCull()) {
                material = Material.ENTITY_CUTOUT;
            } else {
                throw failure(state, "unsupported render type or shader pipeline: " + renderType);
            }
            if (renderType.mode() != VertexFormat.Mode.QUADS || renderType.hasBlending()) {
                throw failure(state, "body material is not an audited nonblended quad material");
            }
            CapturedVertices vertices = new CapturedVertices(state, MAX_VERTICES_PER_MODEL - vertexCount);
            // Model instances belong to the native renderer and are mutable: read synchronously now.
            model.setupAnim(modelState);
            model.renderToBuffer(pose, sprite.wrap(vertices), light, overlay, tintedColor);
            float[] data = vertices.finish();
            vertexCount += data.length / 5;
            layers.add(new Layer(atlas, material, data));
        }

        private Mesh finish() {
            if (layers.isEmpty()) throw failure(state, "native renderer submitted no body geometry");
            return new Mesh(layers);
        }
    }

    private static final class CapturedVertices implements VertexConsumer {
        private final BlockState state;
        private final int vertexLimit;
        private float[] values = new float[320];
        private int size;
        private boolean hasUv;
        private boolean hasColor;

        private CapturedVertices(BlockState state, int vertexLimit) {
            this.state = state;
            this.vertexLimit = vertexLimit;
        }

        @Override public VertexConsumer addVertex(float x, float y, float z) {
            finishVertex();
            if (size / 5 >= vertexLimit) throw failure(state, "native body exceeds the vertex capture limit");
            if (!local(x) || !local(y) || !local(z)) throw failure(state, "nonfinite or nonlocal native body geometry");
            if (size + 5 > values.length) values = Arrays.copyOf(values, Math.min(vertexLimit * 5, values.length * 2));
            values[size++] = x;
            values[size++] = y;
            values[size++] = z;
            values[size++] = 0;
            values[size++] = 0;
            hasUv = false;
            hasColor = false;
            return this;
        }

        @Override public VertexConsumer setColor(int r, int g, int b, int a) {
            if (r != 255 || g != 255 || b != 255 || a != 255) throw failure(state, "nonwhite native vertex color");
            return setColor(-1);
        }

        @Override public VertexConsumer setColor(int color) {
            requireVertex();
            if (color != -1) throw failure(state, "nonwhite native vertex color");
            hasColor = true;
            return this;
        }

        @Override public VertexConsumer setUv(float u, float v) {
            requireVertex();
            if (!Float.isFinite(u) || !Float.isFinite(v) || u < 0 || u > 1 || v < 0 || v > 1) {
                throw failure(state, "invalid atlas UV in native vertex");
            }
            values[size - 2] = u;
            values[size - 1] = v;
            hasUv = true;
            return this;
        }

        @Override public VertexConsumer setUv1(int u, int v) {
            requireVertex();
            if (OverlayTexture.pack(u, v) != OverlayTexture.NO_OVERLAY) throw failure(state, "native vertex has an overlay");
            return this;
        }

        @Override public VertexConsumer setUv2(int u, int v) {
            requireVertex();
            if ((u | v << 16) != FULL_BRIGHT) throw failure(state, "native vertex has non-prototype lighting");
            return this;
        }

        @Override public VertexConsumer setNormal(float x, float y, float z) {
            requireVertex();
            if (!Float.isFinite(x) || !Float.isFinite(y) || !Float.isFinite(z)) throw failure(state, "nonfinite native normal");
            return this;
        }

        @Override public VertexConsumer setLineWidth(float width) {
            throw failure(state, "line primitives are not static body quads");
        }

        private void requireVertex() {
            if (size == 0) throw failure(state, "vertex attribute before position");
        }

        private void finishVertex() {
            if (size != 0 && (!hasUv || !hasColor)) throw failure(state, "native vertex lacks UV or explicit white color");
        }

        private float[] finish() {
            finishVertex();
            if (size == 0 || size % 20 != 0) throw failure(state, "native model did not emit complete nonempty quads");
            return Arrays.copyOf(values, size);
        }

        private static boolean local(float value) {
            // Rotated wall/standing signs can extend a little beyond their owning block cell.
            return Float.isFinite(value) && value >= -1.0F && value <= 2.0F;
        }
    }
}

StaticModelAtlas.java

Original path: src/main/java/dev/muffin/cubicchunks/compat/voxy/staticmodel/StaticModelAtlas.java

SHA-256: f3f4ea40e2540c58478228f8f146c40badfe9818cc657583b652bc1f5dc81bf5

package dev.muffin.cubicchunks.compat.voxy.staticmodel;

import com.mojang.blaze3d.opengl.GlTexture;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.GpuTexture;
import com.mojang.blaze3d.textures.TextureFormat;
import java.nio.ByteOrder;
import java.util.Objects;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureAtlas;
import net.minecraft.resources.Identifier;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11C;
import org.lwjgl.opengl.GL12C;
import org.lwjgl.opengl.GL15C;
import org.lwjgl.opengl.GL21C;
import org.lwjgl.opengl.GL45C;

/**
 * An owned mip-zero atlas snapshot for one static-model resource generation.
 * Capture touches only the client atlas; workers retain no renderer or GL objects.
 */
public final class StaticModelAtlas {
    private static final int[] PACK_PARAMETERS = {
        GL11C.GL_PACK_ALIGNMENT,
        GL11C.GL_PACK_ROW_LENGTH,
        GL12C.GL_PACK_IMAGE_HEIGHT,
        GL11C.GL_PACK_SKIP_ROWS,
        GL11C.GL_PACK_SKIP_PIXELS,
        GL12C.GL_PACK_SKIP_IMAGES,
        GL11C.GL_PACK_SWAP_BYTES,
        GL11C.GL_PACK_LSB_FIRST
    };

    private final int width;
    private final int height;
    private final int[] pixels;

    private StaticModelAtlas(int width, int height, int[] pixels) {
        this.width = width;
        this.height = height;
        this.pixels = pixels;
    }

    public int width() { return this.width; }
    public int height() { return this.height; }

    /**
     * Read-only borrowed sampler data, in native atlas row order and 0xAABBGGRR.
     * The caller must never mutate this array; it is shared by this generation's workers.
     */
    public int[] pixels() { return this.pixels; }

    /**
     * Captures an already loaded atlas texture identifier, such as Sheets.CHEST_SHEET.
     * Must run after atlas upload, on the client/render thread, before worker publication.
     */
    public static StaticModelAtlas capture(Identifier atlas) {
        Objects.requireNonNull(atlas, "atlas");
        RenderSystem.assertOnRenderThread();
        Minecraft minecraft = Minecraft.getInstance();
        if (!minecraft.isSameThread()) {
            throw new IllegalStateException("Static atlas capture requires the client thread");
        }

        // AtlasManager is queried instead of TextureManager.getTexture: an invalid identifier
        // must not register or load a missing texture as a side effect of this read.
        TextureAtlas[] found = new TextureAtlas[1];
        minecraft.getAtlasManager().forEach((id, texture) -> {
            if (atlas.equals(texture.location())) {
                if (found[0] != null) throw new IllegalStateException("Duplicate atlas texture: " + atlas);
                found[0] = texture;
            }
        });
        if (found[0] == null) throw new IllegalArgumentException("Unknown atlas texture: " + atlas);

        GpuTexture texture = found[0].getTexture();
        if (!(texture instanceof GlTexture glTexture) || texture.isClosed()) {
            throw new IllegalStateException("Atlas requires a live OpenGL texture: " + atlas);
        }
        if (texture.getFormat() != TextureFormat.RGBA8 || texture.getDepthOrLayers() != 1
            || texture.getMipLevels() < 1 || (texture.usage() & GpuTexture.USAGE_CUBEMAP_COMPATIBLE) != 0) {
            throw new IllegalStateException("Atlas must be a two-dimensional RGBA8 texture: " + atlas);
        }
        if (GL.getCapabilities().glGetTextureImage == 0L
            || GL.getCapabilities().glGetTextureLevelParameteriv == 0L) {
            throw new IllegalStateException("Atlas capture requires OpenGL direct-state texture readback");
        }

        // Do not mistake an already pending renderer error for a successful capture.
        requireNoGlError("before atlas capture", atlas);
        int width = texture.getWidth(0);
        int height = texture.getHeight(0);
        int limit = GL11C.glGetInteger(GL11C.GL_MAX_TEXTURE_SIZE);
        long pixelCount = (long) width * height;
        if (width <= 0 || height <= 0 || width > limit || height > limit
            || pixelCount > Integer.MAX_VALUE / Integer.BYTES) {
            throw new IllegalStateException("Atlas dimensions exceed readback bounds: " + atlas
                + " (" + width + "x" + height + ")");
        }
        int textureId = glTexture.glId();
        if (!GL11C.glIsTexture(textureId)
            || GL45C.glGetTextureLevelParameteri(textureId, 0, GL11C.GL_TEXTURE_WIDTH) != width
            || GL45C.glGetTextureLevelParameteri(textureId, 0, GL11C.GL_TEXTURE_HEIGHT) != height
            || GL45C.glGetTextureLevelParameteri(textureId, 0, GL11C.GL_TEXTURE_INTERNAL_FORMAT) != GL11C.GL_RGBA8) {
            throw new IllegalStateException("Atlas GPU storage does not match RGBA8 dimensions: " + atlas);
        }
        requireNoGlError("validating atlas storage", atlas);

        int[] pixels = new int[(int) pixelCount];
        int previousBuffer = GL11C.glGetInteger(GL21C.GL_PIXEL_PACK_BUFFER_BINDING);
        int[] previousPack = new int[PACK_PARAMETERS.length];
        for (int i = 0; i < PACK_PARAMETERS.length; i++) {
            previousPack[i] = GL11C.glGetInteger(PACK_PARAMETERS[i]);
        }
        requireNoGlError("reading pixel-pack state", atlas);

        try {
            GL15C.glBindBuffer(GL21C.GL_PIXEL_PACK_BUFFER, 0);
            for (int i = 0; i < PACK_PARAMETERS.length; i++) {
                GL11C.glPixelStorei(PACK_PARAMETERS[i], i == 0 ? Integer.BYTES : 0);
            }
            // Client-memory texture readback provides completion for this texture. No global
            // finish, framebuffer binding, texture binding, or renderer cache mutation is needed.
            GL45C.glGetTextureImage(textureId, 0, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, pixels);
            requireNoGlError("reading atlas pixels", atlas);
        } finally {
            try {
                for (int i = 0; i < PACK_PARAMETERS.length; i++) {
                    GL11C.glPixelStorei(PACK_PARAMETERS[i], previousPack[i]);
                }
            } finally {
                GL15C.glBindBuffer(GL21C.GL_PIXEL_PACK_BUFFER, previousBuffer);
            }
        }
        requireNoGlError("restoring pixel-pack state", atlas);

        if (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) {
            for (int i = 0; i < pixels.length; i++) pixels[i] = Integer.reverseBytes(pixels[i]);
        }
        // The array is fully populated before these final fields are published to workers.
        return new StaticModelAtlas(width, height, pixels);
    }

    private static void requireNoGlError(String operation, Identifier atlas) {
        int error = GL11C.glGetError();
        if (error != GL11C.GL_NO_ERROR) {
            throw new IllegalStateException("OpenGL error 0x" + Integer.toHexString(error)
                + " " + operation + ": " + atlas);
        }
    }
}

VoxyStaticModelBakery.java

Original path: src/main/java/dev/muffin/cubicchunks/compat/voxy/staticmodel/VoxyStaticModelBakery.java

SHA-256: d4959454aabc2af0b46ca18db6a02293301eef0c4bc399676095630255c98a1e

package dev.muffin.cubicchunks.compat.voxy.staticmodel;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import me.cortex.voxy.client.core.model.ModelFactory;
import me.cortex.voxy.client.core.model.bakery.ReuseVertexConsumer;
import me.cortex.voxy.client.core.model.bakery.SoftwareRasterizer;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.block.state.BlockState;
import org.joml.Matrix4f;
import org.lwjgl.system.MemoryUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Adapts native static BER geometry to the installed Voxy software baker. */
public final class VoxyStaticModelBakery implements AutoCloseable {
    private static final Logger LOGGER = LoggerFactory.getLogger("cubicchunks26-voxy-models");
    private static final int SIZE = ModelFactory.MODEL_TEXTURE_SIZE;
    // Voxy's six projection matrices include mirrored UP/NORTH/WEST views.
    private static final boolean[] CULL = {false, true, true, false, true, false};
    private final Map<BlockState, PreparedModel> models;
    private final Matrix4f[] views;
    private final SoftwareRasterizer rasterizer = new SoftwareRasterizer(SIZE);
    private final ReuseVertexConsumer vertices = new ReuseVertexConsumer();
    private boolean closed;

    private VoxyStaticModelBakery(Map<BlockState, PreparedModel> models, Matrix4f[] views) {
        this.models = models;
        this.views = views;
    }

    /** Called before Voxy starts its model worker; no native renderer escapes this call. */
    public static VoxyStaticModelBakery capture(Matrix4f[] projections) {
        long started = System.nanoTime();
        Minecraft client = Minecraft.getInstance();
        if (!client.isSameThread()) throw new IllegalStateException("Static model capture requires client thread");
        if (projections.length != 6) throw new IllegalArgumentException("Expected Voxy's six model views");
        Matrix4f[] views = new Matrix4f[projections.length];
        for (int i = 0; i < views.length; i++) views[i] = new Matrix4f(projections[i]);
        Map<Identifier, StaticModelAtlas> atlases = new HashMap<>();
        Map<SamplerKey, Sampler> samplers = new HashMap<>();
        Map<BlockState, PreparedModel> models = new HashMap<>();
        for (BlockState state : NativeStaticBlockEntityModels.supportedStates()) {
            var captured = NativeStaticBlockEntityModels.capture(client, state);
            List<PreparedLayer> layers = new ArrayList<>();
            // All audited native pipelines are directionally lit. PER_FACE_LIGHTING selects
            // front/back normal handling, not whether the material receives lighting.
            int flags = 1; // Voxy shaded; discard is selected per native material below.
            for (var layer : captured.layers()) {
                boolean cutout = layer.material().cutout();
                if (cutout) flags |= 8;
                Sampler sampler = samplers.computeIfAbsent(new SamplerKey(layer.atlas(), cutout), key -> {
                    StaticModelAtlas atlas = atlases.computeIfAbsent(key.atlas, StaticModelAtlas::capture);
                    int[] pixels = atlas.pixels().clone();
                    // Audited cutout pipelines use ALPHA_CUTOUT=0.1. Voxy's
                    // rasterizer discards alpha==0, so adapt only our owned sampler.
                    for (int i = 0; i < pixels.length; i++) {
                        if (key.cutout) {
                            if ((pixels[i] >>> 24) < 26) pixels[i] = 0;
                        } else {
                            // ENTITY_SOLID neither blends nor discards texture alpha.
                            pixels[i] |= 0xff000000;
                        }
                    }
                    return new Sampler(pixels, atlas.width(), atlas.height());
                });
                layers.add(new PreparedLayer(sampler, layer.doubleSided(), cutout, layer.xyzuv()));
            }
            if (layers.isEmpty()) throw new IllegalStateException("Empty supported static model: " + state);
            models.put(state, new PreparedModel(List.copyOf(layers), flags));
        }
        LOGGER.info("[CC26_VOXY_STATIC] captured states={} atlases={} milliseconds={}",
                models.size(), atlases.size(), (System.nanoTime() - started) / 1_000_000);
        return new VoxyStaticModelBakery(Map.copyOf(models), views);
    }

    /** Returns -1 for states that must follow Voxy's ordinary model/fluid path. */
    public int render(BlockState state, long output) {
        if (closed) throw new IllegalStateException("Static model bakery was closed");
        if (!NativeStaticBlockEntityModels.supports(state)) return -1;
        PreparedModel model = models.get(NativeStaticBlockEntityModels.normalize(state));
        if (model == null) throw new IllegalStateException("Supported static model was not captured: " + state);
        if (output == 0) throw new IllegalArgumentException("Null Voxy output buffer");
        for (int face = 0; face < views.length; face++) {
            rasterizer.clear();
            rasterizer.setBlending(false);
            for (PreparedLayer layer : model.layers) {
                rasterizer.setSamplerTexture(layer.sampler.pixels, layer.sampler.width, layer.sampler.height);
                vertices.reset().setDefaultMeta(layer.cutout ? 1 : 0);
                float[] data = layer.xyzuv;
                for (int i = 0; i < data.length; i += 5) {
                    vertices.addVertex(data[i], data[i + 1], data[i + 2]).setUv(data[i + 3], data[i + 4]);
                }
                rasterizer.setFaceCull(CULL[face]);
                rasterizer.raster(views[face], vertices);
                if (layer.doubleSided) {
                    // The installed rasterizer accepts one winding at a time. Each
                    // nondegenerate triangle is accepted in exactly one of these passes.
                    rasterizer.setFaceCull(!CULL[face]);
                    rasterizer.raster(views[face], vertices);
                }
            }
            long[] pixels = rasterizer.getRawFramebuffer();
            long destination = output + (long) face * SIZE * SIZE * Long.BYTES;
            for (int i = 0; i < pixels.length; i++) MemoryUtil.memPutLong(destination + (long) i * Long.BYTES, pixels[i]);
        }
        return model.flags;
    }

    /** Voxy joins its model worker before freeing the owning bakery. */
    @Override
    public void close() {
        if (!closed) {
            closed = true;
            vertices.free();
        }
    }

    private record Sampler(int[] pixels, int width, int height) {}
    private record SamplerKey(Identifier atlas, boolean cutout) {}
    private record PreparedLayer(Sampler sampler, boolean doubleSided, boolean cutout, float[] xyzuv) {}
    private record PreparedModel(List<PreparedLayer> layers, int flags) {}
}

SoftwareModelStaticEntitiesMixin.java

Original path: src/main/java/dev/muffin/cubicchunks/compat/voxy/mixin/SoftwareModelStaticEntitiesMixin.java

SHA-256: 9b7290b5dbb32b1288b1435515d46f8ceb5e4f4dae1325c8803a01ec6b8e7b41

package dev.muffin.cubicchunks.compat.voxy.mixin;

import dev.muffin.cubicchunks.compat.voxy.staticmodel.VoxyStaticModelBakery;
import net.minecraft.world.level.block.state.BlockState;
import org.joml.Matrix4f;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

/** Enabled only for the exact audited, installed Voxy build by VoxyMixinPlugin. */
@Pseudo
@Mixin(targets = "me.cortex.voxy.client.core.model.bakery.SoftwareModelTextureBakery", remap = false)
public abstract class SoftwareModelStaticEntitiesMixin {
    @Shadow @Final private static Matrix4f[] VIEWS;
    @Unique private VoxyStaticModelBakery cc26$staticModels;

    @Inject(method = "setupTexture", at = @At("RETURN"))
    private void cc26$captureStaticModels(CallbackInfo ci) {
        if (cc26$staticModels != null) throw new IllegalStateException("Static models already captured for this bakery");
        cc26$staticModels = VoxyStaticModelBakery.capture(VIEWS);
    }

    @Inject(method = "renderToOutput", at = @At("HEAD"), cancellable = true)
    private void cc26$renderStaticModel(BlockState state, long output, CallbackInfoReturnable<Integer> cir) {
        if (cc26$staticModels == null) throw new IllegalStateException("Static models were not prepared before baking");
        int flags = cc26$staticModels.render(state, output);
        if (flags >= 0) cir.setReturnValue(flags);
    }

    @Inject(method = "free", at = @At("HEAD"))
    private void cc26$freeStaticModels(CallbackInfo ci) {
        if (cc26$staticModels != null) {
            cc26$staticModels.close();
            cc26$staticModels = null;
        }
    }
}

CubicVoxyStaticModelClientChecks.java

Original path: src/clienttest/java/dev/muffin/cubicchunks/CubicVoxyStaticModelClientChecks.java

SHA-256: 00bd00d6f8b4b40c38b20c9a25c99b9d4fa65496fa931da3ab838e589de76b18

package dev.muffin.cubicchunks;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.imageio.ImageIO;
import me.cortex.voxy.client.core.model.ModelFactory;
import me.cortex.voxy.client.core.model.bakery.SoftwareModelTextureBakery;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.BedBlock;
import net.minecraft.world.level.block.CeilingHangingSignBlock;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.EnderChestBlock;
import net.minecraft.world.level.block.StandingSignBlock;
import net.minecraft.world.level.block.ShulkerBoxBlock;
import net.minecraft.world.level.block.WallHangingSignBlock;
import net.minecraft.world.level.block.WallSignBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.ChestType;
import net.minecraft.world.level.block.state.properties.BedPart;
import org.lwjgl.system.MemoryUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** A menu-only check of actual native model capture and the installed Voxy rasterizer. */
public final class CubicVoxyStaticModelClientChecks {
    private static final Logger LOGGER = LoggerFactory.getLogger("cubicchunks26-client-test");
    private static final int SIZE = 16;
    private static final int FACE_PIXELS = SIZE * SIZE;
    private static final int PIXELS = 6 * FACE_PIXELS;
    private static final String[] FACES = {"DOWN", "UP", "NORTH", "SOUTH", "WEST", "EAST"};

    private CubicVoxyStaticModelClientChecks() {}

    public static void run(ClientGameTestContext context) {
        boolean bedsShulkersOnly = Boolean.getBoolean("cc26.test.voxyBedsShulkersOnly");
        if (!Boolean.getBoolean("cubicchunks26.voxy.extendedCoordinates"))
            throw new AssertionError("The audited optional Voxy mixins must be enabled");
        if (ModelFactory.MODEL_TEXTURE_SIZE != SIZE)
            throw new AssertionError("Installed Voxy no longer uses the audited 16-pixel model faces");
        context.waitFor(client -> client.getOverlay() == null, 400);
        SoftwareModelTextureBakery bakery = context.computeOnClient(client -> {
            if (!client.isSameThread() || client.level != null || client.player != null)
                throw new AssertionError("Static capture must run on the client thread without a world");
            SoftwareModelTextureBakery created = new SoftwareModelTextureBakery();
            try {
                created.setupTexture();
                return created;
            } catch (RuntimeException | Error failure) {
                created.free();
                throw failure;
            }
        });
        try {
            // Fabric invokes this body on its test thread. The production Voxy model worker
            // likewise consumes immutable captured geometry without accessing GL or live BEs.
            if (context.computeOnClient(client -> Thread.currentThread()) == Thread.currentThread())
                throw new AssertionError("Software baking must be checked outside the client thread");
            List<Baked> results = new ArrayList<>();
            // Voxy's ordinary model path reads Minecraft.getInstance(). Fabric explicitly
            // forbids that on its test thread; keep only the new snapshot path off-thread.
            Baked stoneBefore = context.computeOnClient(client ->
                    bake(bakery, new Sample("stone_before", Blocks.STONE.defaultBlockState(), true)));
            results.add(stoneBefore);
            for (Sample sample : bedsShulkersOnly ? bedAndShulkerSamples() : samples()) results.add(bake(bakery, sample));
            Baked stoneAfter = context.computeOnClient(client ->
                    bake(bakery, new Sample("stone_after", Blocks.STONE.defaultBlockState(), true)));
            results.add(stoneAfter);
            Path output = FabricLoader.getInstance().getGameDir().resolve("screenshots")
                    .resolve(bedsShulkersOnly ? "cc26-voxy-static-beds-shulkers.png" : "cc26-voxy-static-model-faces.png");
            writeEvidence(results, output);
            for (Baked baked : results) validate(baked);
            requireSame(stoneBefore, stoneAfter, "Static BE atlas/metadata leaked into the ordinary STONE bake");
            if (bedsShulkersOnly) {
                requireDifferent(find(results, "red_bed_head"), find(results, "red_bed_foot"),
                        "The native bed head/foot model was lost");
                requireDifferent(find(results, "red_bed_head"), find(results, "red_bed_south"),
                        "The bed orientation was lost");
                requireDifferent(find(results, "red_bed_head"), find(results, "blue_bed_head"),
                        "Native bed dye textures collapsed to one color");
                requireDifferent(find(results, "shulker_uncolored"), find(results, "shulker_purple"),
                        "Undyed shulker lost its distinct native texture");
                requireDifferent(find(results, "shulker_purple"), find(results, "shulker_red_up"),
                        "Native shulker dye textures collapsed to one color");
                requireDifferent(find(results, "shulker_red_up"), find(results, "shulker_red_down"),
                        "The downward shulker rotation was lost");
                requireDifferent(find(results, "shulker_red_up"), find(results, "shulker_red_east"),
                        "The sideways shulker rotation was lost");
            } else {
                requireDifferent(find(results, "chest_north"), find(results, "chest_south"),
                        "The chest facing was lost during native prototype extraction");
                requireDifferent(find(results, "chest_north"), find(results, "chest_east"),
                        "A quarter-turn chest still has the same six projected faces");
                requireDifferent(find(results, "chest_left"), find(results, "chest_right"),
                        "The two double-chest halves were normalized to the same model");
                requireSame(find(results, "chest_north"), find(results, "chest_waterlogged"),
                        "The static body changed when waterlogged; Voxy handles the fluid in a separate bake");
                requireDifferent(find(results, "hanging_ceiling"), find(results, "hanging_attached"),
                        "The two native hanging-sign attachment models were lost");
            }
            LOGGER.info("[CC26_VOXY_STATIC_MODEL] PASS models={} faces={} stone_unchanged=true "
                            + "worker_thread={} world_created=false image={}",
                    results.size() - 2, results.size() * 6, Thread.currentThread().getName(), output);
        } finally {
            context.runOnClient(client -> bakery.free());
        }
    }

    private static List<Sample> samples() {
        List<Sample> samples = new ArrayList<>();
        for (Direction facing : List.of(Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST))
            samples.add(new Sample("chest_" + facing.getName(), Blocks.CHEST.defaultBlockState()
                    .setValue(ChestBlock.FACING, facing), true));
        samples.add(new Sample("chest_left", Blocks.CHEST.defaultBlockState()
                .setValue(ChestBlock.FACING, Direction.NORTH).setValue(ChestBlock.TYPE, ChestType.LEFT), true));
        samples.add(new Sample("chest_right", Blocks.CHEST.defaultBlockState()
                .setValue(ChestBlock.FACING, Direction.NORTH).setValue(ChestBlock.TYPE, ChestType.RIGHT), true));
        samples.add(new Sample("chest_waterlogged", Blocks.CHEST.defaultBlockState()
                .setValue(ChestBlock.FACING, Direction.NORTH).setValue(BlockStateProperties.WATERLOGGED, true), true));
        samples.add(new Sample("trapped_chest", Blocks.TRAPPED_CHEST.defaultBlockState()
                .setValue(ChestBlock.FACING, Direction.NORTH), true));
        samples.add(new Sample("ender_chest", Blocks.ENDER_CHEST.defaultBlockState()
                .setValue(EnderChestBlock.FACING, Direction.NORTH), true));
        samples.add(new Sample("copper_chest", Blocks.COPPER_CHEST.defaultBlockState()
                .setValue(ChestBlock.FACING, Direction.NORTH), true));
        samples.add(new Sample("oxidized_copper_chest", Blocks.OXIDIZED_COPPER_CHEST.defaultBlockState()
                .setValue(ChestBlock.FACING, Direction.NORTH), true));
        samples.add(new Sample("standing_oak", Blocks.OAK_SIGN.defaultBlockState()
                .setValue(StandingSignBlock.ROTATION, 0), false));
        samples.add(new Sample("standing_oak_rotated", Blocks.OAK_SIGN.defaultBlockState()
                .setValue(StandingSignBlock.ROTATION, 3), false));
        samples.add(new Sample("wall_oak", Blocks.OAK_WALL_SIGN.defaultBlockState()
                .setValue(WallSignBlock.FACING, Direction.NORTH), false));
        samples.add(new Sample("hanging_ceiling", Blocks.OAK_HANGING_SIGN.defaultBlockState()
                .setValue(CeilingHangingSignBlock.ROTATION, 0).setValue(CeilingHangingSignBlock.ATTACHED, false), false));
        samples.add(new Sample("hanging_attached", Blocks.OAK_HANGING_SIGN.defaultBlockState()
                .setValue(CeilingHangingSignBlock.ROTATION, 0).setValue(CeilingHangingSignBlock.ATTACHED, true), false));
        samples.add(new Sample("hanging_wall", Blocks.OAK_WALL_HANGING_SIGN.defaultBlockState()
                .setValue(WallHangingSignBlock.FACING, Direction.NORTH), false));
        return samples;
    }

    private static List<Sample> bedAndShulkerSamples() {
        BlockState redBed = Blocks.RED_BED.defaultBlockState().setValue(BedBlock.FACING, Direction.NORTH);
        BlockState redShulker = Blocks.RED_SHULKER_BOX.defaultBlockState();
        return List.of(
                new Sample("red_bed_head", redBed.setValue(BedBlock.PART, BedPart.HEAD), false),
                new Sample("red_bed_foot", redBed.setValue(BedBlock.PART, BedPart.FOOT), false),
                new Sample("red_bed_south", redBed.setValue(BedBlock.PART, BedPart.HEAD)
                        .setValue(BedBlock.FACING, Direction.SOUTH), false),
                new Sample("blue_bed_head", Blocks.BLUE_BED.defaultBlockState()
                        .setValue(BedBlock.FACING, Direction.NORTH).setValue(BedBlock.PART, BedPart.HEAD), false),
                new Sample("shulker_uncolored", Blocks.SHULKER_BOX.defaultBlockState()
                        .setValue(ShulkerBoxBlock.FACING, Direction.UP), true),
                new Sample("shulker_purple", Blocks.PURPLE_SHULKER_BOX.defaultBlockState()
                        .setValue(ShulkerBoxBlock.FACING, Direction.UP), true),
                new Sample("shulker_red_up", redShulker.setValue(ShulkerBoxBlock.FACING, Direction.UP), true),
                new Sample("shulker_red_down", redShulker.setValue(ShulkerBoxBlock.FACING, Direction.DOWN), true),
                new Sample("shulker_red_east", redShulker.setValue(ShulkerBoxBlock.FACING, Direction.EAST), true));
    }

    private static Baked bake(SoftwareModelTextureBakery bakery, Sample sample) {
        long address = MemoryUtil.nmemAllocChecked((long) PIXELS * Long.BYTES);
        try {
            // Poisoning makes incomplete writes visible instead of relying on zeroed allocation.
            MemoryUtil.memSet(address, 0x5a, (long) PIXELS * Long.BYTES);
            int flags = bakery.renderToOutput(sample.state(), address);
            long[] pixels = new long[PIXELS];
            for (int i = 0; i < pixels.length; i++) pixels[i] = MemoryUtil.memGetLong(address + (long) i * Long.BYTES);
            return new Baked(sample, flags, pixels);
        } finally {
            MemoryUtil.nmemFree(address);
        }
    }

    private static void validate(Baked baked) {
        int[] counts = new int[6];
        long minimumDepth = 0xffffffL;
        long maximumDepth = 0;
        for (int i = 0; i < baked.pixels().length; i++) {
            long pixel = baked.pixels()[i];
            if (pixel == 0x5a5a5a5a5a5a5a5aL)
                throw new AssertionError("Voxy left output memory unwritten: " + baked.sample().label());
            if (((int) pixel >>> 24) == 0) continue;
            counts[i / FACE_PIXELS]++;
            long depth = pixel >>> 40;
            if (depth == 0xffffffL)
                throw new AssertionError("Opaque output retained the clear depth: " + baked.sample().label());
            minimumDepth = Math.min(minimumDepth, depth);
            maximumDepth = Math.max(maximumDepth, depth);
        }
        int nonemptyFaces = (int) Arrays.stream(counts).filter(count -> count != 0).count();
        int visiblePixels = Arrays.stream(counts).sum();
        LOGGER.info("[CC26_VOXY_STATIC_MODEL] model={} flags={} visible={} face_pixels={} depth={}..{} hash={}",
                baked.sample().label(), baked.flags(), visiblePixels, Arrays.toString(counts),
                minimumDepth, maximumDepth, Integer.toHexString(Arrays.hashCode(baked.pixels())));
        // Native double-chest halves omit the internal joining face, unlike single boxes.
        boolean doubleChestHalf = baked.sample().state().hasProperty(ChestBlock.TYPE)
                && baked.sample().state().getValue(ChestBlock.TYPE) != ChestType.SINGLE;
        if (doubleChestHalf && counts[ChestBlock.getConnectedDirection(baked.sample().state()).get3DDataValue()] != 0)
            throw new AssertionError("Native double-chest internal face was unexpectedly closed: " + baked.sample().label());
        // Single boxes must survive all six projections. Sign planes thinner than one texel
        // can disappear edge-on at Voxy's fixed resolution; require both broad sides and
        // retain all six images/counts for inspection instead of demanding invented thickness.
        if (nonemptyFaces < (baked.sample().box() ? (doubleChestHalf ? 5 : 6) : 2))
            throw new AssertionError("Missing static model faces for " + baked.sample().label() + ": " + Arrays.toString(counts));
        boolean shulker = baked.sample().state().getBlock() instanceof ShulkerBoxBlock;
        if (!baked.sample().label().startsWith("stone_") && !shulker && visiblePixels == PIXELS)
            throw new AssertionError("A sparse static model became a full opaque cube: " + baked.sample().label());
        if (baked.sample().state().getBlock() instanceof BedBlock && baked.flags() != 1)
            throw new AssertionError("Native solid bed must use shaded, non-discard material: " + baked.flags());
        if (shulker && baked.flags() != 9)
            throw new AssertionError("Native shulker must use shaded cutout material: " + baked.flags());
        if (!baked.sample().label().startsWith("stone_") && (baked.flags() & 4) != 0)
            throw new AssertionError("Opaque/cutout static body was classified as translucent: " + baked.sample().label());
    }

    private static Baked find(List<Baked> results, String label) {
        return results.stream().filter(result -> result.sample().label().equals(label)).findFirst().orElseThrow();
    }

    private static void requireSame(Baked first, Baked second, String message) {
        if (first.flags() != second.flags() || !Arrays.equals(first.pixels(), second.pixels()))
            throw new AssertionError(message);
    }

    private static void requireDifferent(Baked first, Baked second, String message) {
        if (Arrays.equals(first.pixels(), second.pixels())) throw new AssertionError(message);
    }

    private static void writeEvidence(List<Baked> results, Path output) {
        int scale = 5;
        int labelWidth = 230;
        int faceWidth = SIZE * scale;
        int cellWidth = faceWidth + 12;
        int rowHeight = faceWidth + 14;
        int headerHeight = 45;
        BufferedImage image = new BufferedImage(labelWidth + cellWidth * 6,
                headerHeight + rowHeight * results.size(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics = image.createGraphics();
        try {
            graphics.setColor(new Color(0xff20242a, true));
            graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
            graphics.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
            graphics.setColor(Color.WHITE);
            graphics.drawString("Voxy static BE: native six-face output", 10, 17);
            for (int face = 0; face < 6; face++)
                graphics.drawString(FACES[face], labelWidth + face * cellWidth, 36);
            for (int row = 0; row < results.size(); row++) {
                Baked baked = results.get(row);
                int yOrigin = headerHeight + row * rowHeight;
                graphics.setColor(Color.WHITE);
                graphics.drawString(baked.sample().label(), 10, yOrigin + 21);
                graphics.setColor(new Color(0xffbbc4d0, true));
                graphics.drawString("flags=" + baked.flags(), 10, yOrigin + 42);
                for (int face = 0; face < 6; face++) for (int y = 0; y < SIZE; y++) for (int x = 0; x < SIZE; x++) {
                    int xDraw = labelWidth + face * cellWidth + x * scale;
                    // The Voxy framebuffer uses a bottom-left origin; PNG uses top-left.
                    int yDraw = yOrigin + (SIZE - 1 - y) * scale;
                    graphics.setColor(((x / 2 + y / 2) & 1) == 0 ? new Color(0xff555b63, true) : new Color(0xff373d44, true));
                    graphics.fillRect(xDraw, yDraw, scale, scale);
                    int abgr = (int) baked.pixels()[face * FACE_PIXELS + y * SIZE + x];
                    int argb = (abgr & 0xff00ff00) | ((abgr & 0xff) << 16) | ((abgr >>> 16) & 0xff);
                    graphics.setColor(new Color(argb, true));
                    graphics.fillRect(xDraw, yDraw, scale, scale);
                }
            }
        } finally {
            graphics.dispose();
        }
        try {
            Files.createDirectories(output.getParent());
            if (!ImageIO.write(image, "png", output.toFile())) throw new IOException("No PNG writer is available");
        } catch (IOException failure) {
            throw new UncheckedIOException(failure);
        }
        LOGGER.info("[CC26_VOXY_STATIC_MODEL] six_face_image={}", output);
    }

    private record Sample(String label, BlockState state, boolean box) {}
    private record Baked(Sample sample, int flags, long[] pixels) {}
}

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the supplied NativeStaticBlockEntityModels.java and Voxy's existing bakery, SoftwareRasterizer, and ReuseVertexConsumer paths. Inspect the client GameTest helper and lifecycle or mixin hooks before adapting the reference implementation to current upstream. Done means supported block-entity bodies pass current-head integration, reload, lifecycle, and rendering checks without retaining native objects.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
computer-graphics, game-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.