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 split(FormattedText input, int maxWidth) { SdfGlyphAtlas a = atlas(); - if (a == null) return List.of(FormattedCharSequence.forward(input.getString(), Style.EMPTY)); float s = scale(); List lines = wrapLines(a, input.getString(), maxWidth, s); List result = new ArrayList<>(lines.size()); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java index a5f44c4b..f3fad55f 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java @@ -1,6 +1,7 @@ package dev.anvilcraft.lib.v2.font; import dev.anvilcraft.lib.v2.font.screen.FontConfigScreen; +import dev.anvilcraft.lib.v2.font.sdf.SdfGlyphAtlas; import net.minecraft.resources.Identifier; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.ModContainer; @@ -9,6 +10,7 @@ import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import java.awt.Font; +import java.util.List; @EventBusSubscriber @Mod(value = AnvilLibFont.MOD_ID, dist = Dist.CLIENT) @@ -20,10 +22,22 @@ public class AnvilLibFont { public AnvilLibFont(ModContainer container) { AnvilLibFontConfig.AnvilLibFontConfigManager.readConfig(AnvilLibFont.CONFIG); container.registerExtensionPoint(IConfigScreenFactory.class, FontConfigScreen::new); + // Start building the SDF atlas for the base font and all common style + // variants on background threads at mod init time. This avoids render- + // thread blocking (visible lag) when bold/italic text is first drawn. + Font base = getSelectFont(); + SdfGlyphAtlas.getOrCreate(base); + for (int style : List.of(Font.BOLD, Font.ITALIC, Font.BOLD | Font.ITALIC)) { + SdfGlyphAtlas.getOrCreate(base.deriveFont(style)); + } } public static Font getSelectFont() { - return FontManager.INSTANCE.getFont(AnvilLibFont.CONFIG.getFont()); + Font font = FontManager.INSTANCE.getFont(AnvilLibFont.CONFIG.getFont()); + // Eagerly start building the SDF atlas on a background thread so it is + // ready (or nearly ready) by the time the first text is rendered. + SdfGlyphAtlas.getOrCreate(font); + return font; } public static Identifier of(String path) { diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java index 011cad80..9e455568 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java @@ -6,6 +6,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.neoforged.fml.loading.FMLLoader; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.io.File; @@ -41,6 +42,7 @@ public void setFont(String font) { AnvilLibFontConfigManager.saveConfig(this); } + @ApiStatus.Internal @Slf4j public static class AnvilLibFontConfigManager { public static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java index aee93ee6..071e3c43 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.font; +import com.google.common.annotations.Beta; import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; import lombok.Getter; @@ -19,6 +20,7 @@ import java.util.TreeSet; import javax.swing.UIManager; +@Beta @Slf4j @Getter public class FontManager { diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java index 5228bb2e..77d28408 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java @@ -8,7 +8,9 @@ import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.common.data.LanguageProvider; import net.neoforged.neoforge.data.event.GatherDataEvent; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal @EventBusSubscriber(modid = AnvilLibFont.MOD_ID) public class AnvilLibFontData { @SubscribeEvent diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java index 30928279..e5709c00 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.font.extension; +import com.google.common.annotations.Beta; import dev.anvilcraft.lib.v2.font.sdf.SdfTextRenderer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -11,6 +12,7 @@ import java.awt.Font; +@Beta public interface GuiGraphicsExtractorExtension { default GuiGraphicsExtractor self() { return (GuiGraphicsExtractor) this; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java index 004f6514..4477cacc 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java @@ -3,9 +3,11 @@ import dev.anvilcraft.lib.v2.font.extension.GuiGraphicsExtractorExtension; import dev.anvilcraft.lib.v2.font.sdf.SdfTextRenderer; import net.minecraft.client.gui.GuiGraphicsExtractor; +import org.jetbrains.annotations.ApiStatus; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; +@ApiStatus.Internal @Mixin(GuiGraphicsExtractor.class) public abstract class GuiGraphicsExtractorMixin implements GuiGraphicsExtractorExtension { @Unique diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java index d7f99f0c..1142e2fe 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java @@ -8,12 +8,14 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import net.neoforged.fml.ModContainer; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +@ApiStatus.Internal public class FontConfigScreen extends Screen { protected final Screen lastScreen; private Dropdown.@Nullable Shielding shielding; @@ -96,7 +98,7 @@ protected void init() { this.refreshFontOptions(this.familyDropdown.getValueId(), AnvilLibFont.CONFIG.getFont(), false); - this.familyDropdown.setValue(AnvilLibFont.CONFIG.getFont()); + this.fontDropdown.setValue(AnvilLibFont.CONFIG.getFont()); this.addRenderableWidget(this.testBtn); this.addRenderableWidget(this.fontDropdown); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java index 058c07b6..8ea2d4b5 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java @@ -6,8 +6,9 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Style; -import org.joml.Matrix3x2fStack; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal public class FontTestScreen extends Screen { protected final Screen lastScreen; @@ -30,7 +31,7 @@ public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mo ); graphics.anvillib$centeredText( AnvilLibFont.getSelectFont(), - Component.literal("č¯ˇä¸čρ兺闭äŊ įš„čŽĄįŽ—æœē").withStyle(ChatFormatting.GREEN), + Component.literal("č¯ˇä¸čρ兺闭äŊ įš„čŽĄįŽ—æœē").withStyle(Style.EMPTY.withColor(ChatFormatting.GREEN).withStrikethrough(true)), offsetX, offsetY + this.font.lineHeight, 0xFFFFFFFF @@ -60,7 +61,7 @@ public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mo graphics.anvillib$centeredText( AnvilLibFont.getSelectFont(), Component.literal("The quick brown fox jumped over the lazy dog.") - .withStyle(Style.EMPTY.withItalic(true).withColor(ChatFormatting.DARK_PURPLE)), + .withStyle(Style.EMPTY.withItalic(true).withColor(ChatFormatting.DARK_PURPLE).withUnderlined(true)), offsetX, offsetY + this.font.lineHeight * 5, 0xFFFFFFFF @@ -87,6 +88,14 @@ public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mo offsetY + this.font.lineHeight * 8, 0xFFFFFFFF ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("æˇˇæˇ†æ–‡å­—æĩ‹č¯•") + .withStyle(Style.EMPTY.withItalic(true).withBold(true).withObfuscated(true)), + offsetX, + offsetY + this.font.lineHeight * 9, + 0xFFFFFFFF + ); } @Override diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java index 15f9ce24..9b1db96d 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java @@ -10,6 +10,7 @@ import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.network.chat.Component; import net.minecraft.util.Util; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.util.ArrayList; @@ -18,6 +19,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; +@ApiStatus.Internal public class Dropdown extends AbstractWidget { private final List allows = new ArrayList<>(); private final Minecraft minecraft = Minecraft.getInstance(); @@ -164,7 +166,10 @@ protected void extractWidgetRenderState(GuiGraphicsExtractor guiGraphicsExtracto public int calcMaxHeight() { int startHeight = this.getY() + this.getHeight(); - return Math.clamp((long) this.allows.size() * this.getHeight(), 0, this.screenHeight - startHeight - 10); + int maxHeight = Math.clamp((long) this.allows.size() * this.getHeight(), 0, this.screenHeight - startHeight - 10); + // Round down to a multiple of the row height so the last visible row + // fills the entire area without trailing blank space. + return (maxHeight / this.getHeight()) * this.getHeight(); } private int visibleRowCount() { diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java index 11ef20a1..c73459e0 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java @@ -8,6 +8,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.texture.AbstractTexture; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.ApiStatus; import java.awt.image.BufferedImage; import java.util.Map; @@ -16,6 +17,7 @@ /** * Uploads SDF glyph atlas pages to GPU textures with LINEAR filtering. */ +@ApiStatus.Internal public final class SdfAtlasTexture { private static final Map CACHE = new ConcurrentHashMap<>(); @@ -28,19 +30,27 @@ private SdfAtlasTexture() { public static Identifier uploadPage(SdfGlyphAtlas atlas, int pageIndex) { SdfGlyphPage page = atlas.page(pageIndex); String key = atlas.key() + ".p" + pageIndex; - int hash = page.hash; + Identifier id = Identifier.fromNamespaceAndPath("anvillib_font", "dynamic/sdf_atlas/" + sanitize(key)); + // Synchronize on page to read the version and image data atomically + // with respect to the glyph creation thread, which also synchronizes + // on page for placeGlyph / fillPaddingForCell. + NativeImage nativeImage; + int version; + synchronized (page) { + version = page.version.get(); + nativeImage = toNativeImage(page.image); + page.dirty = false; + } PageEntry entry = CACHE.get(key); - if (entry != null && entry.hash == hash) return entry.id; + if (entry != null && entry.version == version) return entry.id; - Identifier id = Identifier.fromNamespaceAndPath("anvillib_font", "dynamic/sdf_atlas/" + sanitize(key)); - SdfTexture texture = new SdfTexture(toNativeImage(page.image)); + SdfTexture texture = new SdfTexture(nativeImage); Minecraft.getInstance().getTextureManager().register(id, texture); if (entry != null) entry.texture.close(); - CACHE.put(key, new PageEntry(id, texture, hash)); + CACHE.put(key, new PageEntry(id, texture, version)); page.textureId = id; - page.dirty = false; return id; } @@ -50,7 +60,12 @@ public static Identifier uploadPage(SdfGlyphAtlas atlas, int pageIndex) { public static void ensureUploaded(SdfGlyphAtlas atlas) { for (int i = 0; i < atlas.pageCount(); i++) { SdfGlyphPage page = atlas.page(i); - if (page.dirty || page.textureId == null) { + String key = atlas.key() + ".p" + i; + PageEntry entry = CACHE.get(key); + if (page.textureId == null + || entry == null + || entry.version != page.version.get() + || page.dirty) { uploadPage(atlas, i); } } @@ -100,25 +115,15 @@ private static String sanitize(String key) { return sb.toString(); } - public static int hashImage(BufferedImage image) { - int hash = 1; - for (int y = 0; y < image.getHeight(); y++) { - for (int x = 0; x < image.getWidth(); x++) { - hash = 31 * hash + image.getRGB(x, y); - } - } - return hash; - } - private static final class PageEntry { final Identifier id; final SdfTexture texture; - final int hash; + final int version; - PageEntry(Identifier id, SdfTexture texture, int hash) { + PageEntry(Identifier id, SdfTexture texture, int version) { this.id = id; this.texture = texture; - this.hash = hash; + this.version = version; } } } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java index 8f263ebe..8db7b9a9 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java @@ -1,5 +1,7 @@ package dev.anvilcraft.lib.v2.font.sdf; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.awt.Color; @@ -9,7 +11,6 @@ import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -17,6 +18,8 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Consumer; /** @@ -25,6 +28,8 @@ * Glyphs are packed into fixed-size 1024×1024 pages. ASCII 32-126 is * pre-warmed; all other codepoints are rendered lazily on first use. */ +@ApiStatus.Internal +@Slf4j public final class SdfGlyphAtlas { static final int PAGE_SIZE = 1024; private static final int FIRST_CHAR = 32; @@ -36,28 +41,36 @@ public final class SdfGlyphAtlas { private static final Map> CACHE = new ConcurrentHashMap<>(); + /** + * Per-task virtual-thread executor for background glyph creation. + * Each glyph (or batch) gets its own virtual thread; the existing + * {@code synchronized} blocks on the atlas and page objects already + * provide the necessary mutual exclusion. + */ + private static final ExecutorService GLYPH_EXECUTOR = Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("AnvilLib-SDF-Glyph-", 0).factory() + ); + private final String key; private final Font font; final int cellSize; final int padding; final int paddedCellSize; final float sdfRadius; - private int awtAscent; - private int awtHeight; + private final int awtAscent; + private final int awtHeight; private final FontMetrics fontMetrics; private final List pages = new ArrayList<>(); - private final Map glyphMap = new HashMap<>(); + private final Map glyphMap = new ConcurrentHashMap<>(); + private final Set pendingGlyphs = ConcurrentHashMap.newKeySet(); private SdfGlyphAtlas(String key, Font font) { this.key = key; this.font = font; - this.cellSize = Math.max(24, font.getSize() + 12); - this.sdfRadius = Math.max(12, font.getSize() * 0.25f); - this.padding = Math.max(4, this.cellSize / 6); - this.paddedCellSize = this.cellSize + 2 * this.padding; + log.info("SdfGlyphAtlas building for key={}, thread={}", key, Thread.currentThread().getName()); - // Capture font metrics + // Capture font metrics first — needed by cellSize calculation BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); Graphics2D g = tmp.createGraphics(); try { @@ -69,7 +82,18 @@ private SdfGlyphAtlas(String key, Font font) { g.dispose(); } + // Cell must accommodate full ascent + descent above and below baseline, + // plus 2px margin on each side. Previously used font.getSize()+12, which + // could clip descenders or ascenders for fonts with large extents. + int awtDescent = this.fontMetrics.getDescent(); + this.cellSize = Math.max(24, this.awtAscent + awtDescent + 4); + this.sdfRadius = Math.max(12, font.getSize() * 0.25f); + this.padding = Math.max(4, this.cellSize / 6); + this.paddedCellSize = this.cellSize + 2 * this.padding; + log.info("SdfGlyphAtlas cellSize={} ascent={} descent={} key={}", this.cellSize, this.awtAscent, awtDescent, key); + preWarmAscii(); + log.info("SdfGlyphAtlas ready for key={}, pages={}", key, this.pages.size()); } // ── Public API ────────────────────────────────────────────── @@ -80,11 +104,18 @@ private SdfGlyphAtlas(String key, Font font) { public static CompletableFuture getOrCreate(@Nullable Font font) { Font resolved = resolveFont(font); String key = resolved.getFontName(Locale.ENGLISH) + "." + resolved.getStyle() + "." + resolved.getSize(); - return CACHE.computeIfAbsent(key, _ -> CompletableFuture.supplyAsync(() -> new SdfGlyphAtlas(key, resolved))); + return CACHE.computeIfAbsent(key, _ -> { + log.debug("Starting SDF atlas build for key={}", key); + return CompletableFuture.supplyAsync(() -> new SdfGlyphAtlas(key, resolved), GLYPH_EXECUTOR); + }); } /** * Return the atlas if fully built, or {@code null} if still constructing. + *

+ * 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 getOrCreate(@Nullable Font font) CompletableFuture f = CACHE.get(key); if (f != null && f.isDone()) { try { - return f.get(); - } catch (Exception ignored) { + SdfGlyphAtlas atlas = f.get(); + return atlas; + } catch (Exception e) { + log.error("SDF atlas build failed for key={}, retrying", key, e); + CACHE.remove(key, f); + f = null; } } - if (f == null) getOrCreate(font); + if (f == null) { + log.info("No atlas future for key={}, starting build", key); + getOrCreate(font); + } return null; } @@ -115,7 +153,7 @@ public Font font() { } public int awtHeight() { - return this.font.getSize(); + return this.awtHeight; } public int awtAscent() { @@ -131,12 +169,40 @@ public SdfGlyphPage page(int index) { } /** - * Get (or lazily create) the glyph entry for a codepoint. + * Get the glyph entry for a codepoint. If the glyph is not yet in the + * atlas, requests async creation and returns {@code null}. The caller + * should handle the missing glyph gracefully (e.g. advance by a default + * width); the glyph will be available on the next call. */ public @Nullable GlyphEntry glyph(int codepoint, Consumer pageConsumer) { GlyphEntry entry = this.glyphMap.get(codepoint); if (entry != null) return entry; - return createGlyph(codepoint, pageConsumer); + // Trigger async creation on background thread + if (this.pendingGlyphs.add(codepoint)) { + GLYPH_EXECUTOR.submit(() -> createGlyphAsync(codepoint)); + } + return null; + } + + /** + * Background-thread entry point for glyph creation. + * Synchronized to ensure only one glyph is created at a time per atlas, + * avoiding races on page state (nextCol, nextRow, image pixels). + */ + private void createGlyphAsync(int codepoint) { + try { + synchronized (this) { + // Double-check: may have been created since we were enqueued + if (this.glyphMap.containsKey(codepoint)) return; + createGlyph(codepoint, SdfGlyphPage::updateHash); + } + // Invalidate cached layouts so they pick up the new glyph + SdfTextLayout.invalidateAtlas(this.key); + } catch (Exception e) { + log.error("Failed to create SDF glyph for codepoint {} (U+{})", codepoint, Integer.toHexString(codepoint).toUpperCase(), e); + } finally { + this.pendingGlyphs.remove(codepoint); + } } public int measureText(String text) { @@ -169,9 +235,8 @@ public int measureCodepoint(int codepoint) { // ── Glyph creation ────────────────────────────────────────── - private @Nullable GlyphEntry createGlyph(int codepoint, Consumer pageConsumer) { + private GlyphEntry createGlyph(int codepoint, Consumer pageConsumer) { BufferedImage mask = renderMask(codepoint); - if (mask == null) return null; int pageIdx = findOrCreatePageIndex(); SdfGlyphPage page = this.pages.get(pageIdx); @@ -221,7 +286,7 @@ private BufferedImage renderMask(int codepoint) { g.setColor(new Color(0, 0, 0, 0)); g.fillRect(0, 0, this.cellSize, this.cellSize); g.setColor(Color.WHITE); - g.drawString(s, 2, Math.min(this.cellSize - 4, this.awtAscent + 2)); + g.drawString(s, 2, this.awtAscent + 2); } finally { g.dispose(); } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java index 955143fe..03fbc16b 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java @@ -1,20 +1,23 @@ package dev.anvilcraft.lib.v2.font.sdf; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.ApiStatus; import java.awt.image.BufferedImage; +import java.util.concurrent.atomic.AtomicInteger; /** * A single 1024×1024 atlas page holding packed glyphs. */ +@ApiStatus.Internal public final class SdfGlyphPage { private static final int SIZE = SdfGlyphAtlas.PAGE_SIZE; final BufferedImage image; - int hash = 1; + final AtomicInteger version = new AtomicInteger(); final int cols, rows; int nextCol, nextRow; Identifier textureId; - boolean dirty = true; + volatile boolean dirty = true; SdfGlyphPage(int paddedCellSize) { this.cols = SIZE / paddedCellSize; @@ -26,7 +29,7 @@ boolean hasSpace() { return nextRow < rows; } - SdfGlyphAtlas.GlyphEntry placeGlyph(SdfGlyphAtlas atlas, BufferedImage mask) { + synchronized SdfGlyphAtlas.GlyphEntry placeGlyph(SdfGlyphAtlas atlas, BufferedImage mask) { int col = nextCol, row = nextRow; int padX = col * atlas.paddedCellSize; int padY = row * atlas.paddedCellSize; @@ -42,7 +45,7 @@ SdfGlyphAtlas.GlyphEntry placeGlyph(SdfGlyphAtlas atlas, BufferedImage mask) { return new SdfGlyphAtlas.GlyphEntry(0, innerX, innerY, atlas.cellSize, atlas.cellSize, 0); } - void fillPaddingForCell(SdfGlyphAtlas atlas, SdfGlyphAtlas.GlyphEntry e) { + synchronized void fillPaddingForCell(SdfGlyphAtlas atlas, SdfGlyphAtlas.GlyphEntry e) { int col = (e.atlasX() - atlas.padding) / atlas.paddedCellSize; int row = (e.atlasY() - atlas.padding) / atlas.paddedCellSize; SdfGlyphAtlas.fillPadding( @@ -56,6 +59,6 @@ void fillPaddingForCell(SdfGlyphAtlas atlas, SdfGlyphAtlas.GlyphEntry e) { } public void updateHash() { - this.hash = SdfAtlasTexture.hashImage(this.image); + this.version.incrementAndGet(); } } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java index 434825ad..8c7b207c 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java @@ -1,18 +1,29 @@ package dev.anvilcraft.lib.v2.font.sdf; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** * CPU-side glyph layout output for SDF text rendering, grouped by atlas page. + *

+ * 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 LAYOUT_CACHE = new ConcurrentHashMap<>(); + private final List pages; private final int width; private final int height; @@ -23,12 +34,60 @@ private SdfTextLayout(List pages, int width, int height) { this.height = height; } + /** + * Compute glyph layout for a string. The returned quads have positions + * relative to (0, 0); add {@code x}/{@code y} when rendering. + *

+ * 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> buckets = new java.util.LinkedHashMap<>(); + boolean allAvailable = true; Set glyphPages = new HashSet<>(); for (int ci = 0; ci < text.length(); ) { @@ -37,6 +96,7 @@ public static SdfTextLayout fromAtlas(SdfGlyphAtlas atlas, @Nullable String text SdfGlyphAtlas.GlyphEntry glyph = atlas.glyph(cp, glyphPages::add); if (glyph == null) { + allAvailable = false; penX += Math.round(Math.max(6, atlas.font().getSize() / 2) * scale); continue; } @@ -53,7 +113,8 @@ public static SdfTextLayout fromAtlas(SdfGlyphAtlas atlas, @Nullable String text int w = Math.round(glyph.width() * scale); int h = Math.round(glyph.height() * scale); - GlyphQuad quad = new GlyphQuad(penX, y, penX + w, y + h, u0, v0, u1, v1, (char) cp); + // Positions relative to layout origin (0, 0) + GlyphQuad quad = new GlyphQuad(penX, 0, penX + w, h, u0, v0, u1, v1, (char) cp); buckets.computeIfAbsent(glyph.pageIndex(), _ -> new ArrayList<>()).add(quad); penX += Math.max(1, Math.round(glyph.advance() * scale)); @@ -69,14 +130,21 @@ public static SdfTextLayout fromAtlas(SdfGlyphAtlas atlas, @Nullable String text pages.add(new PageQuads(pi, tex, page.image.getWidth(), page.image.getHeight(), entry.getValue())); } - totalWidth = Math.max(0, penX - x); - return new SdfTextLayout(pages, totalWidth, maxHeight); + SdfTextLayout layout = new SdfTextLayout(pages, penX, maxHeight); + layout.allGlyphsAvailable = allAvailable; + return layout; } + private boolean allGlyphsAvailable = true; + + private static final SdfTextLayout EMPTY = new SdfTextLayout(List.of(), 0, 0); + public List pages() { return Collections.unmodifiableList(this.pages); } public int width() { return this.width; } public int height() { return this.height; } + private record CacheKey(String atlasKey, String text, int scaleInt) {} + public record PageQuads(int pageIndex, @Nullable Identifier atlasTexture, int pageWidth, int pageHeight, List quads) {} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java index 42bffae2..187e0d38 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.font.sdf; +import com.google.common.annotations.Beta; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.textures.FilterMode; import com.mojang.blaze3d.textures.GpuSampler; @@ -10,13 +11,12 @@ import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.Style; import net.minecraft.util.FormattedCharSequence; +import org.joml.Matrix3x2f; +import org.joml.Matrix3x2fStack; import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.awt.Font; import java.util.List; -import org.joml.Matrix3x2f; /** * SDF text renderer that draws strings via the SDF text render pipeline. @@ -24,14 +24,22 @@ * Uses a CPU-generated SDF glyph atlas uploaded to a GPU texture, * sampled by a custom fragment shader for smooth anti-aliased text. */ +@Beta public final class SdfTextRenderer { - private static final Logger LOGGER = LoggerFactory.getLogger(SdfTextRenderer.class); - private final GpuSampler diffuseSampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR); public SdfTextRenderer() { } + /** + * Get the SDF atlas for a font, blocking until it is ready on first access. + * The atlas is cached forever, so only the very first call for a particular + * font blocks (typically ~200-400 ms while the glyph atlas is built). + */ + private static SdfGlyphAtlas getAtlas(@Nullable Font font) { + return SdfGlyphAtlas.getOrCreate(font).join(); + } + public void drawString( GuiGraphicsExtractor graphics, @Nullable Font font, @@ -42,20 +50,19 @@ public void drawString( boolean dropShadow ) { if (text == null || text.isEmpty()) return; - SdfGlyphAtlas atlas = SdfGlyphAtlas.getIfReady(font); - if (atlas == null) return; - drawStringWithAtlas(graphics, atlas, text, x, y, color); + drawStringWithAtlas(graphics, getAtlas(font), text, x, y, color); } - private void drawStringWithAtlas(GuiGraphicsExtractor graphics, SdfGlyphAtlas atlas, - String text, int x, int y, int color) { + private void drawStringWithAtlas( + GuiGraphicsExtractor graphics, SdfGlyphAtlas atlas, + String text, int x, int y, int color + ) { float scale = scaleFor(atlas); - int quadY = y - 2; - SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, quadY, scale); - if (layout.pages().isEmpty()) return; SdfAtlasTexture.ensureUploaded(atlas); + SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, y, scale); + if (layout.pages().isEmpty()) return; for (SdfTextLayout.PageQuads pq : layout.pages()) { - drawAtlasPipeline(graphics, pq, this.diffuseSampler, color); + drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, y); } } @@ -103,7 +110,10 @@ public void drawFormatted( int color, boolean dropShadow ) { - int[] pen = {x, x}; + int[] pen = { + x, + x + }; StringBuilder buf = new StringBuilder(); int[] segColor = {color}; boolean[] segBold = {false}; @@ -145,15 +155,13 @@ public void drawFormatted( } private int flushFormattedSegment(GuiGraphicsExtractor graphics, @Nullable Font font, String text, int x, int y, int color) { - SdfGlyphAtlas atlas = SdfGlyphAtlas.getIfReady(font); - if (atlas == null) return x; + SdfGlyphAtlas atlas = getAtlas(font); float scale = scaleFor(atlas); - int quadY = y - 2; - SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, quadY, scale); SdfAtlasTexture.ensureUploaded(atlas); + SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, y, scale); for (SdfTextLayout.PageQuads pq : layout.pages()) { if (pq.atlasTexture() != null && !pq.quads().isEmpty()) { - drawAtlasPipeline(graphics, pq, this.diffuseSampler, color); + drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, y); } } return x + layout.width(); @@ -173,14 +181,18 @@ private static void drawDecorations( ) { if (x1 <= x0) return; int lh = Minecraft.getInstance().font.lineHeight; + Matrix3x2fStack pose = graphics.pose(); + pose.pushMatrix(); + pose.scale(1.0f, 0.5f); if (strikethrough) { - int sy = y + lh / 2; + int sy = (y + lh / 2) * 2; graphics.fill(x0, sy, x1, sy + 1, color); } if (underline) { - int sy = y + lh; + int sy = (y + lh - 2) * 2; graphics.fill(x0, sy, x1, sy + 1, color); } + pose.popMatrix(); } public void drawWrapped( @@ -193,8 +205,7 @@ public void drawWrapped( int color, boolean dropShadow ) { - SdfGlyphAtlas atlas = SdfGlyphAtlas.getIfReady(font); - if (atlas == null) return; + SdfGlyphAtlas atlas = getAtlas(font); float scale = scaleFor(atlas); List lines = wrapLines(atlas, text.getString(), width, scale); int lineHeight = Minecraft.getInstance().font.lineHeight; @@ -209,8 +220,7 @@ public void drawCentered(GuiGraphicsExtractor graphics, @Nullable Font font, Com public void drawCentered(GuiGraphicsExtractor graphics, @Nullable Font font, FormattedCharSequence text, int x, int y, int color) { String value = flattenToString(text); - SdfGlyphAtlas atlas = SdfGlyphAtlas.getIfReady(font); - if (atlas == null) return; + SdfGlyphAtlas atlas = getAtlas(font); float scale = scaleFor(atlas); int drawX = x - Math.round(atlas.measureText(value) * scale) / 2; this.drawFormatted(graphics, font, text, drawX, y, color, false); @@ -237,7 +247,9 @@ private static void drawAtlasPipeline( GuiGraphicsExtractor graphics, SdfTextLayout.PageQuads pq, GpuSampler diffuseSampler, - int color + int color, + int originX, + int originY ) { if (pq.atlasTexture() == null || pq.quads().isEmpty()) return; SdfTextRenderState state = new SdfTextRenderState( @@ -248,6 +260,8 @@ private static void drawAtlasPipeline( pq.pageWidth(), pq.pageHeight(), color, + originX, + originY, graphics.peekScissorStack() ); graphics.submitGuiElementRenderState(state); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java index 64e1141f..a13b702e 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java @@ -12,18 +12,18 @@ import net.minecraft.client.gui.render.TextureSetup; import net.minecraft.client.renderer.texture.AbstractTexture; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.ApiStatus; import org.joml.Matrix3x2f; import org.jspecify.annotations.Nullable; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Render state for SDF text rendering. * *

Coordinates are in screen space and will be transformed via the pose matrix.

*/ +@ApiStatus.Internal public record SdfTextRenderState( Matrix3x2f pose, List glyphs, @@ -32,10 +32,10 @@ public record SdfTextRenderState( int atlasWidth, int atlasHeight, int color, + int originX, + int originY, @Nullable ScreenRectangle scissorArea ) implements LibGuiElementRenderState { - private static final Logger LOGGER = LoggerFactory.getLogger(SdfTextRenderState.class); - @Override public RenderPipeline pipeline() { return ALFPipelines.SDF_TEXT; @@ -55,14 +55,16 @@ public void executeDrawAfterSetPipeline(RenderPass renderPass) { @Override public void buildVertices(VertexConsumer consumer) { + int ox = this.originX; + int oy = this.originY; for (SdfTextLayout.GlyphQuad quad : this.glyphs) { // Build quad vertices in screen space // The pose matrix will handle transformation to clip space - float x0 = quad.x0(); - float y0 = quad.y0(); - float x1 = quad.x1(); - float y1 = quad.y1(); + float x0 = quad.x0() + ox; + float y0 = quad.y0() + oy; + float x1 = quad.x1() + ox; + float y1 = quad.y1() + oy; float u0 = quad.u0(); float v0 = quad.v0(); diff --git a/roseau.yaml b/roseau.yaml new file mode 100644 index 00000000..1f196728 --- /dev/null +++ b/roseau.yaml @@ -0,0 +1,9 @@ +common: + excludes: + annotations: + - name: java.lang.Deprecated + - name: com.google.common.annotations.Beta + - name: org.jetbrains.annotations.ApiStatus$Experimental + - name: org.jetbrains.annotations.ApiStatus$Internal + - name: org.jetbrains.annotations.ApiStatus$Obsolete + - name: org.jetbrains.annotations.ApiStatus$ScheduledForRemoval