nosingleplayer 1.2.0 : textures créées par MC (fix menu noir Windows/NVIDIA)
Le menu titre rendait entièrement noir sur les PC Windows + NVIDIA (2 postes) : le mod créait sa texture (DynamicTexture) et son sampler pendant l'extraction du rendu, et blittait une texture hors atlas. Correctifs : - la texture est désormais enregistrée via le mécanisme officiel de MC (ReloadableTexture + TextureContents) : c'est MC qui crée la texture GPU ET le sampler, comme pour ses propres textures. Plus de sampler custom, plus d'upload manuel ; - la création de la texture GPU est déplacée dans TitleScreen.init, donc HORS de l'enregistrement des commandes de la frame (le créer pendant l'extraction invalide le frame entier sur les backends à command encoder => écran noir) ; - fond réduit de 3840x2400 à 1920x1200 (36 Mo -> 9 Mo de VRAM) ; - suppression des 6 panorama_*.png embarqués : MC 26.3 n'expose pas les ressources des mods au ResourceManager, ils n'ont jamais été rendus (vérifié : le panorama vanilla'affiche le stock Mojang) ; - jar réduit de 3,6 Mo à 871 Ko. Vérifié par capture d'écran en jeu : menu OK en OpenGL (Mesa/AMD) et en Vulkan (lavapipe). atlas : distribution.json regénérée (nosingleplayer 1.2.0, MD5/URL à jour) ; retrait de spatial-gui du pack (fait par le repo, documenté dans server/mods/SOURCE.md).
@@ -5,7 +5,7 @@ loader_version=0.20.0
|
||||
loom_version=1.19.1
|
||||
|
||||
# Mod Properties
|
||||
mod_version=1.1.9
|
||||
mod_version=1.2.0
|
||||
maven_group=fr.surviepote
|
||||
archives_base_name=nosingleplayer
|
||||
|
||||
|
||||
@@ -1,69 +1,74 @@
|
||||
package fr.surviepote.nosingleplayer;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureContents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/**
|
||||
* Chargement des textures du mod.
|
||||
* Enregistrement des textures du mod dans le TextureManager de MC.
|
||||
*
|
||||
* <p>MC 26.3 n'expose plus les ressources des mods au ResourceManager : les packs ne
|
||||
* sont lus que depuis des dossiers, pas depuis les jars du classpath. Une texture
|
||||
* référencée par son Identifier est donc introuvable (placeholder magenta). On lit
|
||||
* donc le PNG directement dans le jar du mod et on enregistre une DynamicTexture dans
|
||||
* le TextureManager.
|
||||
* <p>IMPORTANT : l'enregistrement doit être fait sur le thread de rendu mais
|
||||
* <b>en dehors</b> de l'extraction du rendu (ex. dans {@code TitleScreen.init}).
|
||||
* Créer une ressource GPU pendant l'enregistrement des commandes d'une frame
|
||||
* invalide le frame entier sur les backends à command encoder (Vulkan/NVIDIA) :
|
||||
* résultat, un menu titre entièrement noir. D'où le try/catch + le repli sur le
|
||||
* panorama vanilla dans TitleScreenRenderMixin.
|
||||
*/
|
||||
public final class ModTextures {
|
||||
|
||||
private static final Map<Identifier, DynamicTexture> CACHE = new HashMap<>();
|
||||
private static final Map<Identifier, SurviePoteTexture> CACHE = new HashMap<>();
|
||||
|
||||
private ModTextures() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renvoie la texture (chargée depuis le jar du mod), ou null si le fichier est absent.
|
||||
* Enregistre une texture lue dans le jar du mod.
|
||||
*
|
||||
* @param id identifiant sous lequel la texture est enregistrée
|
||||
* @param resourcePath chemin dans le jar, ex {@code /assets/nosingleplayer/...}
|
||||
* @param name nom affiché dans les crash reports GPU
|
||||
* @param linear true = filtrage linéaire (images agrandies), false = nearest (pixel art)
|
||||
* @param blur true = filtrage linéaire, false = nearest (pixel art)
|
||||
* @return true si la texture est utilisable
|
||||
*/
|
||||
public static DynamicTexture get(Identifier id, String resourcePath, String name, boolean linear) {
|
||||
DynamicTexture texture = CACHE.get(id);
|
||||
var textures = Minecraft.getInstance().getTextureManager();
|
||||
if (texture != null && textures.getTexture(id) == texture) {
|
||||
return texture; // déjà chargée et toujours enregistrée
|
||||
public static boolean register(Identifier id, String resourcePath, boolean blur) {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
SurviePoteTexture cached = CACHE.get(id);
|
||||
if (cached != null && minecraft.getTextureManager().getTexture(id) == cached) {
|
||||
return true; // déjà enregistrée et toujours en place
|
||||
}
|
||||
try (InputStream in = ModTextures.class.getResourceAsStream(resourcePath)) {
|
||||
if (in == null) {
|
||||
NoSingleplayer.LOGGER.error("[SurviePote] Texture introuvable dans le jar : {}", resourcePath);
|
||||
return null;
|
||||
}
|
||||
NativeImage image = NativeImage.read(in);
|
||||
texture = linear
|
||||
? new LinearTexture(() -> name, image)
|
||||
: new DynamicTexture(() -> name, image);
|
||||
try {
|
||||
SurviePoteTexture texture = new SurviePoteTexture(id, resourcePath, blur);
|
||||
TextureContents contents = texture.loadContents(minecraft.getResourceManager());
|
||||
texture.apply(contents);
|
||||
minecraft.getTextureManager().register(id, texture);
|
||||
CACHE.put(id, texture);
|
||||
textures.register(id, texture);
|
||||
return texture;
|
||||
} catch (IOException e) {
|
||||
NoSingleplayer.LOGGER.error("[SurviePote] Échec de chargement de la texture " + resourcePath, e);
|
||||
return null;
|
||||
NoSingleplayer.LOGGER.info("[SurviePote] Texture enregistrée : {} ({}x{})",
|
||||
id, texture.getWidth(), texture.getHeight());
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
NoSingleplayer.LOGGER.error("[SurviePote] Échec d'enregistrement de la texture " + resourcePath, t);
|
||||
CACHE.remove(id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** DynamicTexture avec filtrage linéaire (utile quand l'image est réduite à l'écran). */
|
||||
private static final class LinearTexture extends DynamicTexture {
|
||||
LinearTexture(java.util.function.Supplier<String> name, NativeImage image) {
|
||||
super(name, image);
|
||||
this.sampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR);
|
||||
}
|
||||
/** True si la texture est enregistrée ET encore présente dans le TextureManager. */
|
||||
public static boolean isRegistered(Identifier id) {
|
||||
SurviePoteTexture cached = CACHE.get(id);
|
||||
return cached != null
|
||||
&& Minecraft.getInstance().getTextureManager().getTexture(id) == cached;
|
||||
}
|
||||
|
||||
/** Largeur de la texture enregistrée (0 si inconnue). */
|
||||
public static int getWidth(Identifier id) {
|
||||
SurviePoteTexture cached = CACHE.get(id);
|
||||
return cached != null ? cached.getWidth() : 0;
|
||||
}
|
||||
|
||||
/** Hauteur de la texture enregistrée (0 si inconnue). */
|
||||
public static int getHeight(Identifier id) {
|
||||
SurviePoteTexture cached = CACHE.get(id);
|
||||
return cached != null ? cached.getHeight() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.surviepote.nosingleplayer;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import net.minecraft.client.renderer.texture.MipmapStrategy;
|
||||
import net.minecraft.client.renderer.texture.ReloadableTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureContents;
|
||||
import net.minecraft.client.resources.metadata.texture.TextureMetadataSection;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
|
||||
/**
|
||||
* Texture du mod (PNG lu dans le jar) gérée exactement comme une texture vanilla.
|
||||
*
|
||||
* <p>MC 26.3 n'expose plus les ressources des mods au ResourceManager : les packs ne
|
||||
* sont lus que depuis des dossiers, pas depuis les jars du classpath. Une texture
|
||||
* référencée par son Identifier est donc introuvable (placeholder magenta). On lit
|
||||
* donc le PNG directement dans le jar du mod et on l'enregistre via le mécanisme
|
||||
* officiel {@link ReloadableTexture} : c'est MC qui crée la texture GPU ET le
|
||||
* sampler, exactement comme pour ses propres textures. Aucun sampler custom, aucun
|
||||
* upload manuel => compatible avec tous les backends (OpenGL, Vulkan) et tous les
|
||||
* pilotes (Mesa, NVIDIA, AMD, Intel).
|
||||
*/
|
||||
public class SurviePoteTexture extends ReloadableTexture {
|
||||
|
||||
private final String resourcePath;
|
||||
private final boolean blur;
|
||||
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
public SurviePoteTexture(Identifier id, String resourcePath, boolean blur) {
|
||||
super(id);
|
||||
this.resourcePath = resourcePath;
|
||||
this.blur = blur;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextureContents loadContents(ResourceManager resourceManager) throws IOException {
|
||||
try (InputStream in = SurviePoteTexture.class.getResourceAsStream(this.resourcePath)) {
|
||||
if (in == null) {
|
||||
throw new IOException("Texture introuvable dans le jar : " + this.resourcePath);
|
||||
}
|
||||
NativeImage image = NativeImage.read(in);
|
||||
this.width = image.getWidth();
|
||||
this.height = image.getHeight();
|
||||
// blur=true => filtrage linéaire (image agrandie/réduite),
|
||||
// clamp=true => pas de répétition sur les bords.
|
||||
return new TextureContents(image,
|
||||
new TextureMetadataSection(this.blur, true, MipmapStrategy.AUTO, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class DiscordButton extends AbstractButton {
|
||||
super(x, y, width, height, Component.literal(label));
|
||||
this.action = action;
|
||||
// La texture doit être enregistrée avant le premier rendu.
|
||||
ModTextures.get(TEXTURE, TEXTURE_PATH, "SurviePote Discord", false);
|
||||
ModTextures.register(TEXTURE, TEXTURE_PATH, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -30,7 +30,7 @@ public class IconButton extends AbstractButton {
|
||||
this.texture = texture;
|
||||
this.action = action;
|
||||
// La texture doit être enregistrée avant le premier rendu.
|
||||
ModTextures.get(texture, resourcePath, message.getString(), false);
|
||||
ModTextures.register(texture, resourcePath, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,12 +11,13 @@ import net.minecraft.client.gui.components.LogoRenderer;
|
||||
import net.minecraft.client.gui.components.SplashRenderer;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
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.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(TitleScreen.class)
|
||||
public abstract class TitleScreenRenderMixin {
|
||||
@@ -25,7 +26,7 @@ public abstract class TitleScreenRenderMixin {
|
||||
private static final Identifier SURVIEPOTE_BACKGROUND = Identifier.fromNamespaceAndPath(
|
||||
"nosingleplayer", "textures/gui/background.png");
|
||||
|
||||
// Le PNG est lu directement dans le jar du mod (voir surviepote$loadBackground).
|
||||
// Le PNG est lu directement dans le jar du mod (voir ModTextures).
|
||||
@Unique
|
||||
private static final String SURVIEPOTE_BACKGROUND_RESOURCE =
|
||||
"/assets/nosingleplayer/textures/gui/background.png";
|
||||
@@ -39,22 +40,26 @@ public abstract class TitleScreenRenderMixin {
|
||||
@Unique
|
||||
private static final String DISABLE_CUSTOM_TITLE_PROPERTY = "surviepote.customTitle";
|
||||
|
||||
@Unique
|
||||
private static DynamicTexture surviepote$background;
|
||||
|
||||
@Unique
|
||||
private static int surviepote$backgroundWidth;
|
||||
|
||||
@Unique
|
||||
private static int surviepote$backgroundHeight;
|
||||
|
||||
// Le fond custom a déjà échoué : on ne réessaie plus (le menu reste noir sinon).
|
||||
@Unique
|
||||
private static boolean surviepote$backgroundFailed;
|
||||
|
||||
@Unique
|
||||
private static boolean surviepote$gpuLogged;
|
||||
|
||||
/**
|
||||
* Enregistre le fond custom AVANT le premier rendu du menu.
|
||||
*
|
||||
* <p>La texture GPU doit être créée hors de l'extraction du rendu : la créer
|
||||
* pendant l'enregistrement des commandes de la frame invalide le frame entier
|
||||
* sur les backends à command encoder (Vulkan) => menu titre entièrement noir
|
||||
* (le PC affected est sous Windows + NVIDIA, qui utilise le backend Vulkan).
|
||||
*/
|
||||
@Inject(method = "init", at = @At("HEAD"))
|
||||
private void surviepote$registerBackground(CallbackInfo ci) {
|
||||
try {
|
||||
ModTextures.register(SURVIEPOTE_BACKGROUND, SURVIEPOTE_BACKGROUND_RESOURCE, true);
|
||||
} catch (Throwable t) {
|
||||
NoSingleplayer.LOGGER.error("[SurviePote] Enregistrement du fond custom impossible.", t);
|
||||
}
|
||||
}
|
||||
|
||||
// Pas de panorama dynamique : fond statique custom à la place.
|
||||
// Note : extractPanorama est déclarée dans Screen (héritée), on redirige donc
|
||||
// l'APPEL dans TitleScreen plutôt que la méthode elle-même.
|
||||
@@ -68,11 +73,11 @@ public abstract class TitleScreenRenderMixin {
|
||||
// écran 100 % noir (sans boutons). D'où les garde-fous ci-dessous.
|
||||
surviepote$logGpuOnce();
|
||||
|
||||
// Menu vanilla si le fond custom a déjà échoué, ou si l'utilisateur a forcé
|
||||
// -Dsurviepote.customTitle=false (option de diagnostic).
|
||||
// Menu vanilla si l'utilisateur a forcé -Dsurviepote.customTitle=false,
|
||||
// ou si le fond custom n'est pas (ou plus) enregistré.
|
||||
final boolean customEnabled = !"false".equalsIgnoreCase(
|
||||
System.getProperty(DISABLE_CUSTOM_TITLE_PROPERTY, "true"));
|
||||
if (surviepote$backgroundFailed || !customEnabled) {
|
||||
if (!customEnabled || !ModTextures.isRegistered(SURVIEPOTE_BACKGROUND)) {
|
||||
surviepote$vanillaPanorama(self, extractor, partialTick);
|
||||
return;
|
||||
}
|
||||
@@ -80,41 +85,39 @@ public abstract class TitleScreenRenderMixin {
|
||||
try {
|
||||
int w = Minecraft.getInstance().getWindow().getGuiScaledWidth();
|
||||
int h = Minecraft.getInstance().getWindow().getGuiScaledHeight();
|
||||
|
||||
surviepote$loadBackground();
|
||||
if (surviepote$background == null) {
|
||||
int texW = ModTextures.getWidth(SURVIEPOTE_BACKGROUND);
|
||||
int texH = ModTextures.getHeight(SURVIEPOTE_BACKGROUND);
|
||||
if (texW <= 0 || texH <= 0 || w <= 0 || h <= 0) {
|
||||
surviepote$vanillaPanorama(self, extractor, partialTick);
|
||||
return;
|
||||
}
|
||||
|
||||
float screenAspect = (float) w / (float) h;
|
||||
float texAspect = (float) surviepote$backgroundWidth / (float) surviepote$backgroundHeight;
|
||||
float texAspect = (float) texW / (float) texH;
|
||||
|
||||
int u, v, srcW, srcH;
|
||||
if (screenAspect > texAspect) {
|
||||
// Écran plus large que l'image : on rogne en haut/bas.
|
||||
srcW = surviepote$backgroundWidth;
|
||||
srcH = Math.round(surviepote$backgroundWidth / screenAspect);
|
||||
srcW = texW;
|
||||
srcH = Math.round(texW / screenAspect);
|
||||
u = 0;
|
||||
v = (surviepote$backgroundHeight - srcH) / 2;
|
||||
v = (texH - srcH) / 2;
|
||||
} else {
|
||||
// Écran plus haut que l'image : on rogne à gauche/droite.
|
||||
srcH = surviepote$backgroundHeight;
|
||||
srcW = Math.round(surviepote$backgroundHeight * screenAspect);
|
||||
u = (surviepote$backgroundWidth - srcW) / 2;
|
||||
srcH = texH;
|
||||
srcW = Math.round(texH * screenAspect);
|
||||
u = (texW - srcW) / 2;
|
||||
v = 0;
|
||||
}
|
||||
|
||||
// Surcharge complète : (u, v) = origine dans la texture, (w, h) = taille à
|
||||
// l'écran, (srcW, srcH) = taille de la région source dans la texture.
|
||||
// L'overload court réutilise w/h comme région source (=> zoom sur un coin).
|
||||
extractor.blit(RenderPipelines.GUI_TEXTURED, SURVIEPOTE_BACKGROUND,
|
||||
0, 0, (float) u, (float) v,
|
||||
w, h, srcW, srcH,
|
||||
surviepote$backgroundWidth, surviepote$backgroundHeight, -1);
|
||||
texW, texH, -1);
|
||||
} catch (Throwable t) {
|
||||
// Repli : on bascule sur le panorama vanilla pour ne pas laisser un écran noir.
|
||||
surviepote$backgroundFailed = true;
|
||||
NoSingleplayer.LOGGER.error("[SurviePote] Échec du fond custom, repli sur le panorama vanilla.", t);
|
||||
surviepote$vanillaPanorama(self, extractor, partialTick);
|
||||
}
|
||||
@@ -149,29 +152,6 @@ public abstract class TitleScreenRenderMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MC 26.3 n'expose plus les ressources des mods au ResourceManager : les packs
|
||||
* ne sont lus que depuis des dossiers, pas depuis les jars du classpath. Une
|
||||
* texture référencée par son Identifier est donc introuvable (placeholder
|
||||
* magenta). Le PNG est donc lu dans le jar du mod (voir ModTextures).
|
||||
*/
|
||||
@Unique
|
||||
private static void surviepote$loadBackground() {
|
||||
DynamicTexture texture = ModTextures.get(SURVIEPOTE_BACKGROUND, SURVIEPOTE_BACKGROUND_RESOURCE,
|
||||
"SurviePote Background", true);
|
||||
if (texture == null) {
|
||||
surviepote$background = null;
|
||||
return;
|
||||
}
|
||||
if (surviepote$background != texture) {
|
||||
surviepote$background = texture;
|
||||
surviepote$backgroundWidth = texture.getPixels().getWidth();
|
||||
surviepote$backgroundHeight = texture.getPixels().getHeight();
|
||||
NoSingleplayer.LOGGER.info("[SurviePote] Fond chargé : {}x{}",
|
||||
surviepote$backgroundWidth, surviepote$backgroundHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Pas de texte de version ("26.3 mc") en bas à gauche.
|
||||
@Redirect(method = "extractRenderState",
|
||||
at = @At(value = "INVOKE",
|
||||
|
||||
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 98 B |
|
Before Width: | Height: | Size: 98 B |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 826 KiB |