Saturday, July 22, 2023

DS Ram Leakage in TestNScripts

I had thought that fixing 'slope unit-tests' in addition to 'wall unit tests' would mean my fixes to get the scorpeye behaving as intended. That would mean the 'summer code review' would be over and the 'summer game dev' could start. Well, that was hoping too much, because there's actually one more test that, for some reason, doesn't show up in ./testme --list but still exists and is important: TestNScripts. This one loads the real .cmd files of SchoolRush and checks parsing went fine. And it does not only check one of them: it will reload many scripts in the same "engine", the way the game does.


while (ntests < n) {
   TestBench tc;
   for (uint i=0; i < REDO; i++) {
       printf("== R%i/%i redo%i ", ntests, n, i);
       tc.ResetEngineResources();
       ParseScript(tc, scripts[rand()%nscripts], __FUNCTION__);
       tc.CheckEngine();
   }
   ntests++;
   tc.Over();
}

No assertion failure here, instead one of the trickiest things to track: memory exhaustion. So it's time for me to learn how to use my tracking tools again. I even left documentation for my future self back in the past (thanks, past self ^^). Once again, the problem arise also in 'default'... looks like I had been over-optimistic in September 2021 when I merged sprites overlay into default.

Well, at least if gives me hope this time: on the 'newmap branch', the forgotten test terminates with 'Out of DS memory'. I managed to get a log of what is allocated at that time, but it would require filtering what has leaked from last ParseScript, and what belongs to the current, interrupted because there's no more memory, running ParseScript. Hopefully, when the bug occurs on default, it gives me a nice 'halt because of leakage' and not a 'out of DS memory' condition. 

  • [done] understand why the new branch gets so high on memory consumption
  • [todo] make sure .spr and .cmd files in fakeroot/ are working fine (despite they now need bilou.spr and school.spr)
  • [todo] that should be the job of UnitTest.mk

