Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 24 additions & 91 deletions src/main/java/dev/amble/ait/client/renderers/AITRenderLayers.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package dev.amble.ait.client.renderers;

import java.util.Set;
import java.util.function.Function;

import net.fabricmc.api.EnvType;
Expand All @@ -14,13 +13,29 @@
import net.minecraft.util.Identifier;
import net.minecraft.util.Util;

import dev.amble.ait.AITMod;


@Environment(EnvType.CLIENT)
public class AITRenderLayers extends RenderLayer {

private static RenderLayer emissive(Identifier texture, boolean sorted) {
/**
* The emissive layer, always with the quad sorter.
*
* <p>The sorter is not an optimisation to be skipped when the alpha is opaque. This layer writes
* no depth ({@code COLOR_MASK}) and disables back face culling, so nothing in the batch occludes
* anything else in it and both faces of every part are submitted. Whichever quad is drawn last
* wins, which leaves submission order as the only thing deciding what ends up on top: a part's
* own back face, or a light sitting behind a panel, will paint over the front of it.
*
* <p>That is independent of the texture's alpha. Binary alpha makes the <em>blend</em> order
* independent, not the occlusion, and dropping the sorter on that reasoning put lights through
* panels and left the animated monitor glow on Renaissance and Toyota not reading as animated.
*
* <p>It does cost. {@code BufferBuilder.setSorter} allocates a primitive centre per quad and
* writes an explicit index buffer instead of reusing the shared sequential one, which on Copper's
* roughly 6800 quad emission pass measured about 0.6 ms a frame. That is the price of drawing it
* in the right order.
*/
private static RenderLayer emissive(Identifier texture) {
RenderPhase.Texture texture2 = new RenderPhase.Texture(texture, false, false);
MultiPhaseParameters multiPhaseParameters = RenderLayer.MultiPhaseParameters.builder()
.program(RenderPhase.EYES_PROGRAM)
Expand All @@ -33,98 +48,16 @@ private static RenderLayer emissive(Identifier texture, boolean sorted) {
.depthTest(RenderPhase.LEQUAL_DEPTH_TEST)
.build(false);

// The last flag is what makes RenderLayer.draw hand the buffer a sorter, and that is more than
// a sort: BufferBuilder.setSorter also builds a Vector3f per quad for the primitive centres,
// and build() then writes an explicit index buffer instead of reusing the shared sequential
// one. Copper's emission pass is around 6800 quads, so per flush that is 6800 allocations, a
// 6800-element sort and about 41000 index writes.
//
// It is separate from the blend mode set above. The layer never writes depth (COLOR_MASK), so
// ordering can only matter where emissive geometry overlaps emissive geometry inside one batch.
//
// Mostly the textures make that moot: 106 of the mod's 113 emission textures are strictly
// binary alpha, where SRC_ALPHA blending is order-independent. Seven are not, and one is not
// marginal: hourglass_default_emission.png carries 1372 partial texels including a flat run of
// 768 at alpha 100, which is deliberate semi-transparent glow. Crystalline (64 texels),
// steam_copper (24), steam_playpal (2) and bookshelf_default (34) are the rest. On those, a
// glow-over-glow overlap composites in submission order here where it used to composite in
// depth order.
//
// Vertex alpha is a separate exposure and is not covered by the texture argument at all, which
// is what the sorted layer below is for.
return RenderLayer.of(sorted ? "emissive_cull_z_offset_sorted" : "emissive_cull_z_offset_unsorted",
return RenderLayer.of("emissive_cull_z_offset",
VertexFormats.POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL, VertexFormat.DrawMode.QUADS, 256,
false, sorted, multiPhaseParameters);
false, true, multiPhaseParameters);
}

/**
* Emission textures whose partial alpha is large enough that draw order is visible, so they get the
* sorted layer whoever asks for them. Sortedness is a property of the texture here, not of the call
* site, which keeps one texture mapped to one layer object.
*
* <p>Only hourglass qualifies. Its 1372 partial texels include a flat run of 768 at alpha 100,
* which is deliberate semi-transparent glow rather than an edge ramp. The other six partial-alpha
* emission textures are accepted unsorted: crystalline and its two recolours have 64 texels each at
* alpha 199, bookshelf_default 34, steam_copper 24 at alpha 4, steam_playpal 2. Crystalline was
* checked against a sorted arm in game and showed nothing above its own animation noise.
*/
private static final Set<Identifier> SORT_SENSITIVE_EMISSION = Set.of(
AITMod.id("textures/blockentities/consoles/hourglass_default_emission.png"));
private static final Function<Identifier, RenderLayer> EMISSIVE = Util.memoize(AITRenderLayers::emissive);

private static final Function<Identifier, RenderLayer> EMISSIVE_SORTED = Util
.memoize(texture -> emissive(texture, true));

private static final Function<Identifier, RenderLayer> EMISSIVE_UNSORTED = Util
.memoize(texture -> emissive(texture, false));

/**
* The emissive layer for geometry drawn at full vertex alpha, which is every caller but two.
*
* <p>Returns the sorted layer anyway for the textures in {@link #SORT_SENSITIVE_EMISSION}.
*
* <p>Unsorted. At alpha 1 the blend leaves nothing for the ordering to change except where opaque
* glow overlaps opaque glow, and the sort is the single most expensive thing the console's
* emission pass does. Measured on Copper, interior, landed, interleaved arms inside one client
* session: the {@code monitor} zone went from 1.371-1.404 ms to 0.743-0.830 ms, disjoint ranges,
* while the two emission zones, which are negative controls this cannot affect, moved by under
* 0.06 ms. Frame time moved too but its per-rep ranges overlap at n=3, so it is not claimed.
*
* <p>The cost lands in {@code monitor} only when monitor text is enabled and the variant overrides
* {@code renderMonitorText}: that call is what next asks {@code Immediate} for a buffer, and the
* flush of this layer is billed to whichever zone is open at the time. With the text off it moves
* to {@code sonic_port} or later. It is the same work either way.
*/
/** One layer per emission texture. See {@link #emissive} for why the sort is not optional. */
public static RenderLayer tardisEmissiveCullZOffset(Identifier texture) {
return SORT_SENSITIVE_EMISSION.contains(texture)
? EMISSIVE_SORTED.apply(texture)
: EMISSIVE_UNSORTED.apply(texture);
}

/**
* The emissive layer for geometry drawn at partial vertex alpha, where draw order is visible.
*
* <p>Two callers. {@code TardisStar} draws two nested star models into one batch, the outer at
* alpha 0.5 and the inner at alpha 1, with culling disabled so both faces of both shells are
* submitted; unsorted, the opaque core would land last and paint over the shell that is supposed
* to veil it. {@code ExteriorRenderer} draws the emission at the demat and remat fade alpha, which
* sweeps continuously through the partial range on every takeoff and landing.
*
* <p>The star is 24 quads, so sorting it costs nothing worth measuring. The exterior is around 342,
* and it pays the sort on every landed TARDIS even though only demat and remat need it. That is
* deliberate: picking per frame on the current alpha would hand out two different layer objects for
* one texture, and the moment one TARDIS is fading while another sits landed that alternates every
* frame, which costs more than the sort it saves.
*
* <p>Exterior emission identifiers already reach both layers, because {@code ExteriorRenderer} is
* here while {@code DoorRenderer}, {@code FlightTardisRenderer}, {@code FallingTardisRenderer},
* {@code SnowGlobeRenderer} and the BOTI paths draw the same textures unsorted. {@code Immediate}
* keys buffers on layer identity, so that costs one extra flush when both appear in a frame. It is
* accepted: no {@code AITRenderLayers} layer has a dedicated buffer in {@code BufferBuilderStorage},
* so every switch between them already flushes. Console emissions never collide, being a separate
* texture directory and registry.
*/
public static RenderLayer tardisEmissiveCullZOffsetSorted(Identifier texture) {
return EMISSIVE_SORTED.apply(texture);
return EMISSIVE.apply(texture);
}

private AITRenderLayers(String name, VertexFormat vertexFormat, VertexFormat.DrawMode drawMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ public static void renderStar(WorldRenderContext context, Tardis tardis) {
.rotationDegrees(delta));

TardisStarModel.getTexturedModelData().createModel().render(matrixStack,
provider.getBuffer(AITRenderLayers.tardisEmissiveCullZOffsetSorted(TARDIS_STAR_TEXTURE)),
provider.getBuffer(AITRenderLayers.tardisEmissiveCullZOffset(TARDIS_STAR_TEXTURE)),
LightmapTextureManager.MAX_LIGHT_COORDINATE, OverlayTexture.DEFAULT_UV, tardis.isGrowth() ? 0.1f : 1,
tardis.isGrowth() ? 0.1f : 1, tardis.isGrowth() ? 0.1f : 1, 0.5f);

matrixStack.scale(0.9f, 0.9f, 0.9f);
TardisStarModel.getTexturedModelData().createModel().render(matrixStack,
provider.getBuffer(AITRenderLayers.tardisEmissiveCullZOffsetSorted(TARDIS_STAR_TEXTURE)),
provider.getBuffer(AITRenderLayers.tardisEmissiveCullZOffset(TARDIS_STAR_TEXTURE)),
LightmapTextureManager.MAX_LIGHT_COORDINATE, OverlayTexture.DEFAULT_UV, 1, tardis.isGrowth() ? 0.2f : 1,
tardis.isGrowth() ? 0f : 1, 1f);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,10 @@ private void renderExterior(Profiler profiler, ClientTardis tardis, T entity, fl
? !power ? 0.01f : 0.3f
: u - colorAlpha;

// Sorted, not unsorted: alpha here is the demat and remat fade, which sweeps through the
// partial range where draw order is visible.
//
// TODO the guard above tests `emission`, which DOOM reassigns per rotation, but the layer
// below binds `variant.emission()`, the un-adjusted base. For DOOM those disagree. Left
// alone here because changing which texture DOOM binds is not part of this change.
model.renderWithAnimations(tardis, entity, this.model.getPart(), matrices, vertexConsumers.getBuffer(AITRenderLayers.tardisEmissiveCullZOffsetSorted(variant.emission())),
model.renderWithAnimations(tardis, entity, this.model.getPart(), matrices, vertexConsumers.getBuffer(AITRenderLayers.tardisEmissiveCullZOffset(variant.emission())),
0xF000F0, OverlayTexture.DEFAULT_UV, red, green, blue, alpha, tickDelta);
}
if (DependencyChecker.hasIris()) {
Expand Down
Loading