Showing posts with label guru meditation. Show all posts
Showing posts with label guru meditation. Show all posts

Saturday, April 04, 2026

NDS for DSi

I had announced on some discord channels that I had a new NDS demo of my ongoing work. One of these channels is the one where I'm in touch with Fei, who already tried a copy of the 3-rooms demo back in 2021 and 2023.

Back then, we finally managed to have something running at his place, but it doesn't seem it has been preserved. After locating a register that could tell me whether we're running on DSi (and then skipping the InitGBA function that would blue screen the demo -- as there are no GBA memory around), the best I could get is a "EFS search failed" message, but also a mention that 1) DLDI is around but 2) the demo is installed on Fei's SD card rather than in the filesystem.

Bad news is that I don't see anything in libfat or in libnds that would enable DSi' SD card access, which goes through completely separate hardware and isn't covered by DLDI at all (to my best knowledge). I'm not the only one to have such problems, but there was no answers to be found.

So I guess my best move to make progress there is finally to get homebrew software running on my own DSi thanks to StyleHax...  for that, the next step was to enable a DNS server on the cube. That sounded like a task the rubber duck could take care of and it suggested the following: 

Use a small local DNS resolver (dnsmasq or unbound) bound to the interface and configure only the few hosts to use it via per-interface/etc resolver settings or systemd-resolved split DNS. Since you want minimal config/deps on Ubuntu 18.04, dnsmasq is simplest.

  • Edit /etc/dnsmasq.conf (or add /etc/dnsmasq.d/custom.conf) and add: 
  • interface=eth1 # replace with your interface name 
  • bind-interfaces # ensure dnsmasq binds only to listed interface 
  • listen-address=192.0.2.10 # IP of that interface (optional, stronger) 
  • no-resolv # avoid reading /etc/resolv.conf 
  • add local hostnames: address=/host1.example/192.0.2.101 address=/host2.example/192.0.2.102 