edit: dummy! dummy! dummy! ... the 'DS Ram Usage' statistics in those tests is a lie! when it says 95% used, it doesn't mean you'd use 95% of the NDS 4MiB of RAM to play the script. It means that the dummy-allocator-that-never-recycle-memory used 95% of its storage to process the script. Should parsing become more complex (e.g. because we're loading one more file, or creating more intermediate structures), the memory used by the script will grow while the true RAM usage on the NDS would stay roughly the same.

Eeet ouaip. Je me suis donc morfondu un bon morceau de l'été sur le fait que mon moteur de jeu faisait péter sa consommation de mémoire. En tout cas, ça a sérieusement refroidi mon appétit pour le dévelopement de jeu après les heures de boulot. Trouver le problème, ça demandait de se plonger dans une montagne de données et les outils d'il-y-a-longtemps pour comprendre où était la fuite... Sauf qu'en vrai il n'y avait pas de fuite. Tout ça, c'était juste un ou deux messages d'erreur mal nommés qui me faisaient croire que la lecture des scripts prenait plus de 4MB de mémoire (tout ce que la DS a, en somme), sauf que l'environnement de test pour faire tourner les niveaux de mes jeux DS sur PC n'est pas prévu pour qu'on joue, mais pour qu'on trouve les erreurs. 

Donc contrairement à un système de gestion de mémoire ordinaire, quand on lui "rend" un bloc de mémoire il ne le réutilise jamais. La mémoire qu'il fournit à la demande est toujours de la mémoire qui n'a jamais servi pour une autre tâche dans le niveau, et c'est elle qui finissait par faire défaut. Vous me permettrez de croire que si vous avez envie d'en savoir plus, vous êtes aussi en mesure de lire l'Anglais?

If I want to have an estimate of the memory it would take on the NDS, I'd have to keep track of individual allocs and frees and see whether they reach some maximum at a new alloc. (2023-10-23)

What is that one-time allocator ? well, maybe you've heard of bottom-up allocator, where you just keep track of one position in memory: the top-of-used-area, and everything above the top is unused memory. Of course, as soon as you free things out-of-order compared to how you allocated them, that allocator is no longer enough. You would like at least a list of freed-blocks-that-could-further-reduce-top in case the block at the top is eventually freed. Well, the one-time allocator does not even do that. Any byte of one-time memory can be allocated exactly only once. If freed, it is "painted" as free and will remain like that until the ongoing test terminates. 

That is silly as far as memory management is concerned, but it means if some block has not benn freed when the test terminates, you know what role this memory had when it failed. There can't be any tricky-as-hell case where "yeah, that memory had been used for A and then freed, but then it has been re-allocated for B. So maybe B is wrong or maybe something still had a reference of when it was used for A". Sometimes it truly helps. When I'm not misguided by my past self with lying error messages.

Thursday, July 20, 2023

class AppleWalls : public WallTest, UsingApple

So I had managed to get shell/walls interaction mostly fixed. I thought it would be wise to get it run 'unit tests' to ensure nothing got broken by the fix, but it would do more than a few seconds of tests before failing. In Walls/Walk test , a fairly recent addition to MapTests.cpp, for which I do not seem to have any commit where that test is a success.

The test failure will produce patterns such as

--(120,0)+<-508,0>:[-224,0]     's/7f.f, S/fe08.0, '
--(118,0)+<-512,0>     's/7f.f, S/fe08.0, '
--(116,0)+<-516,0>     's/7f.f, S/fe08.0, '

--(114,0)+<-520,0>:[-232,0]     's/77.f, S/fdf8.0, '
--(112,0)     's/77.f, S/fdf8.0, '
--(110,0):[-240,0]     's/77.f, S/fdf8.0, '
--(108,0)     's/77.f, S/fdf8.0, '
--(106,0):[-248,0]     's/6f.f, S/fdf8.0, '
--(104,0)     's/6f.f, S/fdf8.0, '
--(103,0)+<248,0>:[0,0]     's/6f.f, S/fdf8.0, '

which I, too, find cryptic. I should at least inform future self that they are from saved "last state" array, dumped on exception like assert failure during the test.
  • (%{x}, %{y}) is used to report new coordinates
  • <%{vx}, %{vy}> is used to report a new speed
  • [%{dx}, %{dy}] is used to report new delayed step

Together with the arena layout defined by walls1, it can start making sense. For instance, x=104 is the lowest valid position before entering the two-# "wall" of the upper platform, where the Gob-under-test is walking. Granted, this is arcane and confusing, and it would deserve a review fix even if it is only for me. Possibly even more confusing are the `s/%x.%x` codes reported between quotes afterwards. These are generated from ChiefInspector::report() call from the WalkerController.

  • lowercase s indicates that there has been a change of tile considered in doslopes(). hex values are the coordinates of the hotspot used to decide that.
  • uppercase S indicates that doslopes() reported we're on sloped ground. hex values are the speed defined during doslopes().

Even with that wrong report of sloped ground fixed, I still have my 'follower' capable of entering walls. It happens when we stop next to the wall, but do not cancel the 'step value still needed'. More investigation needed.

With some break getspeed and cond BREAKNO (x >> 8) < 105 && cdata[GOB_XSPEED] == -520 in an epic gdb session, I could trace precisely what happens, one GobController call after another, and how the speed, delayed steps and the like where further processed.

The issue turned to be linked to the 'maxmove computation', an engine feature added in 2020 that scans the animation commands ahead of think() calls, so that we don't ask doslopes() to check for something different than what we'll actually do, else the vertical move and the horizontal move won't match and we'll end up out of the slope. The code that decides what to do when we detect a move must be cancelled assumes that the motion debt stored in the 'delayed step' variable has already been validated by controllers. In case of failure, we're safe to move *at least there*.

That was true with the "school rush" engine, where it was helpful to ensure we actually get close enough to the walls for the testpoints to work as intended. It is no longer true if maxmove shortened some of it. How precisely this can be fixed still has needed a bit more time to be figured out.

PS: while looking for the 'inspector codes' in my 2020-notepad, I got my eyes caught by a character stating "this is contemporary to maxmove introduction"... The line below reads "hopefully, no need to tweak things (how many loops are allowed while processing animation instructions, btw) too much. All it takes is building an animation list with enough 'I_MOVETO(+1, *)' commands before it loops."

PPS: of course, fixing things here broke things there, but mostly because code was wrongly assuming that some things (like vertical position or FAIL counter)

Saturday, June 24, 2023

area %d = anim %x

Bon, franchement, je me demande depuis quand ce problème attend que je m'y attelle... que ce soit mon blog, twitter ou mon calepin, impossible de trouver une date correspondant à "oui, la plate-forme se reclappe, mais quand je me mets plus sur sa gauche, je passe à travers.

J'imagine que ça fait partie des choses qui m'ont motivé, fin du mois dernier, à faire en sorte qu'Inspector Widget marche à nouveau dans la démo "3 rooms". Et la visualisation des zones de collision m'avait porté à croire que j'avais mal défini les zones de collision dans l'éditeur, sur NDS.

At some point, I noticed that InspectorWidget wasn't working anymore in Dreams.nds ... not quit ideal when you're trying to add bits of gameplay to your years-long-under-development demo. Especially not when you barely manage to get more than 1 hour of gamedev at once. And after I managed to get it back, I suddenly remembered that seeing Bilou falling through the flipping platform was nothing new and that it was linked to the platform having bad hitbox.

Parce que oui, ayant la flemme de définir les zones à la main, cette 'crocforme' est (avec la branche-qui-rebondit), un des premiers objets du jeu à importer une zone de collision passive directement avec area 0 = anim ${FLAGS}, plutôt que d'en fixer explicitement les coordonnées comme j'ai fait avec Apple Assault et School Rush.

Sauf que sur DS, les hitbox sont parfaites. Rien à redire. Je re-transfère le fichier (le lendemain), des fois que j'aurais oublié que j'avais déjà corrigé le soucis. Je recompile (le surlendemain), je teste. Pas d'amélioration. On est samedi, je sors l'artillerie lourde: une nouvelle instruction `break` pour le langage de script histoire de pouvoir mettre un breakpoint dans ddd et commencer à inspecter le comportement du parseur de script pile là où ça coince: dans la boucle qui lit les commandes 'Define' de l'animation où on fixe les coordonnées de la zone solide et des hitboxes pour l'animation.

At first, I thought I had just messed the hitbox within AnimEditor for DS. But a few checks and WiFi transfers later, it turned out that no: the data (as far as the editor was concerned) was ok, but the game still would use the bad hitbox. Checking that on the parser would be darker than night -- or so I thought. This is how the parser now supports an additional "break" instruction that is a noop where you can set a gdb breakpoint while parsing things. These are not meant to be committed, of course, but it definitely helped.

A few 'next instruction' later, it turns out that my engine is using tool-absolute coordinates for the hitbox instead of object-relative coordinates. That is, when you edit an animation in AnimEDS, you don't only store how to use it in-game, but also what you need to edit it again in the editor, and that includes where to put each component on-screen in the editor. The character's center and the edition widget center may match, but they don't have to.

Le code qui gère ça n'est pas tout neuf: il a permis d'améliorer les hitboxes pour attraper les objets dans SchoolRush il y a près de 10 ans. Mais j'ai commis l'erreur de copier-coller le code plutôt que de faire une fonction "importArea" qui serait utilisée des deux côtés: pour les zones actives et les zones passives.
ça n'aura donc pas été trop compliqué de faire en sorte que les deux utilisent le même bloc de code (le bon) et d'avoir une plate-forme qui fait *enfin* ce qu'on attend d'elle :-P

J'espère que je passerai un peu plus vite de la découverte du problème à la recherche de sa solution, la fois prochaine ^^"

If I want to fix that, I need to track not only hitbox#n ORIGIN coordinates, but also those of the 'solid' box -- the one used to decide whether the object cando() something. then when one 'ORIGIN' instruction shows up for the area we're interested in, we create relative coordinates. Hopefunny enough, it turns out I already have code that does precisely that, because I have both active and passive hitboxes and I failed to avoid repeating myself when writing the original parsing code ... and later failed to fix both copies when I realised things were not going as they should have.

Thursday, June 01, 2023

Déveloper pour DS

Figurez-vous que le youtubeur Nathan Fallet vient de se lancer dans une série de vidéo "live coding" pour faire un petit jeu DS. Son idée est de prendre juste libnds et les outils de devkitpro et de faire une adaptation d'un petit jeu pour smartphone qu'il avait fait il y a quelques années. L'idée est assez proche de ce qu'aurait fait un nouveau venu sur un forum à l'époque, donc j'ai envie de suivre ça.

Son point de départ, c'est l'exemple 'animate simple.nds', où on peut promener un sprite sur chaque écran avec le DPAD en modifiant l'image utilisée par le sprite en fonction de la direction prise.

Pour ce programme, les développeurs ont choisi d'embarquer les images directement dans la section "données en lecture seules" du programme. Là où se trouveraient aussi les chaînes de texte si on avait un texte. Pas d'ouverture de fichier avec ce système-là, mais pas non plus la possibilité de prévoir de charger/décharger des images de la mémoire principale pour y mettre autre-chose. En même temps, comme la DS a 4 beaux gros Méga, on devrait être tranquilles pour un projet modeste.

Il y a un programme (que je n'ai pour ainsi dire jamais utilisé) qui prendra en charge la lecture du fichier .png pour le convertir en données brutes (des tiles et des palettes) exploitables directement par le hardware de la console. Par contre, ce programme ne voit qu'une seule image à la fois. Il peut ranger les couleurs dans la palette, éviter les doublons, etc. mais si vous lui donnez deux .png différents, chaque .png produira son lot de sprites supposant que sa première couleur est en position 1 dans la palette. Essayer d'afficher les deux à la fois donnera de mauvais résultats. Le programme d'exemple masque un peu ça avec son man.png et sa woman.png, parce que dans ce cas précis, chaque personnage est chargé dans la mémoire de son écran. L'un sur le chip "main", l'autre sur le chip "sub". Il ne sauront jamais se rencontrer.

Le programme d'exemple n'utilise que la fonction "sprites" du hardware 2D. Aucun des 4 plans de décor possible n'a été configuré. Mais ça ne veut pas dire qu'on soit condamné à errer dans le noir pour autant.

Comme dans le cas de la Super NES, chacun de ces plans serait scrollable indépendamment des autres et possèderait une "couleur transparente": l'entrée 0 dans la palette de couleur. Mais si aucun plan n'a donné de pixel à afficher, le hardware nous mettra un plan avec la couleur 0. Et cette couleur, elle est trouvée tout simplement dans la mémoire vidéo. Les fichiers d'en-tête fournis avec libnds nous donnent un symbole BG_PALETTE qui permet de lire et d'écrire dans la palette "décor" de l'écran principal.

Ce n'est pas la seule manière de faire, bien sûr. On aurait pu activer un plan de tiles, remplir un tile avec une autre couleur puis remplir la map avec ce tile. On aurait pu activer un plan bitmap et remplir 98K de mémoire vidéo avec la même valeur. On aurait pu activer le moteur 3D et mettre un gros polygone monochrome...

Déplacez le BG_PALETTE[0] dans la boucle principale, modifiez sa valeur d'une itération à l'autre, et vous aurez un fond "stroboscopique". Pour faire un dégradé, par contre, je pense qu'il faudra passer par le HDMA. Mais ça, je n'ai pas encore tenté.

Bon voyage, M. Nathan ;-)

Je note au passage que depuis le temps, libnds s'est dotée d'un mécanisme de gestion des ressources (allocation dynamique de mémoire vidéo pour les sprites) et d'une cache pour la mémoire vidéo contrôlant les sprites (permettant potentiellement de les mettre tous à jour pendant le vblank)

Sunday, May 28, 2023

special properties

While thinking at 'how to implement conveyer belts', I end up investigating some currently incomplete code in special blocks handling. They have correct implementation of their collision areas, but since the migration to the new tile types engine, they can no longer act as 'doors' because they all have the same cando() properties.

There's some code in the parser to change that, and some code in 'FlatWorld' to return BlockInfo::props when we encounter one ...

Wait ... BlockInfo::props is precisely what this last `siscanf()` updates ... so ... that means the code is not incomplete at all ? it just needs to be tested ?

Thursday, May 25, 2023

Battle Plan : Big Caterpillar

Maybe these should stay secret. Maybe I should make sure my blog doesn't spoil the game by having too much "strategy guide" items in it. Who knows ... Anyway, here's a few ideas on how being able to throw things or having the Big Punch power-up would affect the fight.

It keeps the 'crawl' and the 'tail attack' of the BASIC version. It keeps the 'changing target' where there's one segment to hit, and that's the one where the magic stone currently is. It simplifies the fight by assuming we're good hitting *the segment with the stone* rather than the one next to it "so that it pushes the stone in the right direction".

J'avoue, ça fait un moment que j'hésite à les poster, celles-là. Ce serait moins drôle que mon blog se transforme en soluce du jeu avant même qu'on ne puisse l'essayer, non ? Mais bon, en même temps, j'ai besoin de pouvoir me confronter à mes idées, les trier, les rassembler... et les cahiers à points ne sont pas forcément l'idéal pour ça.

Voilà donc mes notes pour le face-à-face Bilou/Big Caterpillar. Le seul boss qui ait jamais été codé dans la version BASIC. Je conserve la manière dont il avance en rampant sur le sol et son attaque "coup de queue". Comme je l'avais déjà dit, le fait que Bilou puisse depuis SchoolRush lancer des objets change la donne. Plus question de demander de frapper toujours la tête ou la queue. Mais si on doit viser un segment en particulier, il faut que ce soit évident pour le joueur. J'avais déjà envisagé d'utiliser la gemme magique comme illustration pour ça, mais avec un système tordu où il aurait fallu frapper le segment *suivant* la gemme pour la forcer à avancer, frapper le segment précédent l'ayant au contraire fait reculer. Plus rien de ce genre: frapper la gemme la fera avancer, frapper ailleurs sera sans effet.

Then I started trying to bring all that into one single state machine. The goal was to ensure actions taken by the boss would make sense and not lead to tricky corner cases. For instance, the 'tail attack' should only be performed if there's enough room in front of the boss so that it does not attack through the edge of the arena. (as a bonus, that also means that it won't do that attack in a case where that would 'trap' Bilou in a situation where taking damage is inevitable). But then there's a new 'coil' attack that is completely horizontal.

