diff --git a/src/main/java/dev/amble/ait/client/renderers/AITRenderLayers.java b/src/main/java/dev/amble/ait/client/renderers/AITRenderLayers.java index 9b16f1ed0..8c59b8f2b 100644 --- a/src/main/java/dev/amble/ait/client/renderers/AITRenderLayers.java +++ b/src/main/java/dev/amble/ait/client/renderers/AITRenderLayers.java @@ -1,6 +1,5 @@ package dev.amble.ait.client.renderers; -import java.util.Set; import java.util.function.Function; import net.fabricmc.api.EnvType; @@ -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. + * + *

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. + * + *

That is independent of the texture's alpha. Binary alpha makes the blend 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. + * + *

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) @@ -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. - * - *

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 SORT_SENSITIVE_EMISSION = Set.of( - AITMod.id("textures/blockentities/consoles/hourglass_default_emission.png")); + private static final Function EMISSIVE = Util.memoize(AITRenderLayers::emissive); - private static final Function EMISSIVE_SORTED = Util - .memoize(texture -> emissive(texture, true)); - - private static final Function EMISSIVE_UNSORTED = Util - .memoize(texture -> emissive(texture, false)); - - /** - * The emissive layer for geometry drawn at full vertex alpha, which is every caller but two. - * - *

Returns the sorted layer anyway for the textures in {@link #SORT_SENSITIVE_EMISSION}. - * - *

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. - * - *

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. - * - *

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. - * - *

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. - * - *

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, diff --git a/src/main/java/dev/amble/ait/client/renderers/TardisStar.java b/src/main/java/dev/amble/ait/client/renderers/TardisStar.java index e8e6eba45..ef22d531e 100644 --- a/src/main/java/dev/amble/ait/client/renderers/TardisStar.java +++ b/src/main/java/dev/amble/ait/client/renderers/TardisStar.java @@ -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); diff --git a/src/main/java/dev/amble/ait/client/renderers/exteriors/ExteriorRenderer.java b/src/main/java/dev/amble/ait/client/renderers/exteriors/ExteriorRenderer.java index 1516cebdc..39125adf2 100644 --- a/src/main/java/dev/amble/ait/client/renderers/exteriors/ExteriorRenderer.java +++ b/src/main/java/dev/amble/ait/client/renderers/exteriors/ExteriorRenderer.java @@ -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()) {