ok, fine. That allowed me to fake some website with some other DNS name, and eventually get stylehax page shown on the DSi, even once making a green screen (exploit worked, but boot.nds was missing) and once loaded the dumptool.nds, but it complained the SD card was too small (it's a 128MiB one) and refused to do anything. Each attempt takes 1-2 minutes, and I'd say the success rate is at best 1:10.

One of my ideas was to use e.g. runME as boot.nds and work directly from there, but from this experience, it doesn't seem to be a viable plan ...  

Maybe I should pay a closer look to the updated nds_loader ? 

Maybe I should check the way GetDeviceOpTab is implemented and try to write something that reports all available devices ? (alt. repo from Patater)

Or maybe I should simply use another hack ? like the Memory Pit for the camera application ? 

edit (25/5): Got lucky with stylehax this morning: every boot.nds made it through in 1 or 2 attempts. So I managed to get DLDI scanning of Dreams-inspector.twl.nds and figured out I had to fix my Makefile to apply efstool processing over that new .twl.nds as well ^^. And eventually, I got Dreamlands 3-room demo running on my own DSi !_!  

Friday, August 09, 2024

Fixing the Inspector

I've been through another guru-meditation session last night. It's a bit weird to claim "I like it" while it implies checking hex codes between disassembled C++ and numbers shown on the Nintendo DS screen, looking up for virtual table addresses in gdb to confirm some arbitrary location found in the DS registers actually leads to a GobArea rather than a GobState and so. But I actually do like it :P

Eh oui: c'est encore un post avec des captures de débuggeur, du code assembleur, des chiffres sur la DS et des tas de gribouilles sur un bloc de feuilles: c'est la saison des guru meditations. Le fautif cette fois, c'est Inspector Widget, qui se met à regarder dans des zones mémoires invalides dans certaines situations. Dont la situation que je voudrais bien essayer pour vérifier une stratégie pour améliorer la branche-qui-rebondit. Parce que le tuning ne convainc définitivement pas.

Hopefully my "blue screen of death" isn't that dead and I can navigate almost freely in the DS's core memory to follow pointers and such. Hopefully, too, the bug I encountered last week-end wasn't too hard to reproduce. Well, you have to pick a specific sprite (the bouncing tree branch), give it the debugging focus, activate its passive collision box, then activate Bilou's collision boxes as well and frame-step the game until they collide again ... but at least I was doing that intently last time the bug happened.

Mais cette fois, j'ai été attentif à profiter du plantage sur la DS pour partir explorer la mémoire et prendre des petites notes sur un bloc-notes. Repérer les addresses du code sur la pile, prendre le début des différentes structures pointées par les registres, etc. 

ça n'a pas suffit, malheureusement, et je n'ai pas pu reproduire le bug dans l'émulateur, mais au moins cette fois-ci, je savais comment il était apparu et je pouvais le reproduire sur DS. Donc, recompilation, synchronisation des montres exécutables, petit passage par le stick WiFi et je me retrouve avec un setup de méditation digne de ce nom: la RAM sur l'écran bleu de la DS, le code désassemblé dans mon terminal, et un émulateur qui tente de reproduire le problème grâce auquel je peux vérifier des hypothèses comme "le 116eme byte à partir du début du GameObject, c'est le pointeur vers le GobState. Si c'est bon, le devrais y voir 0x0206c16c qui est l'adresse de la table virtuelle de la classe GobState".

But at least, I could upload a recompiled runme.nds to my DS over my wifi stick and have hex numbers that can be directly used rather than playing match-three between .elf debugging in ddd and hex numbers on the NDS. And yeah, that implied graph paper, writing down values, annotating assembly code with registers values and the like.

All of this to figure that I was missing a test against NULL somewhere in the Inspector Widget logic. and then I feel like a hunter coming back with some evaded cows, proud to bring them back only to be welcomed by his relatives frowning, with their arms crossed and a look saying, "Well, maybe if you were paying a bit more attention when crafting fences, we wouldn't have to hunt after our own cows every 3 weeks ! ..."

Anyway ... that was part of an attempt to see whether I could make Bilou better follow the branch position in a new "soft-land" state and it turns out that it will take more effort before it starts working.

Wednesday, February 07, 2024

Appleman 2.0

Bon, je me suis enfin refait les animations de l'Appleman dans l'éditeur d'animation modulaire. C'est que déjà avant que je ne m'attaque à Apple Assault, j'avais dans l'idée que Bilou puisse ramasser le corps de l'Appleman pour s'en servir ensuite de projectile. Puis c'est plus cohérent avec le comportement de dumblador qui joue le même rôle dans la School Zone.

Sauf que, vous vous en doutez, ça ne s'est pas passé sans mal. D'abord quelques plantages de MEDS (oui, encore) qui m'ont obligé à tout refaire, puis le niveau qui ne voulait plus rien charger le temps que je lui réexplique où se trouvent les animations demandées et que je corrige celles qui étaient lues en boucle alors que la machine d'état prévoit une transition "à la fin de l'animation".

You could easily claim that this was a cursed sprite and I would almost believe you. See, I've had applemen pixels from over 15 years now, and yet I wanted to upgrade its motion into a compound sprite. The idea would be to make it more like the dumblador, losing its feet when stomped, staying stunned until feet are recovered etc. I expect that it would feel fun...

Then the curse started, with blue screens in the editor as I tried to "clean up" some animations, despite I fixed something similar in September. Then the level would no longer load because some of the newly defined animations were looping and AppleAssault assumed it could wait for them to be done for some transition. And when all that was fixed, with WiFi transfers over RunMe through the NUC, the appleman did not have feet but huge, purple fists instead.

Ensuite, voilà que les pieds de l'appleman sont remplacés par des gros poings mauve alors qu'il devrait réutiliser les pieds de Bilou relookés en brun. La faute à un code encore un peu trop jeune dans le chargement d'un 2eme fichier de graphismes. Les couleurs erronées, ça, ça m'aura pris plus de temps. Je me replonge dans le code d'il y a 2 ans, je vérifie qu'on a bien assez de mémoire vidéo pour charger tout ce petit monde (scoop: oui. On a 16KiB, assez pour 8192 couleurs alors que les 16 palettes accessibles par les sprites n'en consommeraient que 4096)

To get proper images, the first thing is to ensure SpritePages are properly remapped when loading the additional set. For the school zone, that was done with spr.more "school.spr" page8,82 meds+4. Yeah, I know. That's not the most self-documenting line of script in the world ^^". What's important here is page8,82. That is controlling the remapping.

  • The first number (8) indicates that we expect Bilou.spr to feature 8 sprite pages, and that pages for "school.spr" are to be numbered 8, 9, 10, ...
  • The second is not a number: it is a series of digits each identifying a page, and each telling where each page is starting with the 8th slot. Page 8 remains unchanged (since the series starts with an 8), but page 9 will use page 2 instead (where Bilou hands and feet are)

That was for the school zone, but the green zone spriteset did not have hand and feet. Instead, they had to be added as the 11th page within the set ... almost page 20 for the level. Code parsing that was a bit crude too and was never meant for things identifying page number higher than 10. Oh, you could do it, but you might have to identify page 11 in the whole set with ;. I tried to simplify that a bit so that we can actually use spr.more "green.spr" page8,-..---.----2 meds+4 instead. Here,

  • every - in the sequence means "that page holds no sprites. It shouldn't be used by animations".
  • . means "don't touch: that page is perfect as it is". Any sprite page in green.spr has such a dot.
  • And you know about the final 2 already: it identifies one page within bilou.spr that must appear instead of a "draft" page of green.spr and be used by Appleman animations

Definitely, some colors from green.spr were loaded here. We wouldn't have the sprite-branch with colours that close to those of the trunk otherwise. But the yellow worm now has weird blueish outlines and the appleman feet are both light-Bilou-green rather than having one dark brown and the other darker brown. 

Et là, la raison était plus ... exotique, on va dire. Le code C++, rien à redire. C'est presque limpide: ça *doit* ajuster les commandes qui définissent les images et palettes utilisées par les personnages, à condition que le décalage soit correct. Et le décalage, je fournis à la main dans le script du niveau. Mais manque de bol: le compilateur (gcc 8.1.0 ajouté à devkitARM 49 en 2018) produit du code machine qui ne correspond pas à ce qu'il me faut (une fois encore). Vérification faite, le gcc 10.1.0 du devkitARM 54 ne fait pas mieux. Pour avoir enfin mes bonnes couleurs, il faudra que je réexprime mon code différemment pour contourner ce qui semble être un bug dans le générateur de code ou dans l'optimiseur ...

There's a line in the remapPages() function that should deal with that: find "change sprite" instructions in the animation code, isolate the current palette value and add the "adjust palette" computed by the caller. It should work, really, but single stepping through the code with all the optimized-away locals left me puzzled. So I dug deeper, taking notes of the machine code, tracking what register meant what and ... 0_0

and there was no add instruction to be found. Nowhere in the loop. The machine code generated by gcc 8.1.0 would just assign the same green_palette[0] to every sprite no matter what palette they were using. That seemed to happen with gcc 10.1.0 as well... it wasn't too hard to work around, but yet. Troublesome.

Saturday, September 30, 2023

Un p'tit coup de marteau ...

Mes dernières sessions avec AnimEDS s'étaient toutes soldées par des "guru meditation". Peu de travail perdu, heureusement, le problème se produisant généralement soit juste au début, soit juste après une sauvegarde. L'impact sur ma motivation à continuer à animer mes p'tits persos en prenait quand-même chaque fois un coup: j'aurais pu effectivement perdre un travail précieux.

Alors j'ai noté de faire du debugging de tout ça. Il y a un bail. D'abord refaire une version précise de l'animateur (rH2021) et garder le .nds et son .elf à côté des fichiers re-générés à tout bout de champ quand je bricole du homebrew, histoire que quand le problème se présente console en main, je puisse effectivement utiliser la valeur du registre PC pour retrouver un numéro de ligne dans le code. C'est le cas depuis Juin.

I can't help wondering whether this is worth translating. It's another epic (?) showdown between me and my code to see who's got the StrongARM. But well, it has been bugging me since January, crashing the animation editor almost every time I used it. I'm just lucky I never lost anything important but motivation to work on some cute things over the evening. The "guru meditation" screens I got during the first half of the year were almost useless: the AnimEDS build on my 'lime' DS was so old I did not have the matching .ELF file for my debugger anymore. Believe it or not, the RealLife (tm) has turned so intense that I even had to write down an agenda check list with "rebuild ; keep .elf apart ; upload .nds to lime" to actually get it done.

Pas de chance: c'est un de ces bugs où l'adresse ne dit pas grand-chose parce qu'on est a suivi des pointeurs de fonctions qui ne voulaient rien dire. Mais! Bonne nouvelle, avec les nouvelles animations pour l'Appleman, le bug est plus facile à reproduire: il suffit d'essayer de copier une frame d'animation juste après avoir chargé la première animation du fichier dans l'éditeur.

Alors j'ai ressorti mon émulateur et mon débuggeur et là, bonne nouvelle, ça foire aussi dans l'émulateur. Différemment, mais ça foire, donc c'est débuggable. Et là, j'ai galéré pendant des heures ... Des structures qui ne veulent rien dire, des vptr complètement à l'ouest ... Il faut dire que pour encoder la ligne du temps utilisée par l'éditeur, j'ai pris une std::list de std::pair de classe dérivées. Bref, on se perd sous les couches de templates, de membres dépendant de l'implémentation et d'optimisation du compilateur qui prend un malin plaisir à rendre inaccessible les variables dont on aurait besoin.

With that done, I wasn't much more lucky. It's one of those bugs where you try doing a virtual function call on something that isn't truly an object and thus end up in the middle of nowhere, especially where memory contents doesn't match any valid opcode. Hopefully, while trying to get the crash to write down registers values, I realised that the bug was actually easy to reproduce (just open one animation and try to copy the frame before selecting any frame) and a bit later, that it also happened with the same file in my emulator. At least I could save the evening where I navigate DS memory to reconstruct objects on paper this time.

J'allais jeter le gant puis un soir j'ai noté dans mon calepin "fait du débugging indirect en surveillant les constructeurs". Bah oui: la variable aura beau avoir été optimisée, il faut bien qu'il soit construit à un moment où à un autre, le TIFrame qui explique pourquoi j'ai du n'importe quoi dans les registres au moment de copier cette première frame.

Sauf que ... non. Les "TIFrames" sont construites vides, puis au fur et à mesure que l'AnimationParser traite la liste de commandes destinées à GEDS -- le moteur de jeu -- il complète les coordonnées, les numéros d'images, etc. J'étais sur le point de laisser tomber (traduire: réécrire tout avec ma propre classe 'liste' plus propice au debugging) quand je réalise que je peux "figer" l'afficher d'une ou de TIFrame dont je capture l'adresse à la construction, et suivre leur évolution jusqu'au bug (l'animation en question ne contient en réalité qu'une frame et une commande de contrôle). Et là, surprise, tout va très bien (Mme la Marquise :)

 It did not save navigating memory altogether though. The way gdb handles std::list and std::pair combined to the amount of variables that were actually "optimized away" when they would be critical to have around still turned that debugging session into some guru meditation. I first thought that it was because of some incoherent animation instructions into the animation itself, but inspecting how the animationParser processed them shown everything should be fine. Yet, if I tried to see what frame was triggering the bug, all I'd get was garbage non-sense. I was about to replace all that std::* by some pype::* when I realised that I could just break on constructors of the contained TimeItems to see what the list contained.

That did not work either, unfortunately, because when TimeItems are added to the list, they are "blank" items that will be modified by the yet-to-come UPDATE animation commands. What did help, was creating some static watches at addresses discovered in a constructor-breakpoints so that I could look at the state of my list just before the offending call. That and realising that the list was just fine, thanks, but that I was using an iterator that might not have been updated since the previous list had been trashed and replaced by the new one :P

Une seule explication restante: c'est l'itérateur qui devient invalide au bout d'un moment. Je sors mon cahier A4 histoire de me faire une map UML de tous les bouts de code impliqués et c'est bien ça: quand on choisit une animation à éditer, la liste est vidée, mais l'itérateur reste inchangé, ce qui est invalide.

Ah oui. Vous vous demandez "pourquoi le marteau" ... et vous n'avez pas Thor. C'est à cause de cette blague d'ingénieur où un consultant rentre une facture de $100000 pour une intervention et ça rouspète chez le client parce que "vous avez juste donné un coup de marteau!". Et le consultant de reconnaître qu'il a fait une erreur et de préparer la facture suivante

  • 1 hammer hit: $10
  • knowing where to hit $99990

(bon, c'était vraiment une grosse machine très chère qu'il fallait dépanner). Bin c'est un peu la sensation que ça me fait sur ce bug-ci. Sauf que je ne vais pas recevoir des cents et des mille et que je ne devrai pas les débourser non plus :P

 

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.

Saturday, August 06, 2022

scanf %ms ou scanf %n?

 it should have been quickly done. Almost trivial. Edit "DHud.cpp", revive calls to the MuadDebug class, change the GOB number so that it tracks the scorpeye rather than Bilou, and make it rewind time

  • when the shell's horizontal speed turns to zero
  • after it has been thrown (there's a flag telling us that)
  • after it has been picked up (need a new flag for that).

Except that however I tried to do it, and regardless of the amount of instruction-by-instruction stepping I made, I couldn't get the second condition to trigger. The optimizer did a great job at packing the expression evaluation code -- I mean there, that you can't tell anymore where you're in the source while doing it, and thus mostly can't set a breakpoint on a specific instruction.

Hopefully, there's the B opcode (not D, which is for Detach) to break at the script expression level. That one at least allowed me to realise that the expression for that second condition was indeed evaluated. But for some reason, the part I was interested in, with flag-setting, never got executed. A 'all done, bye' opcode always showed first. Mysterious...

Faire des essais dans le jeu pour essayer de reproduire un 'trick' pas fréquent, et avoir programmé un critère d'arrêt qui nous permet de passer en débugging pas-à-pas mais en revenant une image en arrière dans le temps, donc en revivant exactement l'évènement qui fait que tout est parti de travers en bullet-time pour pouvoir le comprendre et savoir ce qui doit être corrigé. J'appelle ça le muad-debugging, et c'est vachement pratique.

Enfin, surtout quand ça marche.

It turned out the reason was the expression got truncated even before it was turned into bytecode. Such a thing is not viable to extend the game to "Bilou Dreamland" level.

The code to parse an expression basically looks like

siscanf(base+cont,"[%64[^]]] (%64[^)])",predicate, axion)

And both 'predicate' and 'axion' arrays are static-sized. I know there's another way you could do the 'same' thing, but with any-sized arrays. That'd be something like

siscanf(base+cont,"[%m[^]]] (%m[^)])",predicate, axion)

in which case the siscanf function will allocate some memory by itself and you'll have to invoke free(predicate) when you're done with it. It is sure the way to go in most of your production code, but to be honest, I do not want this within GEDS script parsing code. Parsing a level already takes too much time. Adding dynamic memory for strings processing will only make things worse. Not to mention that it will fragment memory with temporary strings requested while we're making big blocks for the 'tank'.

But there's another trick I could (and likely will) use:

%n Nothing is expected; instead, the number of characters consumed thus far from the input is stored through the next pointer, which must be a pointer to int. This is not a conversion and does not increase the count returned by the function

Combined with the fact that %*s scans a 'string', but does not store it, I should be able to write

siscanf(base+cont, "[%n%*[^]]%n] (%n%*[^)]%n)", &predistart, &prediend, &axstart, &axend);

Now, I'd have start and end pointers for both expressions (if they're actually there), plus that would be copy-less and I would directly perform ascii-to-bytecode conversion. The conversion code would have to be adjusted, of course, so that it takes two pointers instead of one and that it does not expect a terminating \0 character at the end of the expression. So that will be a new "todo" item for when ongoing things are cleared and time is right to start something more experimental.

Oh, yes. And I could just grow the static arrays, of course. That's what I did in the past when they were 32-character long each, and I thought "well, 64-bytes predicates ought to be enough for everybody, right ? ". In another context, I could pick ridiculously large arrays (like 2048 bytes each) and forget about the issues for decades until it strikes back with some auto-generated script. But that would be a bad idea on the Nintendo DS with its fairly small stack. And nah, I won't make the arrays global either. That would sure break so many things in the ability to run that code out of its current context that it has even been considered as an evil thing to do by the former generation of developers.

Monday, June 27, 2022

gdb, intel flavour

J'ai beau avoir de nombreuses heures de gdb derrière moi, la syntaxe AT&T opcode source, destination, ça reste quelque-chose de complètement non-naturel. Quand on s'est habitué à écrire du code assembleur avec opcode destination, source, on y reste. Pareil pour les 42(%eax,%ebx,4) vs. [eax + ebx*4 + 42].

Et voilà qu'à travers le wiki du projet "gdb-dashboard", je découvre qu'il est possible de passer gdb en mode 'intel' d'une simple commande.

set disassembly-flavor intel

là. c'est tout.

I've been using gdb a lot already, but that never made the AT&T assembly syntax natural to me. So I was quite (happily) shocked to discover an option to switch to intel syntax instead. Huzzah! And not only that, but a 'dashboard' option that let me have disassembly + source + values + registers + backtrace view of what I'm doing without losing the power of the basic text-mode GDB command line !

So if you don't mind I'll make this post my advanced-gdb-recipies until all my work stations are in sync.

Et si on veut la même chose en sortie d'objdump, ce sera

objdump -M intel -mes-autres-options

[ sauvegarder et continuer ]

Attention: gdb-dashboard et ddd sont mutuellement exclusifs.

A tout hasard, un appel x86_64, ça donne call(rdi, rsi, rdx, rcx, r8, r9, stack...)

Et si jamais le débuggeur insiste sur le fait que "No function contains program counter for selected frame", on peut toujours forcer disas address, +size ou utiliser set disassemble-next-line on

edit: pour montrer/cacher un panneau, on utilise juste dashboard nom_du_panneau

Juste, pour éviter les pointeurs en bleu-foncé illisible, on peut ajouter les commandes gdb classiques à la fin du fichier .gdbinit (avant `python Dashboard.start()`):

set style address background none
set style address foreground cyan
set style address intensity dim 

Et pour les noms de registres (en noir sur mon Pc de bureau T_T), il faut aller éditer le style "low" ... j'en profite pour tester ces codes ANSI 256-couleurs:

'style_low': {
   'default' : '38;5;103' # 32-16 = 36*red + 6*green + blue
}

oh, et si jamais le panneau "source" paraît un peu étroit, 

dashboard source -style height 20

 (merci Andrea et l'équipe du wiki ^_^)

Friday, January 28, 2022

when the going gets panic, the tough goes kdump

NOTE: kdump is a service that can use kexec tool to create a log-and-core dump into some place in case of kernel panic by pre-loading an additional (rescue) kernel somewhere in memory

I discovered that last year because Red Hat Linux was doing it automatically when a crash occurred (and well, being developing drivers rather than embedded device, it is bound to happen again), but if I remember correctly, one can also --install  and ./configure it on Ubuntu.

When the kernel isn't entering PANIC, though, it could raise a WARN, where you still see a registers and stack dump, but things keep rolling. It might be telling you that something is unsafe and might have triggered a deadlock if we weren't so lucky, though. Better listen to them.

Sunday, October 31, 2021

Loading more, in LEDS

Bon, les soucis avec le moteur de jeu semblaient réglés, donc j'ai voulu vérifier que l'éditeur, dans sa version 1919 (une petite révision en-dessous du code sur mon laptop) présente sur Lime savait bien modifier des choses sans perdre d'information.

Je me rends compte que tenter de re-charger un deuxième niveau dans l'éditeur conduit au mieux à un iScriptException, au pire à un crash à une adresse venant du void*. Entre les deux, un "bon vieux" crash comme sur cette photo, qui pointe du doigt la boucle interne à Block::getnext()

Le 'pire crash' contenait quand-même quelques adresses vers du code dans la pile et suggère de regarder du côté de l'appel à Block::append() dans Level::ScanSubFile().

L'affichage des miniatures dans l'écran d'accueil, c'était pas terrible non plus. Plusieurs fichiers avaient juste des têtes de Bilou partout. Une résurgence d'un problème que je croyais cru réglé ? (ça se produit essentiellement avec greent.cmd et ses SimpleGobs, on dirait)

On dirait bien que ce n'est pas trop difficile de reproduire le problème avec 'cmdck' pour peu qu'on lui ajoute un moyen pour charger un 2eme fichier de commandes. Un peu plus de mettre le doigt sur le problème, puisqu'au moment du SEGV, on est envoyé à une adresse invalide.

Un cran plus tôt, on voit une liste de blocs dont le dernier a une table de méthode virtuelle totalement farfelue. Et ça, pour le premier bloc à traiter dans 'ink.gam'. Mais il y a eu un autre fichier .gam passé en revue avant, et le bloc farfelu est de type 'END'.

Je refais un 3eme tour de programme, je surveille tous les ajouts de blocs spéciaux. Et en réalité, le bloc "end" s'auto-ajoute dans la liste. Du coup, faire un 'delete' dessus parce qu'il n'est pas du type 'spécial', ça casse forcément tout.

edit: un petit 'ledsdbug.nds' qui reprenait les modifications utilisées en Avril pour corriger les miniatures, je remarque du coup que les miniatures sur NDS essaient toutes d'utiliser le sprite #0 ... louche ... la modif 'Anykey()' ramenée aussi et je constate que la ligne 'spr.more' n'a pas été correctement traitée. La faute à une valeur incorrecte pour la correction des numéros de pages d'animations (ouais. C'est technique hein ? j'aurais peut-être juste dû parler de 'pageshift', comme dans le code ^^). Rien de ce genre sur mon PC et pour cause: le code de LEDS a inversé le pageshift lors de la dernière sauvegarde ...

Par contre, du coup, l'outil de test cmdck -s greent.cmd > /tmp/green2.cmd me montre que toute la première moitié du script n'est pas ré-écrite après le traîtement. 'va falloir que je ressorte encore ddd. Euh non. ça c'était juste une interférence entre fprintf(stdout) et fopen("/dev/stdout"). 'faudra que je sois plus prudent avec ça.

re-edit: une fois ce pageshift corrigé, ça ne marche guère beaucoup mieux. Les miniatures ne sont toujours pas chargées parce qu'il y a des valeurs complètement incohérentes à la place des numéros d'animations dans le fichier. En fait, le fichier tout entier est sans-dessus-dessous dès qu'on atteint la section des animations et des miniatures.

Et ce n'est pas la première fois que j'ai des ennuis avec ces sections. Je ne peux pas exclure que c'est parce que j'ai utilisé une ancienne version de SEDS ou MEDS sur Lime. Mais je ne peux pas non plus exclure qu'il reste quelque-chose à corriger dans les éditeurs. (les éditeurs en version finale sont clean. Mieux: AnimEDS est capable de corriger un fichier foireux rien qu'en l'ouvrant et le re-sauvant).

re-re-edit: avec un fichier .spr valide, il me restait deux soucis à résoudre dans LEDS:

  • éviter les crash si on pointe dans le vide pendant l'édition des monstres (MonsterPropertiesWindow n'était pas prêt pour ça). Un 'simple' pointeur NULL, mais qui attire mon attention sur le fait que le hardware de la DS autorise qu'on lise à l'adresse 0 (en fait, tout une page de 32K).
  • avoir un affichage correct des miniatures produites par AnimEDS, et pas cet espèce de potée aux pixels que l'on voit pour Bilou. un simple '+2' qui manquait dans la version qui tournait sur DS.

Mais c'est compris et réglé pour attaquer le dernier week-end de congé. 'faudra que je note de comprendre pourquoi mon curseur de monstre a re-disparu, par contre.

Saturday, September 11, 2021

Wall walk test.

If I want to have an understanding of what causes Bilou to get into walls when he arrives straight into it, I need to find a way to reproduce that at will. I have to update my unit tests to cover that new scenario.

The difficulty with that setup is that the mecha will have to keep walking towards the wall for some frames, and only then try to turn back. Scripting a fake game pad controller to force the mecha to do that sure won't be the easiest path to follow. But I have a plan.

Si je veux comprendre ce qui conduit Bilou à finir dans les murs, il faut que je trouve un moyen de reproduire ça à volonté, c'est à dire reconstruire un setup dans l'environnement de tests "unitaires" que je me suis construit. Sans ne sera pas élémentaire, parce qu'il va falloir faire en sorte d'imiter le comportement d'un joueur qui continuerait d'avancer vers le mur avant de faire demi-tour, mais j'ai ma petite idée... Introduire un 2eme "mecha" qui marche normalement sur un terrain plus large et faire en sorte que notre sujet-test suive sa position de la même manière qu'une des chauve-souris-baie, sauf qu'il marche. Séduisant, non?   

Let's have a second mecha that is walking "normally" on one large floor while another one, the Gob-Under-Test is tracking its position and tries to follow it, just like Berry Bats would do, except "walking". That should work, right ?

Well, reality is hitting me hard, here. I haven't managed to get the test cycle work properly. I'm caught in the process of navigating the history and running because even with the obvious sorted out, my new test gets a "memory is leaking" warning and the former 'slopes' test isn't working either.

ça, c'est en Théorie. Mais une fois la frontière avec la Réalité franchie on se prend une jolie douche froide, sous forme de fuite mémoire dans le module de tests. Puis c'est le test sur les pentes qui ne passe plus. Et comme je n'ai pas encore commencé à utiliser la branche "default" comme "ok, tous les tests passent", je suis bon pour quelques retours vers le futur à bord de Mercurial. Je vous fais grâce des Makefile qui ne détectent pas les changement de version du toolkit et des fichiers .cmd qui ne sont reconstruit que dans certaines conditions, mais j'ai fini par trouver le commit qui plombe l'ambiance.

I think from now on, I'll use the 'default' branch as a way to track what is properly passing automated tests and what isn't. Good news, commit 6ab9f8ca2931 dating from this year (April 13rd) pass the slopes tests.

Caveats when doing things like this:

  • Makefile does not detect whether I change devkitpro version, so things that used to work years ago on an older devkit may fail to build nowadays and you'll have to manually clean output/*.o
  • some tests depend on fakreoot/*.cmd, which is not automatically re-populated from SchoolTests/efsroot/ or AppleAssault/efsroot/. and in SchoolTests, the files are generated from SchoolTest/cmd/, which does not occur if you just rebuild unit tests.

18731874

edit: I identified a commit at which the TestBasicSlopes started failing. Surprisingly, it was not a side-effect or whatever: the commit directly does


       if ((cspeed&cp) && !cdata[0]) continue;// this testpoint only works with speed
-      if ((chkflr&cp) && !(checks[pos] & F_FLOOR)) {
+      if ((chkflr&cp) && (checks[pos] & F_FALLTHRU)) {
        if (isi) isi->TestPoint(xtile, ytile, pos, checks[pos], "floor");
        cdata[GOB_TESTPOINTS]|=cp;
       }

Which cannot have no effect on how testpoints behave in the slope tests. But if we compare how monsters in the green zone behave before (1873) and after (1874) this commit, it is clear that it has desirable effects. So I'm now sure I have to fix the tests, not the way testpoints are handled.

Je m'attendais à une sorte d'effet de bord, résultant malencontreusement d'une modification qui n'aurait rien à voir, mais non. Ce commit modifie directement l'interprétation que le moteur de jeu a du concept de "sol". Jusqu'à 1873, pour être du sol, il fallait avoir la propriété "FLOOR". Depuis 1874, il suffit qu'on ne puisse pas tomber à travers une surface pour qu'elle soit considérée comme sol. 

edit++: okay. The leaking object was a 'builder' helper used by the GobState objets. The builder is a std::* object temporarily used while e.g. collecting transitions from a given state before they got 'flattened' into an array for the 'running' part. The array, just like GobStates are allocated in the 'tank', a larger memory chunk meant to host many small objects that all have the same lifetime: one level.

Ah, et pour les plus curieux, j'ai trouvé la fuite de mémoire. ça se passait dans le code qui accumule les informations sur la machine d'état avant de les "geler" dans un brave tableau de taille fixe. Sauf que ce gel est lié à l'introduction de transitions entre les états et que les tests qui fuitaient sont "tellement simples" qu'il n'y a pas de transition: juste un état unique dans lequel le sujet-test reste du début à la fin. Au travail. 

In the 'slope' tests, each state was used to spawn a GameObject using that state for the test. There were no transitions: we first use a forward-moving mecha to check things are fine when moving forward with the Forward state, and then a distinct backwards-moving-mecha. But for my new test, I need transitions. That means that I won't spawn for every states. That means some states are not 'frozen' into the tank, and hence still hold references to std::* objects like vectors or strings. These are the leaking ones. Let's get cracking!

Monday, August 30, 2021

Trouver le thread fautif

Bon, ce coup-ci, j'étais sur un autre cas de figure: le programme fautif est toujours occupé à tourner, mais un de ses threads est mort, causant une attente infinie dans un autre. Je sais demander un .dmp au Task Manager, ouvrir le .dmp en question sur la machine de dev avec les bons chemins pour les serveurs de symboles (eh oui, moi du futur: tu as oublié mais sous Windows, il n'y a pas de .elf de debugging: les symboles de debug sont d'office dans un fichier séparé) et parcourir la liste des threads en espérant y voir apparaître KERNELBASE!UnhandledExceptionFilter dans la pile d'appels correspondante. 

Ce n'est pas trop dur de repérer l'adresse du crash, mais c'est une autre histoire de connaître l'état des registres au moment du plantage.

Il devrait y avoir une extension de windbg pour gérer ça, mais le site qui la proposait a disparu dans les limbes du temps. Par contre si je parviens à deviner où elle se trouve, la structure EXCEPTION_POINTERS devrait nous fournir toutes les infos nécessaires pour continuer à debugger.

Eh bien voilà: cette structure est produite dans le stack frame (les variables locales, NdT) de ntdll!_C_specific_handler (encadré en rouge dans la capture ci-contre) et référencée dans le stack frame de UnhandledExceptionFilter. J'ai utilisé les adresses renseignées par la colone 'Child-SP' et retracé ça à la main dans Gimp, hein. Ici, un seul candidat: 0x16bee7c0.

Reste à s'assurer que c'est bien la bonne adresse. Je peux utiliser la commande windbg dt 0x16bee7c0 EXCEPTION_POINTERS pour le coup: si je suis dans le bon, elle me donnera deux pointeurs valides

En cliquant sur le 'exception record', on devrait avoir un code valide (type 0xc000000*) et une adresse qui correspond à notre fonction fautive (aussi renseignée comme adresse de retour pour KiUserExceptionDispatch, soit ici à l'intérieur de MoveSmall).

Thursday, August 26, 2021

Trouver un core dump ...

bin oui, parce que coder, c'est aussi chercher pourquoi un programme s'est planté. Linux est assez sympa sur ce coup-là avec son ulimit -c unlimited qui permet d'autoriser la production de 'core dumps' dans le répertoire courant pour un shell donné sans que tout le système ne se mette à saturer le disque dur en cas de dysfonctionnement. Après, on ouvre le dump dans un débuggeur et en avant pour l'autopsie.

Mais là, pas'd'bol, je suis sous Windows. Le boulot, vous comprenez. Le frangin de Jigé me proposait de faire clic-droit-debug dans le gestionnaire de tâches (onglet 'détails' de préférence, apparemment) et hop youpi, visual studio prendrait en marche le programme fautif. ça m'arrangerait assez bien vu qu'il est appelé depuis du python au bout d'une séquence de 3 ou 4 programmes histoire que la caméra à l'autre bout du fil soit dans les bonnes dispositions pour les tests. 

Mais re-pas'd'bol, quand je fais ça, j'ai juste droit à "not responding" sur Visual Studio puis les deux programmes qui s'arrêtent avec pas plus d'infos. ça marchait mieux avec "create dump file", mais ce n'est pas ce qu'il me faut aujourd'hui.

Pour le coup, je vais devoir refaire appel à devenv /debug dans une console VSpéciale et croiser les doigts pour être dans les bonnes conditions pour reproduire le crash. 

  • c'est apparemment normal d'être accueilli dans un Visual Studio qui fait semblant de ne pas savoir ce que vous voulez. Debug>Start Debugging devrait nous mettre en route
  • ça devrait aussi être normal que la fenêtre qui s'ouvre quand le programme est démarré (après tous les téléchargements de symboles depuis les servers MS) se ferme automatiquement en cas d'arrêt du programme. On va mettre un p'tit breakpoint sur exit pour calmer un peu tout ça;
  • ne pas oublier le .exe à la fin du nom du programme. Faute de quoi, VS ne le trouvera pas et proposera juste un bouton 'Attach...' au lieu du bouton 'Start' !
  • bon, par contre j'ai beau torturer ma ligne de commande, pas moyen de reproduire le crash dans cet environnement-là. Il va falloir trouver autre chose.

Et le plus agaçant, c'est que je sais que Windows peut le faire. Sur tant d'autres machines, il m'a saoûlé avec ses boîtes de dialogues ok/cancel/debug... ça pourrait être parce que Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug est vide, mais non: il me fait l'effet d'avoir bien tout ce qu'il faut là où il faut.

Ahmaisattends ... le programme de test mélange une bibliothèque C++ et du code compilé avec GHC, et le runtime haskell a un flag supplémentaire +RTS --generate-crash-dumps -RTS. On peut toujours essayer d'utiliser ça pendant notre debugging VS... Ah ? ... Bingo ?

On transfère bien son .pdb sur la machine de test ... Et voilà: j'ai un nom de fonction et un numéro de ligne. La méditation va pouvoir commencer.

Ah oui, il y a aussi le Windows Error Reporting (WER) system, et ses fichiers "planqués" dans C:\ProgramData\Microsoft\Windows. C'est ce qui ressemble le plus à mon ulimit. il faut créer la clé HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps et y définir "DumpType"=dword:00000002 (et plus si affinités. Attention, ça change le répertoire utiliser pour les dumps :-P)

Il y aura aussi procdump -e à tester, tiens.

Monday, April 26, 2021

pyramt.map

Files transferred. Last week, I had just complete mess of tiles, some even not part of the file, but things remaining from the previous level. Hopefully, each of these level worked fine in the level editor on the NDS.

With a bit of tweaking, I could also get the shown in runMe. After I remembered I had a button to show the loading log in case of failure, I realised I had a few files missing (mostly .gam files) and I could get school title screen working with sprites as well.

But as far as pyramids and green zone maps are concerned, I'm still trying to get them working. Dreams.nds itself triggers a blue screen somewhere in GobAnim::translatecommands.

Avec un peu d'astuce, je peux maintenant avoir un niveau "pyramidesque" dans l'éditeur comme dans runMe. Pour runMe, il aura fallu que je me souvienne d'activer l'affichage du log (parce que oui, il est toujours là, juste caché), histoire de réaliser que la DS n'a pas le fichier .gam demandé. L'éditeur de niveau peut passer par-dessus un problème de ce genre, mais pas le moteur de jeu. 

One thing, though. It's nice to have a memory crawler embedded into my exception handler, but I should make sure it doesn't trash the precious registers I had on-screen when I'm exploring invalid addresses (first, because it is impossible to hop from 0x0b00xxxx stack addresses to 0x020xxxxx heap/code without entering some invalid addresses, and second because moving down to higher addresses makes the DPAD to enter addresses confusing at best)

Saturday, January 02, 2021

Only 6 releases and 2.5 years behind

Believe it or not, that's actually a pretty up-to-date devkit I was running (by my standards) when I encountered a weird GCC bug that would turn a for loop into an endless one. I'm not super-hot about upgrading the toolchain while I just managed to have all my tools working again, but it should be safe to give them a try, at least.

There is at least one trick I'd have to fix or revert: I tried to override the _jp2uc function to save some room in the binary, but with devkitARM release 54, the trick doesn't work anymore. (well, the trick saves 14K. It wouldn't be such a big deal to drop it).

/opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/10.1.0/../../../../arm-none-eabi/bin/ld: /opt/devkitpro/devkitARM/lib/gcc/arm-none-eabi/10.1.0/../../../../arm-none-eabi/lib/thumb/libg.a(lib_a-jp2uc.o): in function `_jp2uc': (.text._jp2uc+0x0): multiple definition of `_jp2uc'; .//outnds/Demo/shrink.o:.//Demo/source/shrink.cpp:22: first defined here collect2: error: ld returned 1 exit status .//common.mk:360: recipe for target './/Demo/Demo.elf' failed 

That trick is based on the idea that symbols from libraries are weak, and thus can be overridden from the linking application if the application decides so. Maybe that assumption doesn't hold anymore ?

  • apart from that trick, no breaking errors occured while rebuilding
  • there is now V=1 support in ds_rules. I'll have to check to what extent that allows common.mk to be simplified.
  • Both LEDS and SEDS seems to work with the new release.
  • I couldn't reproduce the odd bug with latest gcc.

Note: I had tried to solve an issue with the editor getting stuck when using that Zap! command since early 2019, which is in the middle of the "devkitpro update" started in September 2018.

It's not that it crashes when we hit 'Zap!' on the palette windows: the code stalls. That's not helping to track where the bug is, of course. On alternate palettes, it doesn't it keeps working (on emulator) but extremely slowly... sometimes.

The amount of steps performed during this operation is too high to make step-by-step debugging in desmume any practical. And when it hangs, it is in the middle of nowhere, with no code to execute...

It also explains why attempts to use natively-compiled code to unit-test the issue kept useless.

Thursday, August 06, 2020

Petits essais desmume

J'ai pris conscience il y a quelques jours que je n'avais toujours pas écrit de document présentant le fonctionnement de AnimEDS, pourtant lauréat du concours NeoFlash.

Du coup, comme j'ai un PC windows qui traine à la maison en ces temps confinés, j'ai
- re-téléchargé un desmume-0.9.11
- configuré le chargement des ROM en mode "load into RAM" (pour la compatibilité DLDI)
- ajusté le SLOT 1 pour émuler une R4 vers le répertoire "efsroot" de mon dernier tuto en date (auquel j'ai quand-même rajouté un sous-répertoire "moving", pour la forme).

ça suffit pour LEDS. Maintenant, voyons AnimEDS ...
 
Some days ago, I realised I still had no document describing how one would use my Animation Editor for Nintendo DS: AnimEDS, despite it is the only homebrew I had ever submitted to a coding compo. 
Since I have a windows PC at home during "lock down", I re-downloaded desmume-0.9.11 and went through the required "load into RAM" and setup SLOT 1 folder through the 'Config' menu . (yep, I'll be emulating an R4 on slot 1, this time). Contents of the folder will be that latest tutorial I uploaded on gbatemp. That worked for LEDS (packed with the tutorial), but not with AnimEDS.
 
PS: oui, j'ai bien lu: les gens derrière le projet Desmume recommandent plutôt de prendre un "nightly build" plutôt que les "stable build", mais ces derniers sont les seuls à ne pas faire grincer les dents de Winwin avec un auteur du logiciel non défini. On ne va pas tenter le diable, non plus.

Par contre, le dernier build que j'ai d'AnimEDS ne parvient pas à charger les fichiers .spr de la même manière. J'aurais dû m'en douter >_<.

Puis les vacances sont arrivées. J'ai pris un moment pour comprendre ce qui se passait, et pourquoi il fallait une combinaison "desmume/devkit" pour que ça coince. 
 
I should have guessed. I knew there's a compatibility issue between desmume 0.9+ and devkitpro's resources since I downloaded them back in 2018. So far, I just rebuilt stuff with my 'older' devkit (still running GCC 4.x >_<), but the laptop where I did that seems to be definitely damaged. Well. It's locked-down holiday time and the painting is drying on the walls, so I kicked up my debugger and started investigating what happened.
  • things work fine on a real Nintendo DS
  • things work fine with an old emulator
  • things work fine with an old devkit compiling nearly the same sources.
  • when things go wrong, we see that fatInitDefault fails. Yet it found the 'fat' device but it cannot mount it.
Et j'ai fini par me rendre compte que toutes les fonctions liées au DLDI pointaient vers la même fonction qui return false.

Je creuse encore et je me rends compte que les fichiers .nds qui marchent sont renseigné comme ayant un autre type de header que les .nds qui échouent. Et que le code de desmume a un test du type "taille du header < 16K" pour décider si il a affaire ou non à un homebrew.

When I realised that all the IO functions in the DLDI interface were pointing to the same code (actually just returning false), I started to be very suspicious about desmume's DLDI patching feature, and I re-downloaded its sources. While it was building, I noticed that the old-built-working .NDS and the new-built-failing .NDS were not reported similarly by the 'file' linux tool. And here's the catch:
  • desmume has some code checking whether it is running a homebrew before patching DLDI
  • that code only work for so-called 'old' (512-bytes) headers.
  • 'new' devkitpro tools use 'new' (16K) headers by default.
Visiblement, les "nouveaux" homebrews ne sont plus reconnus comme tels, et donc même plus candidats au patch DLDI. Heureusement, l'outil ndstool est toujours capable de faire des "vieux headers" (-h 0x200) en plus des "nouveaux headers" (par défaut, -h 0x4000).

Il faudra que je vérifie que ça marche aussi avec l'émulateur "tout frais" sous Windows, bien sûr, et que je me souvienne que je suis aller bricoler dans /opt/devkitpro/devkitARM/ds_rules ... un patch qui n'était pas indispensable puisque j'ai repris une règle équivalente dans mon common.mk.
Enfin, avec un peu de chance, je pourrai enfin faire mon tuto "comment utiliser AnimEDS" ;)

In devkitpro packages, there is a tool that convert .elf compiled files and into a single .nds ROM image: ndstool. Hopefully, it is still able to produce both types of headers, you just have to request it with -h argument. And luckily, I already had replaced the default makefile rule that uses ndstool in my own common.mk helper, so I don't need to mess with the contents of /opt/devkitpro to get working NDS files again... and be able to load contents in AnimEDS. Tuto-writing can at lasts start (as soon as the temperature drops under 30°C, that is)
 
Sounds like it was all over ? Well, there's a bit more of it. While I was debugging desmume, I trigger another issue that makes desmume-0.9.11 crash on my Ubuntu laptop when it encounter a symbolic link. A bit of bug hunt located a pretty trivial error in vfat.cpp. I'm proud to say that this is my first accepted pull request on a homebrew-related project ^_^ 
 
So
  • I need to use the desmume-cli version I recompiled (0.9.12)
  • desmume-cli --slot1 R4 --preload-rom --slot1-fat-dir FAKEROOT_DIR TOOL.NDS
  • with 0.9.11 (still what's shipped on Ubuntu 22.04 :P) I frequently use desmume-cli --load-type=1 --gbaslot-rom=DS/Dreams/Dreams.nds DS/Dreams/Dreams.nds --nojoy=1 to run standalone games.