Mais à côté des "idées de gameplay", j'ai voulu regarder si je parvenais à rassembler tout ça en un comportement cohérent. A quel moment faire quelle attaque pour éviter que la moitié du boss ne se retrouve dans un arbre ou qu'une des attaques devienne complètement imparable parce que le joueur n'a pas la marge de manoeuvre nécessaire. C'est comme ça qu'on se retrouve avec une attaque supplémentaire, horizontale et dans laquelle le boss s'assomme lui-même (ou écrase Bilou) quand il arrive trop près des murs.

Saturday, May 20, 2023

Zelda Breath of the Kingdom

Parce que non, j'avais pas pré-commande de Tears of the Kingdom. Il faut dire que je n'avais pas été complètement convaincu par Breath of the Wild, même si je l'ai finalement acheté pour mon compte après avoir fini celui que mon frère m'avait filé. Au bout d'un mois et demi. Bon, je dis "fini", en vrai, ça voulait juste dire "vaincu Ganon". En mode de base. S'il est vrai que ce sont surtout les enfants qui ont en suite joué à "Charly Top Chef", j'ai quand-même repris la manette (malgré son joycon drift) pour trouver tous les souvenirs de Zelda et débloquer une fin un poil moins crispée.

Mais non, je n'ai toujours vaincu aucun Lynel, peut-être même aucun Hynox ni aucune de ces grosses bêtes. J'ai un peu tâté du dragon (excellente idée, ces dragons: une des meilleures du jeu) et des équipements inhabituels, mais sans chercher non plus à compléter la tenue archéonique ni rien de ce genre. Bref, j'aurais facilement pu continuer à faire des choses intéressantes dans le jeu pendant des heures et des heures, mais je ne l'ai pas fait.

Le fait que le jeu demande de passer de longs moment juste à "tenir la barre" en allant d'un point à un autre y a joué pour beaucoup, mais par-dessus tout, il y a le sentiment que ce serait immoral d'aller faire tout ce genre de tourisme alors que Zelda est toujours dans le château à essayer de confiner Ganon. J'aurais tellement aimé qu'avec un DLC ils nous rajoutent la princesse en PNJ que l'on escorte d'un village à l'autre comme suggéré dans la séquence de fin. On serait allé repousser un Lynel pendant qu'elle discute avec les Zoras. Ce genre de choses.

ça, ça aurait eu du sens. Mais à ma connaissance, rien de ce genre n'a jamais été proposé.

Bref, il aura finalement fallu que je me retrouve complètement HS, incapable de dessiner ou même de lire, trop assommé pour décider d'un truc à regarder à la télé. Alors pourquoi pas regarder Link se balader sur le plateau du prélude ?