diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 33dc5af0..b52eb1a9 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -363,6 +363,12 @@ jobs: MAIN_BC: ${{ needs.main-roseau.outputs.has_bc }} MAIN_N: ${{ needs.main-roseau.outputs.bc_count }} steps: + - name: Download Roseau reports + uses: actions/download-artifact@v4 + with: + pattern: roseau-* + path: roseau-reports + - name: Generate and post PR comment run: | row() { @@ -398,9 +404,50 @@ jobs: row "wheel" "$WHEEL_BC" "$WHEEL_N" row "main" "$MAIN_BC" "$MAIN_N" echo "" - echo "> Detailed reports: see the *Artifacts* section of [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." } > /tmp/roseau-body.md + # Append breaking change details for each affected module + append_details() { + local mod=$1 bc=$2 count=$3 + if [ "$bc" != "true" ]; then return; fi + local csv="roseau-reports/roseau-${mod}/report.csv" + if [ ! -f "$csv" ]; then return; fi + { + echo "### đ´ ${mod} â ${count} breaking change(s)" + echo "" + while IFS=';' read -r type symbol kind nature location newSymbol binaryBreaking sourceBreaking; do + echo "#### \`${location}\`" + echo '```' + echo "${type}" + echo "${symbol}" + echo "${kind}" + [ "$binaryBreaking" = "true" ] && echo "â binary-breaking" || echo "â binary-compatible" + [ "$sourceBreaking" = "true" ] && echo "â source-breaking" || echo "â source-compatible" + echo '```' + echo "" + done < <(tail -n +2 "$csv") + } >> /tmp/roseau-body.md + } + + append_details "codec" "$CODEC_BC" "$CODEC_N" + append_details "collision" "$COLLISION_BC" "$COLLISION_N" + append_details "config" "$CONFIG_BC" "$CONFIG_N" + append_details "font" "$FONT_BC" "$FONT_N" + append_details "integration" "$INTEGRATION_BC" "$INTEGRATION_N" + append_details "moveable-entity-block" "$MEB_BC" "$MEB_N" + append_details "multiblock" "$MULTIBLOCK_BC" "$MULTIBLOCK_N" + append_details "network" "$NETWORK_BC" "$NETWORK_N" + append_details "recipe" "$RECIPE_BC" "$RECIPE_N" + append_details "registrum" "$REGISTRUM_BC" "$REGISTRUM_N" + append_details "rendering" "$RENDERING_BC" "$RENDERING_N" + append_details "space-select" "$SPACESELECT_BC" "$SPACESELECT_N" + append_details "sync" "$SYNC_BC" "$SYNC_N" + append_details "util" "$UTIL_BC" "$UTIL_N" + append_details "wheel" "$WHEEL_BC" "$WHEEL_N" + append_details "main" "$MAIN_BC" "$MAIN_N" + + echo "> Full CSVs: see the *Artifacts* section of [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." >> /tmp/roseau-body.md + - name: Find existing Roseau comment id: find-comment uses: peter-evans/find-comment@v3 diff --git a/.github/workflows/roseau_check.yml b/.github/workflows/roseau_check.yml index a3a3d052..c07d50b8 100644 --- a/.github/workflows/roseau_check.yml +++ b/.github/workflows/roseau_check.yml @@ -48,7 +48,9 @@ jobs: - name: Run roseauCheck id: roseau continue-on-error: true - run: ./gradlew :${{ inputs.module_id }}-neoforge-26.1:roseauCheck + run: | + chmod +x ./gradlew + ./gradlew :${{ inputs.module_id }}-neoforge-26.1:roseauCheck - name: Collect result id: result diff --git a/gradle/scripts/roseau.gradle b/gradle/scripts/roseau.gradle index ec0ca4e6..99f63b4b 100644 --- a/gradle/scripts/roseau.gradle +++ b/gradle/scripts/roseau.gradle @@ -41,7 +41,9 @@ def roseauBaselineVersion = project.findProperty('roseauBaselineVersion') ?: { def roseauMvnCoord = "${roseauGroup}:${roseauArtifact}:${roseauBaselineVersion}" def roseauBaselineDep = dependencies.create(group: roseauGroup, name: roseauArtifact, version: roseauBaselineVersion) def roseauBaselineConfig = configurations.detachedConfiguration(roseauBaselineDep) +roseauBaselineConfig.transitive = false def roseauBaselineJarProvider = provider { roseauBaselineConfig.singleFile } +def roseauConfig = rootProject.file("roseau.yaml").absolutePath tasks.register('roseauCheck', JavaExec) { group = 'verification' @@ -71,6 +73,7 @@ tasks.register('roseauCheck', JavaExec) { '--fail-on-bc', '--report', "HTML=${new File(reportsDir, 'report.html').absolutePath}", '--report', "CSV=${new File(reportsDir, 'report.csv').absolutePath}", + '--config', roseauConfig ) } } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java index e379e075..58d52082 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java @@ -6,12 +6,12 @@ import com.mojang.blaze3d.shaders.UniformType; import com.mojang.blaze3d.vertex.VertexFormat; import com.mojang.blaze3d.vertex.VertexFormatElement; +import lombok.extern.slf4j.Slf4j; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.client.event.RegisterRenderPipelinesEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jetbrains.annotations.ApiStatus; /** * Font module render pipeline registration. @@ -19,10 +19,10 @@ *
The pipeline is ready for SDF text quad rendering and will be consumed by the * runtime text render state in a follow-up step.
*/ +@ApiStatus.Internal +@Slf4j @EventBusSubscriber(modid = AnvilLibFont.MOD_ID, value = Dist.CLIENT) public final class ALFPipelines { - private static final Logger LOGGER = LoggerFactory.getLogger(ALFPipelines.class); - public static final VertexFormat SDF_TEXT_FORMAT = VertexFormat.builder() .add("Position", VertexFormatElement.POSITION) .add("Color", VertexFormatElement.COLOR) @@ -47,7 +47,7 @@ private ALFPipelines() { @SubscribeEvent public static void on(RegisterRenderPipelinesEvent event) { event.registerPipeline(SDF_TEXT); - LOGGER.info("Registered SDF_TEXT pipeline: {}", SDF_TEXT); + log.info("Registered SDF_TEXT pipeline: {}", SDF_TEXT); } } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java index 67eaecfd..7c15cbb0 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.font; +import com.google.common.annotations.Beta; import dev.anvilcraft.lib.v2.font.sdf.SdfGlyphAtlas; import net.minecraft.client.Minecraft; import net.minecraft.network.chat.FormattedText; @@ -10,12 +11,12 @@ import java.awt.Font; import java.util.ArrayList; import java.util.List; -import org.jspecify.annotations.Nullable; /** * Wraps {@link Font} with text measurement and layout utilities that * delegate to {@link SdfGlyphAtlas} for glyph metrics. */ +@Beta public class ALFont { public final int lineHeight = Minecraft.getInstance().font.lineHeight; private final Font font; @@ -26,19 +27,16 @@ public ALFont(Font font) { public Font awtFont() { return this.font; } - private @Nullable SdfGlyphAtlas atlas() { return SdfGlyphAtlas.getIfReady(this.font); } - private SdfGlyphAtlas atlasBlocking() { return SdfGlyphAtlas.getOrCreate(this.font).join(); } + private SdfGlyphAtlas atlas() { return SdfGlyphAtlas.getOrCreate(this.font).join(); } private float scale() { - SdfGlyphAtlas a = atlas(); - return a == null ? 1f : (float) lineHeight / a.awtHeight(); + return (float) lineHeight / atlas().awtHeight(); } // ââ Width measurement âââââââââââââââââââââââââââââââââââââââ public int width(String str) { - SdfGlyphAtlas a = atlas(); - return a == null ? 0 : Mth.ceil(a.measureText(str) * scale()); + return Mth.ceil(atlas().measureText(str) * scale()); } public int width(FormattedText text) { return width(text.getString()); } @@ -58,7 +56,6 @@ public String plainSubstrByWidth(String str, int maxWidth) { private String plainHeadByWidth(String str, int maxWidth) { SdfGlyphAtlas a = atlas(); - if (a == null) return str; float s = scale(); if (a.measureText(str) * s <= maxWidth) return str; int lo = 0, hi = str.length(); @@ -72,7 +69,6 @@ private String plainHeadByWidth(String str, int maxWidth) { private String plainTailByWidth(String str, int maxWidth) { SdfGlyphAtlas a = atlas(); - if (a == null) return str; float s = scale(); if (a.measureText(str) * s <= maxWidth) return str; int lo = 0, hi = str.length(); @@ -92,7 +88,6 @@ public FormattedText substrByWidth(FormattedText text, int maxWidth) { public List
+ * If the previous build failed (future completed exceptionally), the
+ * failed future is removed and a fresh build is started so that a
+ * transient error does not permanently disable the atlas.
*/
public static @Nullable SdfGlyphAtlas getIfReady(@Nullable Font font) {
Font resolved = resolveFont(font);
@@ -92,11 +123,18 @@ public static CompletableFuture
+ * Layouts are cached by {@code (atlasKey, text, scale)} to avoid redundant
+ * computation each frame. Quads are stored with positions relative to the
+ * layout origin; the renderer supplies the screen-space offset.
*/
+@ApiStatus.Internal
public final class SdfTextLayout {
+ private static final int MAX_CACHE_SIZE = 1024;
+ private static final Map
+ * Result is cached when all glyphs are available in the atlas.
+ */
public static SdfTextLayout fromAtlas(SdfGlyphAtlas atlas, @Nullable String text, int x, int y, float scale) {
if (text == null || text.isEmpty())
- return new SdfTextLayout(List.of(), 0, 0);
+ return EMPTY;
+
+ int scaleInt = Math.round(scale * 1000f);
+ CacheKey key = new CacheKey(atlas.key(), text, scaleInt);
+ SdfTextLayout cached = LAYOUT_CACHE.get(key);
+ if (cached != null) return cached;
+
+ SdfTextLayout layout = computeLayout(atlas, text, scale, scaleInt);
+ if (layout != null && layout.allGlyphsAvailable) {
+ evictIfNeeded();
+ LAYOUT_CACHE.put(key, layout);
+ }
+ return layout;
+ }
+
+ /**
+ * Invalidate cached layouts for a specific atlas (e.g. after glyph additions).
+ */
+ public static void invalidateAtlas(String atlasKey) {
+ LAYOUT_CACHE.keySet().removeIf(k -> k.atlasKey.equals(atlasKey));
+ }
+
+ /**
+ * Clear all cached layouts.
+ */
+ public static void clearCache() {
+ LAYOUT_CACHE.clear();
+ }
+
+ private static void evictIfNeeded() {
+ if (LAYOUT_CACHE.size() >= MAX_CACHE_SIZE) {
+ // Evict half the entries (simple random-ish eviction via iterator)
+ var it = LAYOUT_CACHE.keySet().iterator();
+ int toRemove = MAX_CACHE_SIZE / 2;
+ for (int i = 0; i < toRemove && it.hasNext(); i++) {
+ it.next();
+ it.remove();
+ }
+ }
+ }
- int penX = x, maxHeight = 0, totalWidth = 0;
+ @Nullable
+ private static SdfTextLayout computeLayout(SdfGlyphAtlas atlas, String text, float scale, int scaleInt) {
+ int penX = 0, maxHeight = 0;
java.util.Map Coordinates are in screen space and will be transformed via the pose matrix.