Validate beta.144 water, unloaded boundaries and ocean seams
Build Sanctuary / build (push) Waiting to run
Build Sanctuary / build (push) Waiting to run
This commit is contained in:
@@ -20,8 +20,9 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
|||||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||||
à implémenter.
|
à implémenter.
|
||||||
|
|
||||||
La version **beta.144** prépare une option d’**eau naturelle pixélisée** :
|
La version **beta.144** ajoute une option d’**eau naturelle pixélisée** :
|
||||||
couleur du biome, trois profils selon le fond et visibilité adaptée en immersion.
|
couleur du biome, trois profils selon le fond, vert olive d’étang et visibilité
|
||||||
|
adaptée en immersion.
|
||||||
Les océans gardent leur couleur native et le profil clair.
|
Les océans gardent leur couleur native et le profil clair.
|
||||||
[Contrat et vérifications](docs/natural-water-beta144.md). Livraison en cours.
|
[Contrat et vérifications](docs/natural-water-beta144.md). Livraison en cours.
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ vert olive légèrement jaunâtre : le profil étang passe donc de 45 % à 85 %
|
|||||||
de mélange, tout en laissant les eaux claires et océaniques natives.
|
de mélange, tout en laissant les eaux claires et océaniques natives.
|
||||||
Les surfaces profondes sans fond dans la limite restent donc
|
Les surfaces profondes sans fond dans la limite restent donc
|
||||||
claires. Les profils se mélangent aux sommets sur un bloc, sans filtrer les
|
claires. Les profils se mélangent aux sommets sur un bloc, sans filtrer les
|
||||||
textures. L’eau sous un toit suit son fond, pas l’éclairage du bâtiment.
|
textures. Aux bords océaniques, les sommets partagés restent clairs et la
|
||||||
|
transition occupe le bloc voisin hors océan. L’eau sous un toit suit son fond,
|
||||||
|
pas l’éclairage du bâtiment.
|
||||||
|
|
||||||
La ressource client `assets/sanctuary/water_materials.json` accepte deux listes
|
La ressource client `assets/sanctuary/water_materials.json` accepte deux listes
|
||||||
d’identifiants de blocs, `light` et `pond`, remplaçables par un resource pack.
|
d’identifiants de blocs, `light` et `pond`, remplaçables par un resource pack.
|
||||||
@@ -44,7 +46,9 @@ et tri des transparences restent natifs. Les calculs de profils sont différés
|
|||||||
jusqu’à l’émission d’un sommet visible, puis mutualisés dans la région compilée.
|
jusqu’à l’émission d’un sommet visible, puis mutualisés dans la région compilée.
|
||||||
Un test de palette élimine les sections sans eau avant toute copie de biomes.
|
Un test de palette élimine les sections sans eau avant toute copie de biomes.
|
||||||
Les états de blocs viennent des copies natives ; les appartenances océaniques
|
Les états de blocs viennent des copies natives ; les appartenances océaniques
|
||||||
sont capturées avant l’envoi au worker. Aucun chargement de chunk n’est demandé.
|
sont capturées avant l’envoi au worker. La présence du terrain est vérifiée dans le cache client avec chargement
|
||||||
|
désactivé, sans utiliser `ClientLevel.hasChunk` (toujours vrai). Aucun
|
||||||
|
chargement de chunk n’est demandé par le classificateur.
|
||||||
|
|
||||||
Un changement de bloc invalide la zone dépendante, huit blocs au-dessus et
|
Un changement de bloc invalide la zone dépendante, huit blocs au-dessus et
|
||||||
un bloc horizontalement. Les reconstructions natives recopient leurs données ;
|
un bloc horizontalement. Les reconstructions natives recopient leurs données ;
|
||||||
@@ -68,30 +72,66 @@ sable et boue, support du sable, plantes aquatiques, toit, frontière de section
|
|||||||
et changement de biome océan. Une copie personnelle de ressource remplace
|
et changement de biome océan. Une copie personnelle de ressource remplace
|
||||||
réellement les familles de matériaux puis est désactivée.
|
réellement les familles de matériaux puis est désactivée.
|
||||||
|
|
||||||
Les suites graphiques OpenGL et Vulkan ont passé les contrôles de surface,
|
Les suites graphiques finales OpenGL et Vulkan sont réussies le 18 septembre
|
||||||
d’immersion, de fond modifié, d’océan, de ressources, de préférences et de
|
2026 (`WATER144_PASS` et `WATER144_EXTRAS_PASS`). Elles couvrent surface,
|
||||||
cascades pendant le développement. La dernière optimisation reconstruit
|
immersion, fond modifié, priorité océan, toit, plantes, cascades, lave inchangée,
|
||||||
seulement les sections contenant de l’eau lors du changement d’option.
|
blocs gorgés d’eau, préférences FR/EN et rechargement des ressources/chunks.
|
||||||
Sa série de validation finale reste à terminer avant publication.
|
Les sommets partagés aux limites océaniques et le repli clair lorsque le cache
|
||||||
|
client ne contient pas le terrain font l’objet d’assertions supplémentaires.
|
||||||
|
Aucun autre client de test concurrent n’a été détecté pendant ces séries.
|
||||||
|
Deux passages ciblés supplémentaires (`WATER144_TEXTURE_PASS`), Vulkan en
|
||||||
|
1 min 18 s et OpenGL en 1 min 4 s, chargent de vraies textures personnelles
|
||||||
|
32×32 à deux images animées pour l’eau calme et courante. Grille, animation
|
||||||
|
visible et retour aux textures natives sont vérifiés après désactivation.
|
||||||
|
Les fixtures, leurs alpha et les captures restent dans les preuves locales.
|
||||||
|
|
||||||
### Protocole de performance
|
### Protocole de performance
|
||||||
|
|
||||||
Apple M1, macOS, Java 25, 854×480, distance de rendu 6 chunks,
|
MacBookPro17,1, Apple M1, 8 Gio, macOS 26.5.1 (25F80), Java 25,
|
||||||
|
fenêtre logique 854×480, cible Retina **1708×960 pixels**, distance de rendu 6 chunks,
|
||||||
ombres 32 blocs / 16 pixels, PBR 50 %, autres effets activés.
|
ombres 32 blocs / 16 pixels, PBR 50 %, autres effets activés.
|
||||||
VSync désactivée, limite 260 (illimitée), limitation AFK désactivée.
|
VSync désactivée, limite 260 (illimitée), limitation AFK désactivée.
|
||||||
|
Le mod de test vérifie le limiteur effectif et journalise le mode de présentation.
|
||||||
|
Pendant les échantillons uniquement, il omet l’attente artificielle de
|
||||||
|
`FramerateLimiter.limitDisplayFPS`, afin de mesurer sans temporisation
|
||||||
|
applicative. Cette sonde reste absente du JAR de production. Les temps couvrent
|
||||||
|
la frame native, présentation comprise, et ne sont pas des temps GPU isolés.
|
||||||
Pour chaque scène : OFF/ON/ON/OFF/ON/OFF/OFF/ON, dix secondes par passage,
|
Pour chaque scène : OFF/ON/ON/OFF/ON/OFF/OFF/ON, dix secondes par passage,
|
||||||
avec stabilisation des sections puis quatre secondes de chauffe entre passages.
|
avec stabilisation des sections puis quatre secondes de chauffe entre passages.
|
||||||
Le déplacement avance de 120 blocs et laisse les sections se compiler
|
Le déplacement avance de 120 blocs et laisse les sections se compiler
|
||||||
naturellement, sans vidange artificielle des caches des autres effets.
|
naturellement, sans vidange artificielle des caches des autres effets.
|
||||||
|
La scène de déplacement prolonge le bassin hors de la vue initiale : son terrain
|
||||||
|
est préparé côté serveur de test, puis découvert normalement par le client.
|
||||||
|
Le vérificateur exige des compilations réelles dans chaque passage en mouvement.
|
||||||
Les moyennes des quatre médianes et P95 par état sont comparées avec les seuils
|
Les moyennes des quatre médianes et P95 par état sont comparées avec les seuils
|
||||||
+5 % et +2 ms. Les temps de compilation de sections sont consignés également.
|
+5 % et +2 ms. Les temps de compilation de sections sont consignés également.
|
||||||
|
|
||||||
Les essais courts initiaux ont montré une forte variabilité. Une limitation AFK
|
Les essais courts initiaux, les exécutions concurrentes et les vérifications
|
||||||
à 30 FPS puis des clients Minecraft concurrents dans le chantier PBR/SSR ont
|
incomplètes du limiteur restent conservés comme preuves de développement ;
|
||||||
été identifiés. Ces essais restent dans les preuves de développement ; ils
|
ils ne fondent pas ces résultats. Les séries finales ont été exécutées après
|
||||||
ne valident pas la livraison. La série finale attend la fin du chantier
|
la fin du chantier SSR. Les variations négatives sont du bruit de mesure,
|
||||||
concurrent, conformément à la demande du créateur. Les résultats ne sont pas
|
pas une promesse d’accélération. Vulkan reste proche de 120 images/s dans
|
||||||
présentés comme acquis, et aucun gain de performance universel n’est promis.
|
cette chaîne de présentation, malgré le mode `IMMEDIATE` et l’absence de
|
||||||
|
limiteur applicatif : les conclusions sont limitées à la machine et aux
|
||||||
|
conditions ci-dessus.
|
||||||
|
|
||||||
|
| Moteur / scène | Médiane OFF → ON (ms) | Variation | P95 OFF → ON (ms) | Δ P95 (ms) |
|
||||||
|
| --- | --- | ---: | --- | ---: |
|
||||||
|
| vulkan / mixed | 8.069 → 8.092 | +0.29 % | 9.091 → 9.101 | +0.010 |
|
||||||
|
| vulkan / large | 8.078 → 8.062 | -0.20 % | 9.126 → 9.114 | -0.012 |
|
||||||
|
| vulkan / moving | 8.123 → 8.103 | -0.24 % | 9.103 → 9.080 | -0.023 |
|
||||||
|
| opengl / mixed | 4.545 → 4.407 | -3.02 % | 7.080 → 6.733 | -0.347 |
|
||||||
|
| opengl / large | 4.879 → 5.110 | +4.73 % | 7.519 → 7.767 | +0.248 |
|
||||||
|
| opengl / moving | 6.817 → 6.338 | -7.02 % | 10.618 → 9.759 | -0.859 |
|
||||||
|
|
||||||
|
Les six comparaisons respectent les seuils : médiane ≤ +5 %, P95 ≤ +2 ms.
|
||||||
|
Le cas limitant est la grande surface OpenGL (+4,73 %, +0,248 ms).
|
||||||
|
Les compilations de sections sont mesurées sur le parcours en mouvement :
|
||||||
|
|
||||||
|
| Moteur | Sections OFF / ON | Médiane OFF → ON (ms) | P95 OFF → ON (ms) |
|
||||||
|
| --- | ---: | --- | --- |
|
||||||
|
| vulkan | 222 / 220 | 1.889 → 2.186 | 4.307 → 4.898 |
|
||||||
|
| opengl | 222 / 220 | 1.528 → 1.540 | 5.492 → 5.227 |
|
||||||
|
|
||||||
Pour reproduire, avec Java 25 et un environnement graphique disponible :
|
Pour reproduire, avec Java 25 et un environnement graphique disponible :
|
||||||
|
|
||||||
@@ -115,7 +155,7 @@ assembleTestPack -x :sanctuary:runGameTest` ; la suite complète non exclue
|
|||||||
n’est pas déclarée réussie. Les contrôles natifs de l’eau utilisent le serveur
|
n’est pas déclarée réussie. Les contrôles natifs de l’eau utilisent le serveur
|
||||||
intégré d’un monde jetable. Aucun essai Windows n’est revendiqué.
|
intégré d’un monde jetable. Aucun essai Windows n’est revendiqué.
|
||||||
|
|
||||||
L’assemblage avec l’exclusion ci-dessus a réussi en 3 min 24 s (126 tâches).
|
L’assemblage avec l’exclusion ci-dessus a réussi en 2 min 33 s (126 tâches).
|
||||||
`python3 scripts/pack.py check` réussit. Les cinq archives passent le contrôle
|
`python3 scripts/pack.py check` réussit. Les cinq archives passent le contrôle
|
||||||
ZIP ; les deux MRpack embarquent exactement le même JAR Sanctuary et déclarent
|
ZIP ; les deux MRpack embarquent exactement le même JAR Sanctuary et déclarent
|
||||||
Fabric API 0.160.5+26.3 avec l’empreinte du manifeste source. L’export MRpack est
|
Fabric API 0.160.5+26.3 avec l’empreinte du manifeste source. L’export MRpack est
|
||||||
@@ -131,8 +171,19 @@ Les preuves détaillées restent dans les dossiers ignorés :
|
|||||||
|
|
||||||
| Artefact | Octets | SHA-256 |
|
| Artefact | Octets | SHA-256 |
|
||||||
| --- | ---: | --- |
|
| --- | ---: | --- |
|
||||||
| `sanctuary-beta.144.jar` | 11343178 | `56d7a92cdb4dc5897f3fe66b805cf74bc225cc3c0fc558facdb821d0756f7e27` |
|
| `sanctuary-beta.144.jar` | 11343257 | `1ddbe941f3262da8a1497ea5df4a992e260b80ae95b341623dd68f8f9729ae88` |
|
||||||
| `Sanctuary-beta.144.mrpack` | 10483117 | `8118ad71989d7a1ba0ec2abe207599ca517cd508904003491fd35c8a378c5038` |
|
| `Sanctuary-beta.144.mrpack` | 10483196 | `d5ef5a781f5ee796117210d1f6afc96172461315faee34097323beff541ce6f0` |
|
||||||
| `Sanctuary-Test-beta.144.mrpack` | 10502040 | `46915113ce06336ffa92e30e2e01b4c9afbd9ba499fe2d27e1ee03a197149fbe` |
|
| `Sanctuary-Test-beta.144.mrpack` | 10502119 | `e7cc32ac19ae98ebbe7a9aac3dc08decdf09396c8175b597f87051399ff3812e` |
|
||||||
| `Sanctuary-Template-beta.144.zip` | 4354678 | `e510c0040dc9053492715ac91381b7dbc5a6f7c7defbf81e7502d8e18dbb0ee6` |
|
| `Sanctuary-Template-beta.144.zip` | 4354678 | `e510c0040dc9053492715ac91381b7dbc5a6f7c7defbf81e7502d8e18dbb0ee6` |
|
||||||
| `Sanctuary-Prism-auto-update.zip` | 92423 | `c0920823f40d709c14e074d8d949f2cc3681c24140bf86f21c3be345cfc3e1f6` |
|
| `Sanctuary-Prism-auto-update.zip` | 92423 | `c0920823f40d709c14e074d8d949f2cc3681c24140bf86f21c3be345cfc3e1f6` |
|
||||||
|
|
||||||
|
## Captures comparatives
|
||||||
|
|
||||||
|
Pièces jointes de la release, issues du client Vulkan de validation :
|
||||||
|
[OFF](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-off.png),
|
||||||
|
[ON](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-on.png),
|
||||||
|
[profil étang en proximité](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-pond.png).
|
||||||
|
Les captures sont exportées à 854×480 ; la cible de présentation mesurée est
|
||||||
|
1708×960. Le fichier public `Sanctuary-beta.144-SHA256SUMS.txt` inclut les cinq
|
||||||
|
livrables et ces trois captures. Les journaux et échantillons détaillés restent
|
||||||
|
locaux, dans `build/water144-evidence/` et les reçus `build/water144-*.json`.
|
||||||
|
|||||||
+65
-6
@@ -26,6 +26,7 @@ final class Water144ClientChecks {
|
|||||||
});
|
});
|
||||||
camera(c,server,72,125,16,70);c.waitTicks(50);settle(c);
|
camera(c,server,72,125,16,70);c.waitTicks(50);settle(c);
|
||||||
if(Boolean.getBoolean("sanctuary.test.water144ExtrasOnly")){
|
if(Boolean.getBoolean("sanctuary.test.water144ExtrasOnly")){
|
||||||
|
resourceOverride(c);
|
||||||
camera(c,server,88.5,96.2,8.5,70);look(c,server,0,0);c.waitTicks(20);capture(c,"pond-final-fog");
|
camera(c,server,88.5,96.2,8.5,70);look(c,server,0,0);c.waitTicks(20);capture(c,"pond-final-fog");
|
||||||
var green=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));
|
var green=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));
|
||||||
enable(c,false);var blue=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));capture(c,"native-final-fog");
|
enable(c,false);var blue=c.computeOnClient(m->new org.joml.Vector4f(m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.color));capture(c,"native-final-fog");
|
||||||
@@ -39,6 +40,11 @@ final class Water144ClientChecks {
|
|||||||
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Mud pond");
|
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Mud pond");
|
||||||
check(NaturalWater.sample(m.level,84.5,99.1,8.5).equals(NaturalWater.POND),"Seagrass is not a floor");
|
check(NaturalWater.sample(m.level,84.5,99.1,8.5).equals(NaturalWater.POND),"Seagrass is not a floor");
|
||||||
check(NaturalWater.sample(m.level,88.5,110.1,8.5).equals(NaturalWater.CLEAR),"No bed in bounded search uses clear");
|
check(NaturalWater.sample(m.level,88.5,110.1,8.5).equals(NaturalWater.CLEAR),"No bed in bounded search uses clear");
|
||||||
|
var unloaded=new BlockPos(1_000_016,96,1_000_016);
|
||||||
|
check(!NaturalWater.loaded(m.level,unloaded.getX()>>4,unloaded.getZ()>>4),"Fixture region is unloaded");
|
||||||
|
var missing=new NaturalWater.Region(m.level,62500,5,62500);
|
||||||
|
check(missing.corner(null,unloaded,0,0).equals(NaturalWater.CLEAR),"Unavailable biome snapshot forces clear without reading missing terrain");
|
||||||
|
check(!NaturalWater.loaded(m.level,unloaded.getX()>>4,unloaded.getZ()>>4),"Classification does not load chunks");
|
||||||
var boundary=NaturalWater.sample(m.level,80,99.1,8.5);check(boundary.tint()>.1&&boundary.tint()<.85,"Section boundary mixes profiles");
|
var boundary=NaturalWater.sample(m.level,80,99.1,8.5);check(boundary.tint()>.1&&boundary.tint()<.85,"Section boundary mixes profiles");
|
||||||
});
|
});
|
||||||
enable(c,false);var off=capture(c,"surface-off");enable(c,true);var on=capture(c,"surface-on");
|
enable(c,false);var off=capture(c,"surface-off");enable(c,true);var on=capture(c,"surface-on");
|
||||||
@@ -66,7 +72,20 @@ final class Water144ClientChecks {
|
|||||||
check(Math.abs(pondFog/nativeFog-.4)<.01,"Rendered underwater fog distance matches pond profile");capture(c,"immersed-native");enable(c,true);
|
check(Math.abs(pondFog/nativeFog-.4)<.01,"Rendered underwater fog distance matches pond profile");capture(c,"immersed-native");enable(c,true);
|
||||||
camera(c,server,72,125,16,70);
|
camera(c,server,72,125,16,70);
|
||||||
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:ocean"));c.waitTicks(35);settle(c);
|
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:ocean"));c.waitTicks(35);settle(c);
|
||||||
c.runOnClient(m->check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Ocean overrides muddy bed"));capture(c,"ocean-over-mud");
|
c.runOnClient(m->{
|
||||||
|
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Ocean overrides muddy bed");
|
||||||
|
boolean found=false;
|
||||||
|
for(int x=72;x<89;x++){
|
||||||
|
var p=new BlockPos(x,99,8);
|
||||||
|
if(!NaturalWater.ocean(m.level,p)&&NaturalWater.ocean(m.level,p.east())){
|
||||||
|
var left=new NaturalWater.Region(m.level,(x>>4)-1,5,-1);
|
||||||
|
var right=new NaturalWater.Region(m.level,((x+1)>>4)-1,5,-1);
|
||||||
|
check(left.corner(null,p,1,0).equals(right.corner(null,p.east(),0,0)),"Ocean edge emits identical shared vertices");
|
||||||
|
found=true;break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(found,"Native ocean boundary exercised");
|
||||||
|
});capture(c,"ocean-over-mud");
|
||||||
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:plains"));c.waitTicks(30);
|
server.runOnServer(s->command(s,"fillbiome 80 88 0 95 104 32 minecraft:plains"));c.waitTicks(30);
|
||||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);capture(c,"resources-reloaded");
|
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();settle(c);capture(c,"resources-reloaded");
|
||||||
resourceOverride(c);
|
resourceOverride(c);
|
||||||
@@ -113,36 +132,76 @@ final class Water144ClientChecks {
|
|||||||
camera(c,server,56.5,102,8.5,70);capture(c,"waterlogged-slab");
|
camera(c,server,56.5,102,8.5,70);capture(c,"waterlogged-slab");
|
||||||
System.out.println("WATER144_EXTRAS_PASS falling water, unchanged lava and waterlogged slab");
|
System.out.println("WATER144_EXTRAS_PASS falling water, unchanged lava and waterlogged slab");
|
||||||
}
|
}
|
||||||
|
private static net.minecraft.client.renderer.texture.SpriteContents waterSprite(net.minecraft.client.Minecraft m,String name){
|
||||||
|
return m.getAtlasManager().getAtlasOrThrow(net.minecraft.data.AtlasIds.BLOCKS)
|
||||||
|
.getSprite(net.minecraft.resources.Identifier.withDefaultNamespace("block/"+name)).contents();
|
||||||
|
}
|
||||||
private static void resourceOverride(ClientGameTestContext c){
|
private static void resourceOverride(ClientGameTestContext c){
|
||||||
|
int originalWidth=c.computeOnClient(m->waterSprite(m,"water_still").width());
|
||||||
|
int originalFrames=c.computeOnClient(m->waterSprite(m,"water_still").getUniqueFrames().size());
|
||||||
var directory=c.computeOnClient(m->m.getResourcePackDirectory().resolve("water144-fixture"));
|
var directory=c.computeOnClient(m->m.getResourcePackDirectory().resolve("water144-fixture"));
|
||||||
try{
|
try{
|
||||||
Files.createDirectories(directory.resolve("assets/sanctuary"));
|
Files.createDirectories(directory.resolve("assets/sanctuary"));
|
||||||
var mod=net.fabricmc.loader.api.FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow();
|
var mod=net.fabricmc.loader.api.FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow();
|
||||||
Files.copy(mod.findPath("resourcepacks/template/pack.mcmeta").orElseThrow(),directory.resolve("pack.mcmeta"),StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(mod.findPath("resourcepacks/template/pack.mcmeta").orElseThrow(),directory.resolve("pack.mcmeta"),StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
var textures=directory.resolve("assets/minecraft/textures/block");Files.createDirectories(textures);
|
||||||
|
try(var pixels=new com.mojang.blaze3d.platform.NativeImage(32,64,false)){
|
||||||
|
for(int y=0;y<64;y++)for(int x=0;x<32;x++)pixels.setPixel(x,y,((x/4+y/4+y/32)&1)==0?0xd0ffffff:0x80404040);
|
||||||
|
for(String name:new String[]{"water_still","water_flow"}){
|
||||||
|
pixels.writeToFile(textures.resolve(name+".png"));
|
||||||
|
Files.writeString(textures.resolve(name+".png.mcmeta"),"{\"animation\":{\"frametime\":4,\"frames\":[0,1]}}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Files.writeString(directory.resolve("assets/sanctuary/water_materials.json"),"{\"light\":[],\"pond\":[\"minecraft:quartz_block\"]}");
|
Files.writeString(directory.resolve("assets/sanctuary/water_materials.json"),"{\"light\":[],\"pond\":[\"minecraft:quartz_block\"]}");
|
||||||
}catch(Exception e){throw new AssertionError(e);}
|
}catch(Exception e){throw new AssertionError(e);}
|
||||||
c.runOnClient(m->{var repo=m.getResourcePackRepository();repo.reload();check(repo.addPack("file/water144-fixture"),"Custom materials pack discovered");});
|
c.runOnClient(m->{var repo=m.getResourcePackRepository();repo.reload();check(repo.addPack("file/water144-fixture"),"Custom materials pack discovered");});
|
||||||
var future=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->future.isDone()&&m.gui.overlay()==null,1200);future.join();settle(c);
|
var future=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->future.isDone()&&m.gui.overlay()==null,1200);future.join();settle(c);
|
||||||
c.runOnClient(m->{check(NaturalWater.sample(m.level,56.5,99.1,8.5).equals(NaturalWater.POND),"Resource pack replaces material family");
|
c.runOnClient(m->{check(NaturalWater.sample(m.level,56.5,99.1,8.5).equals(NaturalWater.POND),"Resource pack replaces material family");
|
||||||
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Replacement removes bundled assignment");});
|
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.CLEAR),"Replacement removes bundled assignment");});
|
||||||
capture(c,"custom-materials");
|
c.runOnClient(m->{for(String name:new String[]{"water_still","water_flow"}){
|
||||||
|
var sprite=waterSprite(m,name);check(sprite.width()==32&&sprite.height()==32&&sprite.getUniqueFrames().size()==2,"Custom pixel grid and animation loaded: "+name);
|
||||||
|
}});
|
||||||
|
var first=capture(c,"custom-materials");long animated=0;
|
||||||
|
for(int i=0;i<8;i++){c.waitTicks(1);animated=Math.max(animated,difference(first,capture(c,"custom-animation-"+i)));}
|
||||||
|
check(animated>100000,"Custom water animation remains visible");
|
||||||
c.runOnClient(m->m.getResourcePackRepository().removePack("file/water144-fixture"));
|
c.runOnClient(m->m.getResourcePackRepository().removePack("file/water144-fixture"));
|
||||||
var restore=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->restore.isDone()&&m.gui.overlay()==null,1200);restore.join();settle(c);
|
var restore=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->restore.isDone()&&m.gui.overlay()==null,1200);restore.join();settle(c);
|
||||||
c.runOnClient(m->check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Disabling custom pack restores defaults"));
|
c.runOnClient(m->{
|
||||||
|
check(NaturalWater.sample(m.level,88.5,99.1,8.5).equals(NaturalWater.POND),"Disabling custom pack restores defaults");
|
||||||
|
check(waterSprite(m,"water_still").width()==originalWidth&&waterSprite(m,"water_still").getUniqueFrames().size()==originalFrames,"Native water texture restored");
|
||||||
|
});
|
||||||
|
System.out.println("WATER144_TEXTURE_PASS custom 32px still/flow textures, alpha, two animation frames and restoration");
|
||||||
}
|
}
|
||||||
private static void benchmark(ClientGameTestContext c,TestServerContext s){
|
private static void benchmark(ClientGameTestContext c,TestServerContext s){
|
||||||
int oldLimit=c.computeOnClient(m->m.options.framerateLimit().get());
|
int oldLimit=c.computeOnClient(m->m.options.framerateLimit().get());
|
||||||
var oldInactivity=c.computeOnClient(m->m.options.inactivityFpsLimit().get());
|
var oldInactivity=c.computeOnClient(m->m.options.inactivityFpsLimit().get());
|
||||||
c.runOnClient(m->{m.options.framerateLimit().set(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);m.options.inactivityFpsLimit().set(net.minecraft.client.InactivityFpsLimit.MINIMIZED);});
|
c.runOnClient(m->{m.options.framerateLimit().set(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);m.options.inactivityFpsLimit().set(net.minecraft.client.InactivityFpsLimit.MINIMIZED);m.getFramerateLimitTracker().setFramerateLimit(net.minecraft.client.Options.UNLIMITED_FRAMERATE_CUTOFF);});
|
||||||
var results=new LinkedHashMap<String,Object>();
|
var results=new LinkedHashMap<String,Object>();
|
||||||
try {
|
try {
|
||||||
for(String scene:new String[]{"mixed","large","moving"}){
|
for(String scene:new String[]{"mixed","large","moving"}){
|
||||||
if(scene.equals("large")){s.runOnServer(server->{command(server,"fill 31 94 -17 112 100 -17 glass");command(server,"fill 31 94 48 112 100 48 glass");
|
if(scene.equals("large")){s.runOnServer(server->{command(server,"fill 31 94 -17 112 100 -17 glass");command(server,"fill 31 94 48 112 100 48 glass");
|
||||||
command(server,"fill 31 94 -17 31 100 48 glass");command(server,"fill 112 94 -17 112 100 48 glass");
|
command(server,"fill 31 94 -17 31 100 48 glass");command(server,"fill 112 94 -17 112 100 48 glass");
|
||||||
command(server,"fill 32 93 -16 111 93 47 quartz_block");command(server,"fill 32 94 -16 111 94 47 sand");command(server,"fill 32 95 -16 111 99 47 water");});c.waitTicks(50);}
|
command(server,"fill 32 93 -16 111 93 47 quartz_block");command(server,"fill 32 94 -16 111 94 47 sand");command(server,"fill 32 95 -16 111 99 47 water");});c.waitTicks(50);}
|
||||||
|
if(scene.equals("moving")){
|
||||||
|
// Test fixture only: prepare water beyond the initial client view so movement really compiles terrain.
|
||||||
|
s.runOnServer(server->{
|
||||||
|
command(server,"forceload add 112 -17 288 48");
|
||||||
|
for(int x=7;x<=18;x++)for(int z=-2;z<=3;z++)server.overworld().getChunk(x,z);
|
||||||
|
command(server,"fill 112 94 -17 288 100 -17 glass");command(server,"fill 112 94 48 288 100 48 glass");
|
||||||
|
command(server,"fill 288 94 -17 288 100 48 glass");
|
||||||
|
for(int x=112;x<288;x+=16){
|
||||||
|
command(server,"fill "+x+" 93 -16 "+(x+15)+" 93 47 quartz_block");
|
||||||
|
command(server,"fill "+x+" 94 -16 "+(x+15)+" 94 47 sand");
|
||||||
|
command(server,"fill "+x+" 95 -16 "+(x+15)+" 99 47 water");
|
||||||
|
}
|
||||||
|
});c.waitTicks(80);
|
||||||
|
}
|
||||||
camera(c,s,72,125,16,70);settle(c);
|
camera(c,s,72,125,16,70);settle(c);
|
||||||
for(boolean enabled:new boolean[]{false,true,true,false,true,false,false,true}){
|
for(boolean enabled:new boolean[]{false,true,true,false,true,false,false,true}){
|
||||||
camera(c,s,72,125,16,70);enable(c,enabled);c.waitTicks(80);c.runOnClient(m->Water144Metrics.start());
|
camera(c,s,72,125,16,70);enable(c,enabled);c.waitTicks(80);c.runOnClient(m->{
|
||||||
|
check(m.getFramerateLimitTracker().getFramerateLimit()==260&&!m.options.enableVsync().get(),"Uncapped native limiter and VSync disabled");
|
||||||
|
System.out.println("WATER144_CONDITIONS limit="+m.getFramerateLimitTracker().getFramerateLimit()+" throttle="+m.getFramerateLimitTracker().getThrottleReason()+" renderLimit="+m.gameRenderer.gameRenderState().framerateLimit+" surface="+m.windowSurface().currentConfiguration()+" limiterBypass=true");
|
||||||
|
Water144Metrics.start();});
|
||||||
if(scene.equals("moving")){
|
if(scene.equals("moving")){
|
||||||
for(int i=0;i<200;i++){final int step=i;c.runOnClient(m->{m.player.setPos(48+step*.6,125,16);m.player.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);});c.waitTicks(1);}
|
for(int i=0;i<200;i++){final int step=i;c.runOnClient(m->{m.player.setPos(48+step*.6,125,16);m.player.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);});c.waitTicks(1);}
|
||||||
}else c.waitTicks(200);
|
}else c.waitTicks(200);
|
||||||
@@ -151,6 +210,6 @@ final class Water144ClientChecks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try{Files.writeString(Path.of("water144-performance.json"),new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(results));}catch(Exception e){throw new AssertionError(e);}
|
try{Files.writeString(Path.of("water144-performance.json"),new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(results));}catch(Exception e){throw new AssertionError(e);}
|
||||||
}finally{c.runOnClient(m->{m.options.framerateLimit().set(oldLimit);m.options.inactivityFpsLimit().set(oldInactivity);});}
|
}finally{c.runOnClient(m->{m.options.framerateLimit().set(oldLimit);m.getFramerateLimitTracker().setFramerateLimit(oldLimit);m.options.inactivityFpsLimit().set(oldInactivity);});}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -1,5 +1,6 @@
|
|||||||
package fr.koka.sanctuary.gametest.mixin;
|
package fr.koka.sanctuary.gametest.mixin;
|
||||||
import fr.koka.sanctuary.gametest.Water144Metrics;
|
import fr.koka.sanctuary.gametest.Water144Metrics;
|
||||||
|
import com.llamalad7.mixinextras.injector.wrapoperation.*;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import org.spongepowered.asm.mixin.*;
|
import org.spongepowered.asm.mixin.*;
|
||||||
import org.spongepowered.asm.mixin.injection.*;
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
@@ -7,6 +8,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||||||
@Mixin(Minecraft.class)
|
@Mixin(Minecraft.class)
|
||||||
abstract class Water144FrameProbeMixin {
|
abstract class Water144FrameProbeMixin {
|
||||||
@Unique private long sanctuary$start;
|
@Unique private long sanctuary$start;
|
||||||
|
// Benchmark only: the extracted frame state can retain a 120 FPS limit despite the option/tracker.
|
||||||
|
@WrapOperation(method="renderFrame",at=@At(value="INVOKE",target="Lnet/minecraft/client/FramerateLimiter;limitDisplayFPS(I)V"))
|
||||||
|
private void sanctuary$uncapped(int fps,Operation<Void> original){if(!Water144Metrics.recording)original.call(fps);}
|
||||||
|
|
||||||
@Inject(method="renderFrame",at=@At("HEAD")) private void start(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)sanctuary$start=System.nanoTime();}
|
@Inject(method="renderFrame",at=@At("HEAD")) private void start(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)sanctuary$start=System.nanoTime();}
|
||||||
@Inject(method="renderFrame",at=@At("RETURN")) private void end(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)Water144Metrics.frame(System.nanoTime()-sanctuary$start);}
|
@Inject(method="renderFrame",at=@At("RETURN")) private void end(boolean tick,CallbackInfo ci){if(Water144Metrics.recording)Water144Metrics.frame(System.nanoTime()-sanctuary$start);}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,11 @@ public final class NaturalWater {
|
|||||||
}
|
}
|
||||||
public static float strength(){return strength;}
|
public static float strength(){return strength;}
|
||||||
public static Profile fogProfile(){return fogFrom.mix(fogTarget,Math.clamp((System.nanoTime()-fogChanged)/500_000_000f,0,1));}
|
public static Profile fogProfile(){return fogFrom.mix(fogTarget,Math.clamp((System.nanoTime()-fogChanged)/500_000_000f,0,1));}
|
||||||
public static boolean ocean(ClientLevel level,BlockPos pos){return level.hasChunk(pos.getX()>>4,pos.getZ()>>4)&&level.getBiome(pos).is(BiomeTags.IS_OCEAN);}
|
// ClientLevel.hasChunk always returns true; inspect the cache without requesting a chunk.
|
||||||
|
public static boolean loaded(ClientLevel level,int x,int z){
|
||||||
|
return level.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.status.ChunkStatus.FULL,false)!=null;
|
||||||
|
}
|
||||||
|
public static boolean ocean(ClientLevel level,BlockPos pos){return loaded(level,pos.getX()>>4,pos.getZ()>>4)&&level.getBiome(pos).is(BiomeTags.IS_OCEAN);}
|
||||||
private static int classify(java.util.function.Function<BlockPos,BlockState> read,BlockPos pos,Map<Block,Integer> table){
|
private static int classify(java.util.function.Function<BlockPos,BlockState> read,BlockPos pos,Map<Block,Integer> table){
|
||||||
var cursor=pos.mutable();
|
var cursor=pos.mutable();
|
||||||
for(int i=1;i<=DEPTH;i++){
|
for(int i=1;i<=DEPTH;i++){
|
||||||
@@ -102,7 +106,7 @@ public final class NaturalWater {
|
|||||||
return live(level,bx,by,bz).mix(live(level,bx+1,by,bz),dx).mix(live(level,bx,by,bz+1).mix(live(level,bx+1,by,bz+1),dx),dz);
|
return live(level,bx,by,bz).mix(live(level,bx+1,by,bz),dx).mix(live(level,bx,by,bz+1).mix(live(level,bx+1,by,bz+1),dx),dz);
|
||||||
}
|
}
|
||||||
private static Profile live(ClientLevel l,int x,int y,int z){
|
private static Profile live(ClientLevel l,int x,int y,int z){
|
||||||
var p=new BlockPos(x,y,z);if(!l.hasChunk(x>>4,z>>4)||ocean(l,p))return CLEAR;
|
var p=new BlockPos(x,y,z);if(!loaded(l,x>>4,z>>4)||ocean(l,p))return CLEAR;
|
||||||
return PROFILES[classify(l::getBlockState,p,materials)];
|
return PROFILES[classify(l::getBlockState,p,materials)];
|
||||||
}
|
}
|
||||||
/** Owned by a single native section compilation, with ocean data captured before worker submission. */
|
/** Owned by a single native section compilation, with ocean data captured before worker submission. */
|
||||||
@@ -120,13 +124,14 @@ public final class NaturalWater {
|
|||||||
var copied=new java.util.ArrayList<net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>>(216);
|
var copied=new java.util.ArrayList<net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>>(216);
|
||||||
int qx=(ox>>2)-1,qy=(oy>>2)-1,qz=(oz>>2)-1;
|
int qx=(ox>>2)-1,qy=(oy>>2)-1,qz=(oz>>2)-1;
|
||||||
for(int y=0;y<6;y++)for(int z=0;z<6;z++)for(int x=0;x<6;x++)
|
for(int y=0;y<6;y++)for(int z=0;z<6;z++)for(int x=0;x<6;x++)
|
||||||
copied.add(level.hasChunk((qx+x)>>2,(qz+z)>>2)?level.getBiomeManager().getNoiseBiomeAtQuart(qx+x,qy+y,qz+z):null);
|
copied.add(loaded(level,(qx+x)>>2,(qz+z)>>2)?level.getBiomeManager().getNoiseBiomeAtQuart(qx+x,qy+y,qz+z):null);
|
||||||
biomes=level.getBiomeManager().withDifferentSource((x,y,z)->copied.get(((y-qy)*6+z-qz)*6+x-qx));
|
biomes=level.getBiomeManager().withDifferentSource((x,y,z)->copied.get(((y-qy)*6+z-qz)*6+x-qx));
|
||||||
}
|
}
|
||||||
|
// A missing biome snapshot also forces clear water at unloaded region edges.
|
||||||
public boolean ocean(int x,int y,int z){
|
public boolean ocean(int x,int y,int z){
|
||||||
if(oceanKinds==null)oceanKinds=new byte[18*16*18];
|
if(oceanKinds==null)oceanKinds=new byte[18*16*18];
|
||||||
int key=((y-oy)*18+z-oz+1)*18+x-ox+1;
|
int key=((y-oy)*18+z-oz+1)*18+x-ox+1;
|
||||||
int code=oceanKinds[key];if(code==0){var biome=biomes.getBiome(x,y,z);code=biome!=null&&biome.is(BiomeTags.IS_OCEAN)?2:1;oceanKinds[key]=(byte)code;}
|
int code=oceanKinds[key];if(code==0){var biome=biomes.getBiome(x,y,z);code=biome==null||biome.is(BiomeTags.IS_OCEAN)?2:1;oceanKinds[key]=(byte)code;}
|
||||||
return code==2;
|
return code==2;
|
||||||
}
|
}
|
||||||
private Profile profile(BlockAndTintGetter region,int x,int y,int z){
|
private Profile profile(BlockAndTintGetter region,int x,int y,int z){
|
||||||
@@ -140,6 +145,9 @@ public final class NaturalWater {
|
|||||||
public Profile corner(BlockAndTintGetter region,BlockPos p,int dx,int dz){
|
public Profile corner(BlockAndTintGetter region,BlockPos p,int dx,int dz){
|
||||||
if(ocean(p.getX(),p.getY(),p.getZ()))return CLEAR;
|
if(ocean(p.getX(),p.getY(),p.getZ()))return CLEAR;
|
||||||
int x=p.getX()+dx-1,y=p.getY(),z=p.getZ()+dz-1;
|
int x=p.getX()+dx-1,y=p.getY(),z=p.getZ()+dz-1;
|
||||||
|
// Both cells sharing an ocean edge must emit the same clear vertex.
|
||||||
|
// The one-block transition stays on the non-ocean side, preserving ocean priority.
|
||||||
|
if(ocean(x,y,z)||ocean(x+1,y,z)||ocean(x,y,z+1)||ocean(x+1,y,z+1))return CLEAR;
|
||||||
return profile(region,x,y,z).mix(profile(region,x+1,y,z),.5f).mix(profile(region,x,y,z+1).mix(profile(region,x+1,y,z+1),.5f),.5f);
|
return profile(region,x,y,z).mix(profile(region,x+1,y,z),.5f).mix(profile(region,x,y,z+1).mix(profile(region,x+1,y,z+1),.5f),.5f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ backend=log_path.stem
|
|||||||
log=log_path.read_text()
|
log=log_path.read_text()
|
||||||
rows={key:json.loads(data) for key,data in re.findall(r"WATER144_PERF (\S+) (\[[^\n]+\])",log)}
|
rows={key:json.loads(data) for key,data in re.findall(r"WATER144_PERF (\S+) (\[[^\n]+\])",log)}
|
||||||
assert len(rows)==24, ("Incomplete measurements",len(rows))
|
assert len(rows)==24, ("Incomplete measurements",len(rows))
|
||||||
|
assert log.count("WATER144_CONDITIONS limit=260 throttle=NONE")==24, "Measurements must be uncapped"
|
||||||
result={"backend":backend,"samples":rows,"scenes":{}}
|
result={"backend":backend,"samples":rows,"scenes":{}}
|
||||||
for scene in ("mixed","large","moving"):
|
for scene in ("mixed","large","moving"):
|
||||||
off=[v for k,v in rows.items() if k.startswith(scene+"-false-")]
|
off=[v for k,v in rows.items() if k.startswith(scene+"-false-")]
|
||||||
on=[v for k,v in rows.items() if k.startswith(scene+"-true-")]
|
on=[v for k,v in rows.items() if k.startswith(scene+"-true-")]
|
||||||
assert len(off)==len(on)==4 and all(v[0]>50 for v in off+on)
|
assert len(off)==len(on)==4 and all(v[0]>50 for v in off+on)
|
||||||
|
if scene=="moving":
|
||||||
|
assert all(v[3]>0 for v in off+on), "Moving scene must compile terrain sections"
|
||||||
median_off=sum(v[1] for v in off)/len(off);median_on=sum(v[1] for v in on)/len(on)
|
median_off=sum(v[1] for v in off)/len(off);median_on=sum(v[1] for v in on)/len(on)
|
||||||
p95_off=sum(v[2] for v in off)/len(off);p95_on=sum(v[2] for v in on)/len(on)
|
p95_off=sum(v[2] for v in off)/len(off);p95_on=sum(v[2] for v in on)/len(on)
|
||||||
value={"offMedianMs":median_off,"onMedianMs":median_on,"medianIncreasePercent":100*(median_on/median_off-1),"offP95Ms":p95_off,"onP95Ms":p95_on,"p95IncreaseMs":p95_on-p95_off}
|
value={"offMedianMs":median_off,"onMedianMs":median_on,"medianIncreasePercent":100*(median_on/median_off-1),"offP95Ms":p95_off,"onP95Ms":p95_on,"p95IncreaseMs":p95_on-p95_off}
|
||||||
|
|||||||
Reference in New Issue
Block a user