Re-ran the audit fresh and fixed everything it turned up: 4 dead pre-Astro internal links repointed to their real /blog/<slug> routes, one dead external cover image (perforce.com) vendored locally from a Wayback Machine snapshot, one dead external link (Caltech101 dataset) repointed to its current home at data.caltech.edu, one dead link (isart.com's old summer-camp page) unlinked with no confirmed live replacement, and one dead link to a private Gitea repo (git.gabvdl.xyz/gabrielvidal/backrooms) unlinked — making it public is a call for a human. Manually verified all 12 itch.io 403s plus kryptview.com and cali-rse.com are genuinely live (bot-walls, not dead pages) and added them to an explicit, commented allowlist in the audit script so it stops reporting the same false positives. audit:links:full now comes back clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 line
100 KiB
JSON
1 line
100 KiB
JSON
[{"slug":"2017-08-15-Low-Rez-Guess","category":"blog","title":"Low Rez Guess","description":"This quick guessing arcade game has been made for the LowRez Jam 2017.","tags":["Gamedev","Game jam"],"body":"\n\n\n## Context\n\nThis game has been made for the [LowRez Jam 2017](https://itch.io/jam/lowrezjam2017). The goal of this jam is to create a game with a resolution of 64x64 pixels. There was no other restrictions (like a theme) but I think this is a very good constraint to work with.\n\n_In this game, your goal is to guess what is shown in images from various categories (vehicles, animals, objects, etc.). The faster you guess the images, the more points you earn!_\n\n## The idea\n\nMy idea was to use the low resolution constraint to make things hard to decipher on screen. I then thought of a growing picture, that could start by being 1 \\* 1 pixel, and as the image grow, at some point you can understand what the image is showing. The player could gain points based on his speed: the faster he gets the picture, the more points he gets.\n\n## The game\n\nAt the beginning of a round, you will see 3 choices, that correspond to the left, right and up directional key. All rounds start with a really small and pixilated image at the center of the screen. As time goes on, the picture will grow to fill the entire screen. You have to press the key corresponding to what the image depicts. The faster you are, the more points you get!\n\nThere are various categories of objects (101 to be precise) and they come from training data for deep learning. The data set is available on [Caltech's website](https://data.caltech.edu/records/mzrjq-6wc02).\n\nI wanted to make a very accessible game, so I integrated internationalization by supporting 6 languages: English, French, Italian, Spanish, Portuguese and Deutsche. This was easy, because I only had to translate the 101 categories name, and some of the UI text. The major difficulty was find a readable pixilated font, that also support accented letters (for languages like french and spanish). I didn't manage to find one, so I edited one with only ASCII letters to add accented letters.\n\n## Play the game\n\n<div style=\"aspect-ratio: 1\">\n<iframe frameborder=\"0\" src=\"https://itch.io/embed-upload/567546?color=8f0000\" allowfullscreen=\"\" width=\"100%\" height=\"100%\"><a href=\"https://gabrielvidal.itch.io/low-rez-guess\">Play Low Rez Guess on itch.io</a></iframe>\n</div>\n\n## Tools\n\n- I mostly used [Unity](https://unity.com/) for everything from animations, IO, scripting and integration of all elements.\n- I made the music myself using [Bosca Ceoil](https://boscaceoil.net/)\n- [Freesound.org](https://freesound.org/) for the sound effects\n- As said earlier, all the images that the player can guess come from a training dataset for image recognition available on [Caltech's website](https://data.caltech.edu/records/mzrjq-6wc02)\n"},{"slug":"2017-12-15-first-prototype","category":"blog","title":"First Prototype","description":"I decided to use Unity engine, because it's the game engine I'm the most comfortable working with. First I started to work on a plant system. I want the player to be able to plant plants and watch them grow in fast motion.\n","tags":["Gamedev"],"body":"\nI decided to use [Unity](https://unity.com/) engine, because it's the game engine I'm the most comfortable working with.\nFirst I started to work on a plant system. I want the player to be able to plant plants and watch them grow in fast motion.\n\n## Mesh generation\n\nTo create plants, I decided to generate the plant meshes procedurally in order to generate plants based on the surroundings.\n\nThe plants are constructed with multiple cylinders with varying width and position.\n\n<div class=\"figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/SectionSegment.PNG\">\n <figcaption>The meshes are constructed from triangles</figcaption>\n </figure>\n</div>\n\nI then wrote some code to procedurally generate this mesh, starting with a single segment.\n\n<div class=\"grid\">\n <div class=\"cell cell--12 cell--lg-6 figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/MeshCreation2.gif\">\n <figcaption>A cylindrical mesh composed of only the two ends</figcaption>\n </figure>\n </div>\n <div class=\"cell cell--12 cell--lg-6 figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/MeshCreation3.gif\">\n <figcaption>The final result for a single plant segment</figcaption>\n </figure>\n </div>\n</div>\n\n### Animation & Noise\n\nI wanted to make the plant grow fast in front of the player. To do this, animating the meshes was the next step. One essential component is also noise, to make the plant diverse and procedural. The noise I used is a 3D Perlin noise.\n\n- Radial noise (for the variation in the with of the trunk)\n- Noise influence across the length of the stem\n- Noise in the growth animation before reaching the final girth\n\nThese noises are controlled across the length of the stem and also across time.\n\nThe way I constructed the segments permitted me to easily animate the width of the segment with some noise.\n\n<div class=\"figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/MeshCreation4.gif\">\n <figcaption>An animated version of a single trunk segment</figcaption>\n </figure>\n</div>\n\nThe next step was putting these cylinders back to back to create growing stems. I tried to make all the parameters accessible to have a maximum of control on the way meshes are created. Only my imagination is the limit!\n\n<div class=\"grid\">\n <div class=\"cell cell--12 cell--lg-6 figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/MeshCreation7.gif\">\n <figcaption>A growing stem</figcaption>\n </figure>\n </div>\n <div class=\"cell cell--12 cell--lg-6 figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/long.png\">\n <figcaption>A really long and blocky plant (more like an icycle)</figcaption>\n </figure>\n </div>\n</div>\n\n### Collision\n\nI wanted to make plants grow in the environnement and be completely procedural so the next step was making the plants collide with the surroundings.\n\nI used Unity raycasts to calculate collision all at once when the mesh is created. After a lot of trial and error, I finally managed to make this work!\n\n<div class=\"figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/collision.PNG\">\n <figcaption>A really long mesh trapped in a confined space</figcaption>\n </figure>\n</div>\n\nI then experiment with very thin, spaghetti-like plants, that would look like cables roaming through the white default sandbox.\n\n<div class=\"grid\">\n <div class=\"cell cell--12 cell--lg-6 figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/cables1.png\">\n </figure>\n </div>\n <div class=\"cell cell--12 cell--lg-6 figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/cables2.png\">\n </figure>\n </div>\n</div>\n\n<div class=\"figure\">\n <figure style=\"max-width:100%; width: 100%\">\n <img src=\"/assets/projects/GrenGame/prototype/cables3.png\">\n <a href=\"https://www.reddit.com/r/pcmasterrace/comments/5r1cr4/cable_management_from_the_depths_of_hell/\"> <figcaption style=\"font-style: italic;\">Cable \"management\" from the depths of hell</figcaption></a>\n </figure>\n</div>\n\n### Gravitation influence\n\nAnother really important component is the influence of gravity along the plant. This value can be controlled along the stem, to have a plant go upwards (for trees for example), or be close to the ground, like vines or roots.\n\n<div class=\"grid\">\n <div class=\"cell cell--12 cell--lg-6 figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/Gravity_Up.PNG\">\n <figcaption>plants going upwards as they grow</figcaption>\n </figure>\n </div>\n <div class=\"cell cell--12 cell--lg-6 figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/Gravity_Down.PNG\">\n <figcaption>Plants with really heavy ends, so going downwards when reaching a certain height</figcaption>\n </figure>\n </div>\n</div>\n\n<div class=\"figure\">\n <figure style=\"max-width:100%; width: 100%\">\n <img src=\"/assets/projects/GrenGame/prototype/WIP7.PNG\">\n <figcaption>A plant going straight up and then crawling on the floor</figcaption>\n </figure>\n</div>\n\n## Final results\n\nCombining all these elements, we can get visuals like this:\n\n<div class=\"figure\">\n <figure>\n <img src=\"/assets/projects/GrenGame/prototype/Combination.gif\">\n <figcaption>Procedural mesh generation with collision handling and animated growth</figcaption>\n </figure>\n</div>\n"},{"slug":"2019-02-04-What-do-games-need","category":"blog","title":"What do games need","description":"What do games need? Text written for a Game design assignment where I had to answer this question.","tags":["CSUMB","Game Design"],"body":"\nThis text was written for a Game design assignment.\n\n## What do games need?\n\n**Games should involve our creativity as much as skill.**\n\nMore and more free games gain popularity, as a younger audience take over the elder. The goal of most of these games is to make profit, and all means are being employed to make the player spend money or to gain money behind his back using ads or selling his personal data. Keep in mind that if we use a free service, this service uses us as a product to make money. And because most players won’t spend money on free games, the game needs us to play the most amount of time possible, to watch a lot of ads, that it can make money off. Thus, free games don’t require us to have any kind of skill or creativity, they just want us to play the longer we can. Therefore “Merge” type games have gained a lot of popularity these days, they don’t require us to have any kind of intelligence, skill or creativity (even chimpanzees can play it).\n\nEven if this type of game is not the major parts of games, some games use competition to make us play longer. However, competition rarely highlights creative behaviors (most competitive game are games where players fight each other). Some games try to support creative behaviors, even in a fighting game, where you can have a creative strategy (for example, the card game Hearthstone, in which you create your own deck to battle other players). The creativity is often limited by resources (cards, for Hearthstone) or skill (if you can use a wide range of weapons to defeat enemies, but some are better than others. To be creative and use other weapons, you must be good at the game in order to compensate the bad weapon).\n\nRare are the games that value creativity more than anything else. I can understand that this can be unfair, as some people are creative right away, and some feel not creative at all. We often think that it is useless to play these kinds of games, as we will always lose because we are not creative. However, we don’t say that when the game is skill-involved. We have to understand that creativity can be developed as much as skill or intelligence: through practice. You can get more creative by playing creative games, or by playing with creative people.\n\nOne thing I would like to point out is that competition is not an enemy of creativity. The competition process can value creativity as much as skill. For example, a game jam offering prizes can be a very competitive environment, but the valuable competence would be creativity instead of skill, for example in an e-sport competition.\n"},{"slug":"2019-02-28-Game-Concept","category":"blog","title":"Heist Game Concept","description":"Multiplayer museum robbery coop/competitive arcade game. Players need to cooperate in order to steal a valuable item in a museum or a jeweler. However, there is not enough stuff to steal for all players (you have to be the fastest and use low blows). Fast paced cartoonish visuals and mechanics. Guards keep the things to rob and they adapt each time they catch a player (like in Hello Neighbor)\n","tags":["Gamedev","Idea","CSUMB","Game Design"],"body":"\n## Pitch\n\n_Multiplayer museum robbery coop/competitive arcade game.\nPlayers need to cooperate in order to steal a valuable item in a museum or a jeweler. However, there is not enough stuff to steal for all players (you have to be the fastest and use low blows). Fast paced cartoonish visuals and mechanics. Guards keep the things to rob and they adapt each time they catch a player (like in Hello Neighbor)_\n\nFinally, I think the game will be more about infiltration and cooperation, not much competition between players. I want the atmosphere to be a little bit oppressive, so when players are focused when sabotaging traps or forcing open doors, they can be really scared as the keeper rushes toward them.\nI think I want the game to feel like in the game Hello Neighbor, that is when you are investigating inside the neighbor’s house, you feel pressured as every noise could be him or not, and sometimes you are trapped inside, because you discover the layout of rooms and door as you get chased.\nYou feel insecure as soon as you enter his property, because there is no place he can’t reach or find you.\n\n\n\n## The Keeper\n\nThe keeper is an AI. It can place traps and security systems.\nI’m thinking that multiple keepers can be a good alternative to compensate for the number of players, instead of having only one keeper that is impossible to fool.\n\n<div style=\"display:flex; flex-direction: row; justify-content: center; \">\n<img class=\"image image--md\" src=\"/assets/posts/Robbers/guard.jpg\"/>\n<ul>\n<li>Players need to complete skill checks to disable security systems.</li>\n<li>Players walk slower than the security guard, so when a player gets chased, he can escape only if he outsmarts the guard. If a player get caught, the guard will carry on his back and throw him out of the building (The player will have lost all his stuff, and he will be back at the main entrance of the building).</li>\n<li>The main idea behind the fact that the guard adapts from players behavior is that he will remember where and when he has seen players, and he will place traps in the most frequently used spots (for example, in the middle of a corridor).</li>\n</ul>\n</div>\n\n\n\n## Players\n\n<div style=\"display:flex; flex-direction: row; justify-content: center; \">\n<img class=\"image image--md\" src=\"/assets/posts/Robbers/player.jpg\" style=\"aspect-ratio: 16/9\"/>\n<ul>\n<li>Players will be able to gather items, such as a flashlight, a grappling hook, some firecrackers to distract the guard and more.</li>\n<li>Because players are smaller than the guard, they can fit and crawl through vents in the place they rob. However, if players use vents too often, the guard will seal off vents and players will have to use special items in order to unblock them. Even if the guard can’t fit in the vents, you can’t stay in it because he can steal, grab you and carry you out.</li>\n</ul>\n\n</div>\n\n\nWhen a player reaches the thing to steal and try to walk with it, the guard will be faster and the player carrying will be considerably slower. The player can throw the object in a direction to prevent the guard from getting it back.\nIf the guard gets the item back, he will put it back at its place and stay around to avoid players robbing it back as soon as it’s back.\n\nWhen a player performs a disarming (or sabotage) action to disable a trap or open a closed door, He will have to do a skill check. A skill check is a quick action that he has to perform in a short period of time (for example, click on a button as soon as it’s green, and if he presses it when it’s red, he loses). If a player fails a skill check, the progress of the overall action will be stopped, and the guard will be notified of the position of the player (imagine the player making a big noise, this will draw the attention of the keeper).\n\n\n## The Map\n\nThe map is composed of two parts:\n\n* The outside, where players are safe and where the keeper will not chase players that tried to steal the hoard.\n* The inside, where players can’t legally be. If the keeper sees them inside this zone, he will chase them.\n\nEvery time a player is caught inside by the keeper, he will be carried to a trash conduit that is directly connected to the outside.\n\nThe core loop of a game is that players enter the building through the main entrance, and by sabotaging barricades and by unlocking doors, they will have more and more access to the entirety of the building. New entrance will be unlocked and they will have to navigate through this maze of doors and rooms to find a way to access the main room, where the valuable things lie.\nThe keeper has access to all the rooms,so players must be very careful when entering an unexplored room, as he could be waiting inside.\n\nA basic map could be a map with a direct view on the thing to steal, for example a painting. The players can’t access the painting directly, as it’s protected by multiple layers of security systems. For example, the door to the room is locked and the key is in another room. In addition, there are lasers around the painting that will trigger the emergency state of the building if something passes through. On top of that, the painting is in an indestructible glass box that has to be opened with care and the right tools (a blowtorch). Opening the box doesn’t make a lot of noise, but it takes a long time, so players have to plan their attack, and set up a lot of things beforehand.\n\n\n"},{"slug":"2019-03-11-MDA","category":"blog","title":"MDA","description":"MDA analysis of the game Tictactocalypse","tags":["Gamedev","Game Design"],"body":"\n## Definition\n\nIn game design the **Mechanics-Dynamics-Aesthetics** (MDA) framework is a tool used to analyze games. It formalizes the consumption of games by breaking them down into three components: Mechanics, Dynamics and Aesthetics. These three words have been used informally for many years to describe various aspects of games, but the MDA framework provides precise definitions for these terms and seeks to explain how they relate to each other and influence the player's experience.\n\n\n\n- **Mechanics** are the base components of the game - its rules, every basic action the player can take in the game, the algorithms and data structures in the game engine etc.\n- **Dynamics** are the run-time behavior of the mechanics acting on player input and \"cooperating\" with other mechanics.\n- **Aesthetics** are the emotional responses evoked in the player.\n There are many types of aesthetics, including but not limited to the following eight stated by Hunicke, LeBlanc and Zubek:\n\n - **_Sensation_** (Game as sense-pleasure): Player enjoys memorable audio-visual effects.\n - **_Fantasy_** (Game as make-believe): Imaginary world.\n - **_Narrative_** (Game as drama): A story that drives the player to keep coming back\n - **_Challenge_** (Game as obstacle course): Urge to master something. Boosts a game's replayability.\n - **_Fellowship_** (Game as social framework): A community where the player is an active part of it. Almost exclusive for multiplayer games.\n - **_Discovery_** (Game as uncharted territory): Urge to explore game world.\n - **_Expression_** (Game as self-discovery): Own creativity. For example, creating character resembling player's own avatar.\n - **_Submission_** (Game as pastime): Connection to the game, as a whole, despite of constraints.\n\n## Tictactocalypse analysis\n\n### Mechanics\n\nPlayers place tokens on a 4 by 4 grid, trying to align groups of three pieces, to earn point. Once a player gets the required amount of point to win, the game stops. If the timer runs out and no one has reached the goal, the player with the most point wins the game.\n\nPlayers can also trigger events to knock other players’ pieces of the board, or to reinforce their position by making pieces invulnerable to events. Events have a cooldown after they have been triggered according to the strength of the triggered event. Players receive new event every 5 seconds.\n\nIf a piece is away from its slot but still on the board, a clock will appear on the piece, and after a short time, the piece will be moved to its original position. During this time, other pieces can claim the slot by placing a piece on it.\n\nEvery 5 seconds, all players win 3 points per groups of three pieces aligned, and the events each player can trigger are changed.\n\n### Dynamics\n\nGiven that there are 2 types of events, events helping the player who triggers it and events that slow all players, they have to choose carefully what event play at what time of the game, as they might do more damage to your own pieces than to other players.\n\nThe cooldown of events is not shown, and players have to plan when to trigger their events in order to maximize the number of events they can play, as they only receive a new event every 5 seconds.\n\nPlayers have to find the balance between trying to actively harm other player and trying to place pieces on the board to collect points in order to win, and few events serve only one of these goals.\n\nAfter a few games, players will know what event do what and how to place pieces in order to optimize their position, as some event are only harmful to some part of the bo ard. Players will also know how to place pieces quickly and efficiently, as the placing system can be difficult to apprehend. In the end, the fastest players will be rewarded.\n\n### Aesthetics\n\nFellowship, Challenge, Sensation, Submission\n\nThe game being a competitive multiplayer game, it sure forces players to deal with each other, as they fight in order to win. Some events being exclusively destructive and harmful, even to the player triggering it, these actions of pure destruction may change the relationships of players past the time of play. This game is also very challenging. More players mean more events, making the game more and more apocalyptic. Players can play in a very competitive mode, or they can play in a more casual way, just messing around with the events. Advanced players only have the knowledge of what the events do, they are not intrinsically favored by the game.\n\n#### Current aesthetics outcomes\n\nI think the game TICTACTOCALYPSE have sensation and fellowship as main aesthetics outcomes.\n\nSensation because the screen is filled with events, they seem to make no sense because they are from different universe (an alien, an ancient lion statue, a giant man with chopsticks). This feeling of chaos is one of the aesthetics outcomes I want for my game.\n\nGiven that this game is a race to score the most point, players must compete and attack each other with events. I don’t think for now that this outcome is very important as there is no clear distinctions between events to help yourself and events to slow other players, thus players seem to fight and endure events together and not against one another.\n\nAnother aesthetic outcome that has surfaced is challenge, because events target random pieces and often not the pieces we want, so it’s difficult to get pieces aligned without getting disturb by events we might have triggered.\n\n#### Desired aesthetics outcomes\n\nI really want this game to be competitive and to help players battle each other. To do so, events will be categorized depending of the players they target. Bonuses events will be a beneficial event targeted to the player that triggered it, and maluses event will be detrimental to all players except the player that launched it. This way players would have much more control over what playstyle they want to adopt (slow others or try to win).\n\nI will also try to reinforce the discovery part of the game, as I will increase the number of events and tweak their possible arrival in the game, to have players discover them even if they played a lot to the game (even though the discovery will be very limited as adding events to the game is very time consuming and 5 seconds are enough to see the event).\n\nSensation will also be an important part of the game, as I try to make the game visually appealing and the events more and more chaotic. For now, there is no sounds in the game, and I think adding some can change a lot to the way the game is perceived, as a lot of things are lacking animation to tell the player what they are for or why they do what they do.\n"},{"slug":"2019-03-18-Rulesheet","category":"blog","title":"Rulesheet","description":"Rulesheet of the game Tictactocalypse","tags":["Game Design","Gamedev"],"body":"\n### Main Menu\n\nChoose the number of players and the score to win by clicking on the corresponding buttons. To start the game, click on the button entitled “Play”.\n\n### Goal\n\nThe goal is to be the first to have the required number of points to win.\n\nYou earn points by having three or more piece of your color aligned. Multiple line of three pieces can be aligned (a single piece can make multiple points at once).\n\nThe lines of three pieces (horizontal, vertical or diagonal) are counted every three seconds, thus giving players points (one point per piece).\n\nTo place a piece on the board, press your main action key (it varies depending of the player, see the table below). You can change to preview position by pressing the left and right keys. You will first have to choose the row, and then the actual slot.\n\nMultiples preview piece will browse the board and press the main button to select a row on the board. If you press the main button once again, it will select a slot and finally place your piece on the slot. You can’t place your piece on a slot that already has a piece on it.\n\n### Events\n\nYou can press your event buttons (two per players, they vary depending of the player, see the table below) to trigger events and disrupt other players. All events have a specific cooldown. You will have to wait before you can trigger an event with the button you previously pressed.\n\nAs events can’t overlay each other, you may not be able to trigger an event if it has not the space to happen around the board.\n\nSome events may move your pieces around. If the piece falls off the grid, it is no longer part of the game and the slot it was on become empty. If the piece is moved away from its slot and is still on the board, after a short period of time indicated by a cooldown clock on the piece, it will go back to its slot.\n\n### Inputs (all on a single keyboard)\n\n<html>\n <head>\n <style>\n @font-face { font-family: Keyboard; src: url('/assets/projects/Tictactocalypse/fonts/Keyboard.otf') format(\"opentype\"); }\n .keyboard {\n font-family: Keyboard;\n font-size: 4rem;\n text-align: center;\n margin-bottom: -5em;\n padding: 0;\n }\n </style>\n </head>\n <body>\n<table style=\"width:100%\">\n <tr>\n <th style=\"visibility:hidden;\"></th>\n <th style=\"text-align: center\">Main Button</th>\n <th style=\"text-align: center\">Event Button 1</th>\n <th style=\"text-align: center\">Event Button 2</th>\n </tr>\n <tr>\n <td>Player 1</td>\n <td class=\"keyboard\">q</td>\n <td class=\"keyboard\">s</td>\n <td class=\"keyboard\">t</td>\n </tr>\n <tr>\n <td>Player 2</td>\n <td class=\"keyboard\">W</td>\n <td class=\"keyboard\">A</td>\n <td class=\"keyboard\">D</td>\n </tr>\n <tr>\n <td>Player 3<sup id=\"fn1-rf\"><a href=\"#fn1\">1</a></sup> </td>\n <td class=\"keyboard\">5</td>\n <td class=\"keyboard\">4</td>\n <td class=\"keyboard\">6</td>\n </tr>\n <tr>\n <td>Player 4</td>\n <td class=\"keyboard\">I</td>\n <td class=\"keyboard\">J</td>\n <td class=\"keyboard\">L</td>\n </tr>\n</table>\n\n<aside class=\"wb-fnote\" role=\"note\">\n <dl>\n <dd id=\"fn1\">\n <p class=\"fn-rtn\"><a href=\"#fn1-rf\">1.</a> On the numeric keypad</p>\n </dd>\n </dl>\n</aside>\n</body>\n</html>\n"},{"slug":"2019-03-25-FinalReflection","category":"blog","title":"Final Reflection","description":"Final reflection on the game Tictactocalypse","tags":["Game Design","Gamedev"],"body":"\nThis game is fun to play and triggers competitive behaviors among players, but I think it lacks some elements that could have made it better.\n\nAt first, I wanted it to be like the game Tricky Towers: a physic-based game, fun to play with friends with a little bit of competition, random events. But there is something in this game that I have to failed to implement is Tictactocalypse. The fact that all player place pieces on the same board is maybe too much (in Tricky Tower, each player has a individual tower) and bonuses and maluses are maybe too powerful.\n\nAnother thing that I think is missing is the discovery aesthetic to the game. I think I could have made this aesthetic more important by adding more event and changing the scarcity of them (maybe have some really rare event). For now, there only are 4 different events, and in a single game, a new player can see them all.\n"},{"slug":"2020-05-03-Tiger","category":"blog","title":"Tiger","description":"A Tiger compiler written in C++","tags":["EPITA"],"body":"\nThe Tiger Compiler project is a C++ implementation of a Tiger compiler.\n\nThe Tiger language is described by [Andrew Appel](https://www.cs.princeton.edu/~appel/) in his [Modern Compiler Implementation](https://www.cs.princeton.edu/~appel/modern/) books, and constitutes an important project in the [EPITA](/archive?tag=EPITA) curriculum.\n\n<div class=\"figure\">\n <figure style=\"max-width: 30%\">\n <img src=\"/assets/posts/EPITA/tiger.png\" >\n <figcaption>A badly drawn tiger</figcaption>\n </figure>\n</div>\n\nThis was group project (4 members) carried during several months.\n"},{"slug":"2021-01-31-France-IOI","category":"blog","title":"France-IOI internship","description":"My internship at France-IOI as a first time web developer intern","tags":["Work","EPITA"],"body":"\n## France-IOI\n\n[France-IOI](http://www.france-ioi.org/) is a non-profit association founded in June 2004 with the aim of developing the selection and training of the French team for the International Olympiad in Informatics (IOI). Its board of directors is composed of four founding members (Mathias Hiron, Arthur Charguéraud, Fabrice Bardèche and Joël Courtois) and 7 elected members (Guillaume Le Blanc, Ismael Belghiti, Jacques-Henri Jourdan, Loïc Février, Amaury Pouly, Benjamin Butin and Louis Jachiet).\n\nThe main objectives of the [France-IOI](http://www.france-ioi.org/) are:\nTrain, select and support the French delegation to the IOI.\nCreate and distribute educational content for programming and algorithmic.\nOrganize national and international competitions in programming, algorithmic or computer science discovery.\n\nThe main goal of [France-IOI](http://www.france-ioi.org/) is to make programming and algorithmic accessible for all: free tools and content distributed by the association allow rapid progress, and the [Algorea](http://dev.algorea.org/) platform is the hub of many exercises and training tasks.\n\n## My internship\n\nThe internship focused on the integration between the Front-End and the Back-End of the learning platform, regarding its UI: browsing of the content, management of student groups and progress monitoring, rights management, etc.\nSince the platform is developed by a handful of developers, my main goal as an intern was to increase development speed of the Algorea platform. I worked with the lead developer of the platform ([Damien Leroy](https://github.com/smadbe)) and another intern (also from EPITA).\n\nBecause I had no experience in Angular, Typescript and Rxjs, the internship supervisor tasked me to do a very versatile component, that was a very good introduction to key concepts of the technologies I was going to use for the whole internship. I also had to learn the agile method of doing things: Pull requests must be small enough to be reviewed easily, and the code added to the project must be clean, tested and reviewed by the lead developer.\n\nA designer has already designed most of the components of the platform (so the HTML and CSS), the first part of my work was to adapt these components to make them functional. I had to rewrite some of the components from scratch, given the numerous changes. The main pages of the Algorea website were already built, but it wasn’t ready for production yet. I was tasked to work on the most essential components so that the platform could be used the sooner the better.\n\n\n\nMy work was centered around a daily meeting we had (the lead dev, the other intern and I) at 9am, where we would talk about what we did the day prior and what we planned to do the following day. My objective was to create, update, or close at least one pull request a day (you can find a Gantt chart of my pull requests in the figure 1)\n\n### Works done\n\nAt the beginning, each pull request had to be reviewed multiple times, because I had to change a lot of things (correct errors, or refactoring parts of the code), so the progress was pretty slow. As my skill kept growing, the features I had to implement were more and more complex, that’s why the rhythm of my PRs didn’t accelerate.\n\n\n\n### The Stack\n\nDuring this internship, I learned a few new technologies:\n\n- Angular\n- TypeScript\n- Rxjs\n- Sass\n- Karma\n\nI also developed my skills in the following:\n\n- git and Github\n- Javascript\n- the Agile Methodology\n\n## Final thoughts\n\nTo conclude, I would say this internship was a great experience, especially as a first professional experience in web development. I was reluctant at first to work in the web industry as I thought that it was boring and not technical enough, but I faced problems that changed my point of view. The only downside I can think of is the fact that I had to work remotely as France-IOI is internationally based so they don’t have a dedicated workspace.\n"},{"slug":"2021-03-29-Torch-rnn","category":"blog","title":"Torch-rnn","description":"A shell script to make Torch-rnn easier to use","tags":["AI"],"body":"\nNot long ago I downloaded all the Messenger discussions I had with a friend. There were around 10,000 messages and I tough it would be fun to train an AI to learn these discussions, our way to write messages, and come up with discussions on its own.\n\nTo do that I use the [Torch-rnn](https://github.com/jcjohnson/torch-rnn) which is model of recurrent neural network designed to learn text. It's base on [Char-rnn](https://github.com/karpathy/char-rnn)\n\nI developed a shell script to improve my workflow and make this wonderful tool more accessible.\n\n<a class=\"button button--primary button--rounded button--lg\" href=\"https://github.com/GabrielVidal1/torch-rnn-quickstart\"><i class=\"fab fa-github\"></i> See repo on github</a>\n\n{%- include README.md -%}\n"},{"slug":"2021-05-30-Journal","category":"blog","title":"Journal app idea","description":"An app that make a monthly journal from the things you did each week and wrote articles from data you produced during this time like discussions, calls, photos, position and time.","tags":["Idea"],"body":"\nAn app that make a monthly journal from the things you did each week and wrote articles from data you produced during this time like discussions, calls, photos, position and time.\n\nIt's like a personal diary but with automatic data gathering to help you remember, summarize and write about the bests bits of your life.\n\nThe goal of this app is to help people create memories and keep them in real paper form.\n\n## Data gathering\n\n### Automatic imports from the phone\n\nA lot of data can be imported from others app that already collect data about the user. This data includes :\n\n- Photos, their position and datetime\n- Position in Google Maps\n- Events in Calendar\n- Phone calls\n- And some information about what you did on your phone:\n - Discussions from messaging apps like Messenger, Whatsapp, Instagram...\n - Spotify to see music you listened to\n - Posts you liked\n - Search history\n\n### Manual\n\nSome information still need to ba gathered manually :\n\n- Persons you saw today\n- Things that happen in real life, like topics of discussions, drama, etc...\n- the mood\n- things you ate each day\n- your feelings about things\n\nThe user will be able to easily enter this information through the note module\n\n## The wiki\n\nWith all this information, the user builds some sort of Wiki of his life. From the places you've been, to the people you've seen, all will be link in an easy to navigate Wikipedia like site.\n\nThe user can also edit things manually to add details, information and to link elements together.\n\nSome pages will we automatically generated where the user can browse data about the information link to this.\n\n- Places\n- Peoples\n- Important dates\n\n## The posts\n\nThe user can writes posts about anything, a person, an event, his life, a place...\nTo do this, there will be a text editor in the app, where you can write text in a Markdown kind of way to add formatting, images, tables, links...\n\nThese posts can also be exported in albums, that the user can order through the app to have a real book of his life.\n\n## Development\n\nThe stack :\n\n- backend on [Firebase](https://firebase.google.com/)\n- Front made with [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/)\n\n### Automatic data gathering\n\nThe data can be imported from the multiple other application that provides access (or APIs) :\n\n- Photos from the gallery (or Snapchat, Google photos, etc...)\n- Calls and messages from the phone (or other app most used by the user like Messenger, Whatsapp, etc...)\n- Position with the location tracking features of Google Maps\n- History of their browser (Chrome, Brave, etc...)\n- History on Youtube\n- Liked posts from different apps\n- Musics from Spotify history\n- Events from calendar app, or Google Agenda\n"},{"slug":"2021-07-23-TravelMate","category":"blog","title":"Travel Mates","description":"Prototype of an application offering guided tours to tourists.","tags":["EPITA"],"body":"\nApplication offering guided tours to tourists. Users may answer a short questionnaire (or search parameters) asking for their budget, the type of activity they are interested in (sports, cultural, relaxation), and the duration of their stay.\n\nProject made during my computer science engineering degree at EPITA, in the context of a Design Thinking course.\n\n# Figma Prototype\n\n<img src=\"/assets/posts/EPITA/travel-mates-figma.png\" width=\"100%\"/>\n\n## Features\n\n- Display prices in a detailed manner (total plus each activity).\n- Maintain a history of user travels.\n- Option to bookmark favorite tours.\n- Save user preferences (activity type, dining, accommodation).\n- Personalization of tours (click on an activity to have the site suggest others while keeping the rest of the itinerary). Even customize as the journey progresses.\n- Display the ecological impact of each activity (prioritising activities that pollute less).\n- Highlight local activities and businesses.\n\n## Application Organization\n\n- **Login Page** (possibility to search without logging in).\n- **Homepage**: Trip search with various parameters that cannot be asked during initial setup.\n\nFor the search, users could respond to a questionnaire by selecting options or images, with the questionnaire being personalized based on the destination.\n\n- **Results Page**: Feed with blocks containing key information.\n"},{"slug":"2021-10-05-Digital-Resume","category":"blog","title":"Digital Resume","description":"Updated Resume website made with NextJS + Tailwind hosted on Vercel","tags":["Work"],"body":"\n<a href=\"https://resume.gabriel.vidal--ayrinhac.xyz/\" target=\"_blank\" >\n<img src=\"/assets/posts/resume/resume_preview.png\" alt=\"Resume preview\" class=\"w-full rounded-lg shadow-lg\">\n</a>\n\n## Original post from 2021-10-05\n\nI wanted to remake my resume and why not test a new web front framework.\n\nI made this website with [NuxtJS](https://nuxtjs.org/).\n\nYou can check it out [here](https://resume.gabriel.vidal--ayrinhac.xyz/) or by clicking the **'Resume'** button in the navbar :)\n\n# New Resume\n\nWhen I tried to update this project, I was quickly bored by the debugging I had to do when trying to upgrade the dependencies. So I decided to remake it with NextJS, so I can use React and Typescript, which I'm very familiar with.\n\nI also decided to use [Vercel](https://vercel.com/) to host it. It's a really great platform that allows you to host your website for free and with a really simple setup. No need to setup Github Actions or anything, just push your code and it's live. The domain buying and setup was surprisingly easy compared to Gandi that I used for previous projects.\n\nI added more feature, for example now you can filter projects by category and tags. I also added a dark mode that gets the job done (not very proud of it lol).\n"},{"slug":"2022-08-15-Matters-internship","category":"blog","title":"fullstack web developer internship @ Matters","description":"My end-of-studies internship at Matters, a startup studio working for clients to make the world a better place, as a fullstack & devops web developer.","tags":["Work","EPITA"],"body":"\n<img src=\"/assets/posts/Matters/banner.png\" width=\"100%\"/>\n\nAs a part of my computer science engineer degree at Epita, I had to integrate a company to prove I’m worthy of becoming an engineer. When I started my major SIGL, I didn’t know at all what I wanted to do after my degree. I had quite some experiences as a freelance web developer for one year, but I wasn’t sure that I would want to code my whole life.\n\nAs my SIGL curriculum started to come to an end, I had to search for an end of studies internship. First I searched in the fields that matter to me: mobility, healthcare, education and ecology but didn’t find the right fit. I received a lot of offers, from a lot of different companies, but none of them seem to work in a field that matters to me or with enough technical challenge. That is until I had my first interview with Matters. They had everything none of the others had: a startup studio working for clients to make the world a better place, with a good amount of technical challenge, sign me in!\n\n## Matters\n\nSo I joined [Matters](https://matters.tech/) for 6 months beginning in February 2022. We were 5 interns joining at the same time, and started by an onboarding project that consisted of coding a clone of the social network Twitter. This project was using the same technical stack as the real projects, so we could become familiar with it, and all the processes and good practices. After this starter project, I was ready to join a team and work on a real one.\n\nIn the next part, I’m going to present the two main skills I used the most, and I find them to be the most important in the business world: **rigor and adaptability**. They might seem incompatible, but trust me they can be combined to make yourself a true engineer.\n\n### Cali\n\nAfter the onboarding project, I worked on my first real project : [Cali](https://www.cali-rse.com/) (Corporate Awareness to Limit Impact). It’s a web application to help companies define, measure and improve their corporate social responsibility, through indicators that measure specific metrics, like the amount of fuel or electricity consumed over time. The website proposes actions that act on indicators through quizzes or tips. The business core was new to me, and the frameworks were too, and I was still learning Matters’ processes. To be able to keep up with the work assigned to me, I had to learn at a very fast pace and apply methods with great rigor. As I became more and more used to the different parts of the project, I was assigned bigger and more complex tasks. First I was struggling, but I adapted the methods taught to improve my workflow and overcome the workload. At the end of my time on **Cali**, I had developed a well-defined process to work efficiently on multiple features at the same time, thus increasing my speed, autonomy and overall quality of work.\n\n### Skeleton\n\nI worked a week on the skeleton, the template project which most projects start from. It contains everything you need: authentification, database, frontend and backend. I had already backported improvements I did on Cali to the skeleton, but now I was working on the migration of Yarn to its version 3. Then I joined the [Kryptview](https://kryptview.com/) team, to work on the eponymous project.\n\n### Kryptview\n\n**Kryptview** is a Research-to-earn platform where users can submit fundamental research they make on crypto tokens, others can review them and all can earn KVTs, which is the crypto token put in place by the project to reward users for their participation. The website had a private Alpha just before I joined, so there was plenty of feedback for new and existing features to address.\n\nEven though Cali and Kryptview use the same technical stack, the business core is totally different. I used all the base technical knowledge I gathered on Cali to understand the business and its stakes. During the first few days, I was able to adapt to the new codebase, and implement features just as fast as I did on Cali. I continued to improve my general workflow, adapting it to my new team, and I would say it worked!\n\n### Access V2\n\nI had the opportunity to work on the skeleton and Kryptview because the initial project I was staffed for wasn’t ready yet. 4 weeks later, it was. This last project is Access V2, for the legal consulting firm Beau de Loménie, which specializes in intellectual property. Access V2 is a remake of Access V1, an old project developed by Matters and operational since 2014. Some breaking changes made to the backend application, external to Matters, will make it obsolete in December of this year, so Access V2 needs to be ready by then.\n\nThe core business is (again) new to me, but this time the project just started so no codebase to learn from. It was really challenging at first, because the core business implied a lot of constraints, mainly security practices and a lot legacy technologies: the Oracle database is incompatible with the usual stack at Matters, protected behind a VPN, and no documentation was provided to us from the Access V1 team, with only one developer still at Matters.\n\nWe had to put processes in place to increase visibility, mainly for the client, but also to identify as soon as possible the risks that could make a feature take forever to implement. Rigor again became a key capability to keep the project afloat. I still have a month to work on this project, and since I accepted the offer to stay, I will continue on it in September.\n\n## Conclusion\n\nAs mentioned in the introduction, the ability to follow strict rules and to adapt them when working in a new environment is the key to success for me. That is why I developed a workflow that can be adapted to any feature of any size, with checkpoints to improve visibility of the work being done and its status, while maximizing autonomy via asynchronous communication with my team. Matters strengthened my belief that web development is currently the best way to make any idea possible and get the most impact from it.\n\nIn the end, I would say that I was really lucky to work on such different projects, from corporate awareness, to intellectual property, passing by crypto tokens, it was a wild ride! Who knows what Matters got for me in the future…\n"},{"slug":"2023-06-15-MusicGen-discord-bot","category":"blog","title":"MusicGen Discord Bot","description":"A music bot for discord that generates music with AI using the MusicGen model hosted on Banana.dev","tags":["AI"],"body":"\n<a href=\"https://resume.gabriel.vidal--ayrinhac.xyz/\" target=\"_blank\" >\n<img src=\"/assets/posts/MusicGenDiscord/preview.png\" alt=\"\" class=\"w-full rounded-lg shadow-lg\">\n</a>\n\nI was really impressed by the samples show on the [MusicGen](https://ai.honu.io/papers/musicgen/) website and I wanted to make a discord bot that uses this model to generate music. Besides, I recently discovered [Banana.dev](https://www.banana.dev/), a platform that allows you to host your models and use them with a simple API call. So I decided to use it to host the MusicGen model.\n\nI previously tried to use vast.ai to host the model but the setup was too complicated and barely worked. With Banana.dev, it was really easy to host the model and use it in my bot.\n\nHere is basically how the music generation works:\n\n```python\ndef gen(model: MusicGen, prompt: str, samples=1, duration=8):\n model.set_generation_params(duration=duration)\n wav = model.generate([prompt] * samples) # generates samples.\n\n results = []\n with tempfile.TemporaryDirectory() as tmpdirname:\n for idx, one_wav in enumerate(wav):\n # Will save under {idx}.wav, with loudness normalization at -14 db LUFS.\n path = audio_write(\n os.path.join(tmpdirname, f\"{idx}.mp3\"),\n one_wav.cpu(),\n model.sample_rate,\n format=\"mp3\",\n )\n print(f\"Saving {path} with prompt: {prompt}\")\n\n # read the file and convert it to base64 string\n with open(path, \"rb\") as audio_file:\n encoded_string = base64.b64encode(audio_file.read())\n results.append(\n {\n \"prompt\": prompt,\n \"audio\": encoded_string.decode(\"utf-8\"),\n }\n )\n return results\n```\n"},{"slug":"2026-08-06-Backrooms","category":"blog","title":"backrooms.game: furnishing an endless maze with AI assets and Model Synthesis","description":"How my first-person Backrooms explorer got its content — every texture, poster and piece of furniture is AI-generated (Nano Banana images, TRELLIS.2 3D models from my homelab's 3d-gen service), and every map beyond the first is grown in the browser with Paul Merrell's Model Synthesis.","tags":["AI","Gamedev","Homelab","3D"],"body":"\n<aside class=\"not-prose my-8 flex items-start gap-4 rounded-lg border border-theme-primary/30 bg-theme-primary/5 p-5 dark:border-theme-dark-primary/40 dark:bg-theme-dark-primary/10\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" class=\"mt-0.5 h-7 w-7 shrink-0 text-theme-primary dark:text-theme-dark-primary\">\n <path d=\"M12 2v4\"/>\n <rect x=\"3\" y=\"6\" width=\"18\" height=\"14\" rx=\"3\"/>\n <circle cx=\"8.5\" cy=\"12\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\"/>\n <circle cx=\"15.5\" cy=\"12\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\"/>\n <path d=\"M9 16.5h6\"/>\n <path d=\"M1.5 11v4M22.5 11v4\"/>\n </svg>\n <div class=\"text-sm leading-relaxed\">\n <p class=\"font-bold text-theme-primary dark:text-theme-dark-primary\">AI-generated article</p>\n <p class=\"mt-1 text-gray-700 dark:text-gray-300\">\n This post was written by <strong>Claude Fable 5</strong>, working from my own\n projects, memories and past conversations. The screenshots were captured from\n the live game while writing; the assets and numbers come from my repositories\n and my hardware.\n </p>\n </div>\n</aside>\n\n[**backrooms.game.gabvdl.xyz**](https://backrooms.game.gabvdl.xyz) is a\nfirst-person Backrooms explorer: yellowed wallpaper, damp carpet, humming\nfluorescent tubes, and something that hunts you by sound. It runs in the\nbrowser (Vite + React + Three.js), on desktop and phone, and it was built\nalmost entirely by coding agents steered from\n[my agent cockpit](/blog/2026-08-06-ai-agent).\n\nA solo browser game has a content problem: a maze needs furniture, wall art,\ntextures, a monster, and — if it wants to stay interesting — more than one\nmaze. This post is about the two systems that solved it: **an all-AI asset\npipeline** (Nano Banana for images, my homelab's 3d-gen service for 3D\nmodels), and **procedural maps grown in the browser with Model Synthesis**,\nthe algorithm by Paul Merrell that Wave Function Collapse descends from.\n\n<img src=\"/assets/posts/Backrooms/cover.jpeg\" alt=\"In-game view of a generated map: yellowed rooms, green-lit corridors, a pedestal table and a nightstand scattered on the carpet\" class=\"w-full rounded-lg shadow-lg\">\n\n## Every asset is generated\n\nThere isn't a single hand-drawn or store-bought asset in the game. Two\ngenerators cover everything.\n\n### Nano Banana for every image\n\nThe three surface textures — wallpaper, carpet, acoustic ceiling tile — are\n[Nano Banana](https://blog.google/products/gemini/updated-image-editing-model/)\n(Gemini image) generations. They are *not* tileable, and no amount of prompt\nbegging made them so; the fix was cheaper than the fight: mirrored-repeat\nwrapping, which hides the seams by construction.\n\n<div class=\"not-prose my-8 grid grid-cols-3 gap-4\">\n <img src=\"/assets/posts/Backrooms/tex-wall.webp\" alt=\"Yellowed damask wallpaper texture\" class=\"w-full rounded-lg shadow-lg\">\n <img src=\"/assets/posts/Backrooms/tex-floor.webp\" alt=\"Mustard carpet texture\" class=\"w-full rounded-lg shadow-lg\">\n <img src=\"/assets/posts/Backrooms/tex-ceiling.webp\" alt=\"Stained acoustic ceiling tile texture\" class=\"w-full rounded-lg shadow-lg\">\n</div>\n\nThe wall art is where image generation earns its keep. The rooms are decorated\nwith **vintage furniture-store advertisements** — a fake mid-century brand per\nposter, with period typography and pricing — pasted on random room-facing wall\nfaces. Lore items work the same way: polaroids of these very rooms, mundane\nthrough nightmare, dropped on the floor for you to find.\n\n<div class=\"not-prose my-8 grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <img src=\"/assets/posts/Backrooms/poster-1.webp\" alt=\"Haversham & Sons Fine Furnishings — vintage oak-table advertisement poster\" class=\"w-full rounded-lg shadow-lg\">\n <img src=\"/assets/posts/Backrooms/poster-2.webp\" alt=\"Vintage furniture advertisement poster\" class=\"w-full rounded-lg shadow-lg\">\n <img src=\"/assets/posts/Backrooms/poster-3.webp\" alt=\"Vintage furniture advertisement poster\" class=\"w-full rounded-lg shadow-lg\">\n <img src=\"/assets/posts/Backrooms/poster-4.webp\" alt=\"Vintage furniture advertisement poster\" class=\"w-full rounded-lg shadow-lg\">\n</div>\n\nEach poster is generated as an illustration-on-paper, then background-removed\ninto a WebP cutout with alpha (the torn-paper edge is part of the alpha\nchannel) so it can be pasted straight onto a wall quad with an `alphaTest`\nmaterial. The background removal runs as a job on my\n[EVOX2 AI box](/blog/2026-08-06-trellis2-strix-halo)'s job queue — the\nsegmentation model alone was unreliable on illustration-on-paper images, so\nthe pipeline unions its mask with a corner flood-fill and fills interior\nholes.\n\n### The 3d-gen lab for every model\n\nAll the furniture — armchairs, sofas, coffee tables, nightstands, a coat rack\n— comes from **[3d-gen](/blog/2026-08-06-trellis2-strix-halo)**, the\nimage → 3D service on my homelab: upload one image, and TRELLIS.2 running on a\nStrix Halo mini-PC (Radeon iGPU, ROCm) returns a fully textured `.glb`,\nin about a minute on the draft tier.\n\n<div class=\"not-prose my-8 grid grid-cols-2 gap-4\">\n <img src=\"/assets/posts/Backrooms/armchair-source.webp\" alt=\"Source image: an ornate floral armchair\" class=\"w-full rounded-lg bg-white shadow-lg\">\n <img src=\"/assets/posts/Backrooms/armchair-model.webp\" alt=\"Resulting textured 3D model of the armchair\" class=\"w-full rounded-lg shadow-lg\">\n <img src=\"/assets/posts/Backrooms/sofa-source.webp\" alt=\"Source image: an olive velvet sofa\" class=\"w-full rounded-lg bg-white shadow-lg\">\n <img src=\"/assets/posts/Backrooms/sofa-model.webp\" alt=\"Resulting textured 3D model of the sofa\" class=\"w-full rounded-lg shadow-lg\">\n <img src=\"/assets/posts/Backrooms/table-source.webp\" alt=\"Source image: a vintage oval coffee table\" class=\"w-full rounded-lg bg-white shadow-lg\">\n <img src=\"/assets/posts/Backrooms/table-model.webp\" alt=\"Resulting textured 3D model of the coffee table\" class=\"w-full rounded-lg shadow-lg\">\n</div>\n\n<p class=\"not-prose -mt-4 mb-8 text-sm text-gray-500 dark:text-gray-400\">Left: the single source image each model starts from. Right: the textured glb TRELLIS.2 returns.</p>\n\nA raw TRELLIS.2 glb is not a game asset, so each model goes through a fixed\nweb-optimization pass before it ships: normalize to a unit cube, strip normal\nand PBR maps (the game is entirely unlit — more on that below), re-encode the\nbase color to a 1024px WebP, quantize and meshopt-compress. Each piece of\nfurniture lands around **250 KB**.\n\nPlacement is a generic decoration pass, not hand-layout: a deterministic RNG\nseeded from the map scatters furniture in room cells only, sinks each piece a\nfew centimetres into the carpet (which conveniently hides the one thing\nimage-to-3D can't know: the unmodeled underside), registers a collision box\ncomputed from the glb's own geometry bounds, and — because the game renders\neverything with baked vertex-color lighting and zero realtime lights — tints\neach prop with the light field sampled at its cell. The same pass runs\nunchanged on every generated map.\n\nThe monster went through the same pipeline. The Hum-Eater's mesh is a\nTRELLIS.2 generation whose tattered, see-through shreds turned out to be an\nasset rather than a defect — rendered double-sided, it reads as something\nthat's been down there too long. It has **no animation data at all**: walking,\nreaching, grabbing and the stunned thrash are all procedural bone rotations\ncomputed each frame on a reset bind pose. Skins are just other generations —\none prompt later, Captain Clark joined the roster as a selectable monster.\n\n<img src=\"/assets/posts/Backrooms/captain-clark.webp\" alt=\"Captain Clark, a pirate-captain 3D model in T-pose, one of the selectable monster skins\" class=\"mx-auto w-full max-w-md rounded-lg shadow-lg\">\n\n## Infinite maps from Model Synthesis\n\nThe shipped level is a **60×40 pixel image**. Each pixel is one 2 m cell,\nclassified by palette color: walls, three floor types with different ceiling\nheights, sunken floors, pits, doors, half-walls, lamps. That one image is the\nwhole level format — and it turns out to be the perfect input for\n[**Model Synthesis**](https://paulmerrell.org/model-synthesis/), Paul\nMerrell's 2007 algorithm (the direct ancestor of Wave Function Collapse).\n\nThe idea: slide an N×N window over the example map and collect every pattern\nthat occurs. Then grow a new grid, cell by cell, under one constraint —\n**every N×N window of the output must be a pattern that exists in the\nexample**. Local structure (rooms have doors, corridors connect, lamps hang in\nopen space) carries over automatically, because any window that would violate\nit never occurred in the example.\n\nBrowse the results at\n[**backrooms.game.gabvdl.xyz/maps**](https://backrooms.game.gabvdl.xyz/maps) —\nevery map on that page is generated live in your browser when you open it:\n\n<a href=\"https://backrooms.game.gabvdl.xyz/maps\"><img src=\"/assets/posts/Backrooms/maps-gallery.jpeg\" alt=\"The /maps gallery: the hand-made example map, its palette legend, and three freshly generated N=3 maps with their generation stats\" class=\"w-full rounded-lg shadow-lg\"></a>\n\nA generated map is fully described by **`?gen=N.seed.WxH`** in the URL — the\ngame regenerates it deterministically, so a shareable link *is* the map and no\nPNGs are ever shipped or stored. The gallery's stats are honest generation\nnumbers from your machine: the example yields ~660 patterns, a standard 64×48\nmap takes between half a second and a few seconds depending on N, a big 96×72\naround ten.\n\nGetting it to run comfortably in the browser took three fixes worth writing\ndown:\n\n- **AC-4 propagation.** The naive constraint propagation (union the allowed\n neighbours of every remaining pattern as bitsets) was catastrophically slow —\n minutes-long hangs. Switching to AC-4 support counting (each cell keeps, per\n pattern per direction, a count of supporting neighbours; a pattern dies when\n any count hits zero) made generation interactive.\n- **Weight dampening.** Sampling patterns by raw example frequency works for\n N=3, but at N≥4 the big blank-room patterns dominate and the output\n degenerates into featureless halls. Raising weights to the power 0.4\n flattens the distribution enough to keep the interesting furniture-scale\n structure.\n- **Door punching.** A constraint-satisfying map can still come out as two\n disconnected components. Instead of retrying until connected, the generator\n finds the components and punches doors through shared walls — every map is\n guaranteed walkable on the first try.\n\nThe gallery locks N to 3 and 4 because both failure modes are instructive:\nN=2 windows carry so little context the output is noise, and N=5 is both slow\nand degenerate — almost every window becomes rare enough that the example\neffectively gets copied.\n\nMy favorite part is that the example map is **user-editable**: `/maps` lets\nyou download the current example as a palette-snapped PNG, edit it pixel by\npixel in any image editor, and upload it back. It's stored in your browser's\nlocalStorage, and from then on the gallery, `?gen=` links and direct play all\ngrow from *your* map — the algorithm doesn't care that the example changed,\nit just learns different patterns.\n\n## Play it\n\nThe game is at\n[**backrooms.game.gabvdl.xyz**](https://backrooms.game.gabvdl.xyz) — works on\ndesktop (WASD + mouse) and mobile (virtual joystick). The maps gallery is at\n[/maps](https://backrooms.game.gabvdl.xyz/maps), and the source is on my\nself-hosted Gitea. For the story of the box the 3D assets are generated on, see\n[the TRELLIS.2 on Strix Halo post](/blog/2026-08-06-trellis2-strix-halo).\nMind the hum.\n"},{"slug":"2026-08-06-Trellis2-Strix-Halo","category":"blog","title":"Image → 3D in under two minutes, locally, on an AMD Strix Halo box","description":"Getting Microsoft's TRELLIS.2 to run on a Ryzen AI MAX+ 395 iGPU (gfx1151/ROCm), cutting a 22-minute generation down to ~40 seconds, and wiring it into a real queue-backed workflow — vision LLM, background removal, generation, auto-rigging and publishing.","tags":["AI","Homelab","3D"],"body":"\n<aside class=\"not-prose my-8 flex items-start gap-4 rounded-lg border border-theme-primary/30 bg-theme-primary/5 p-5 dark:border-theme-dark-primary/40 dark:bg-theme-dark-primary/10\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" class=\"mt-0.5 h-7 w-7 shrink-0 text-theme-primary dark:text-theme-dark-primary\">\n <path d=\"M12 2v4\"/>\n <rect x=\"3\" y=\"6\" width=\"18\" height=\"14\" rx=\"3\"/>\n <circle cx=\"8.5\" cy=\"12\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\"/>\n <circle cx=\"15.5\" cy=\"12\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\"/>\n <path d=\"M9 16.5h6\"/>\n <path d=\"M1.5 11v4M22.5 11v4\"/>\n </svg>\n <div class=\"text-sm leading-relaxed\">\n <p class=\"font-bold text-theme-primary dark:text-theme-dark-primary\">AI-generated article</p>\n <p class=\"mt-1 text-gray-700 dark:text-gray-300\">\n This post was written by <strong>Claude Opus 5</strong>, working from my own\n projects, tests, benchmarks, memories and past conversations. The\n measurements, code and screenshots come from my hardware and my repositories —\n the writing is the model's.\n </p>\n </div>\n</aside>\n\nI have a mini-PC in a cupboard whose only job is to run models. It's an AMD\n**Ryzen AI MAX+ 395 \"Strix Halo\"** — 16 Zen 5 cores, a Radeon 8060S integrated\nGPU, and 128 GB of unified memory of which 64 GB is handed to the GPU. On paper\nthat's a lot of VRAM for the money. In practice it's `gfx1151`, an architecture\nthat most of the ML ecosystem has never heard of.\n\nThis post is about what it took to get **TRELLIS.2** — Microsoft's 4B\nimage-to-3D model — running on that box, then making it fast enough to be\n*useful*: drop a picture into a web page, get a textured `.glb` back in under\ntwo minutes, entirely on hardware I own.\n\n<img src=\"/assets/posts/Trellis2StrixHalo/3d-gen-lightbox.jpeg\" alt=\"The 3d-gen web UI showing a generated pirate captain model with its parameters and generation time\" class=\"w-full rounded-lg shadow-lg\">\n\nThree parts:\n\n1. **Making it run at all** on ROCm/gfx1151 — two segfaults and one undocumented\n environment variable.\n2. **Making it fast** — where the time actually goes, and why the answer was not\n where I expected.\n3. **Making it a workflow** — a vision LLM for metadata, background removal,\n generation, auto-rigging, and a job scheduler that keeps a single-GPU box from\n killing itself.\n\n## The hardware, precisely\n\n| | |\n|---|---|\n| CPU/APU | AMD Ryzen AI MAX+ 395 (Strix Halo) |\n| GPU | Radeon 8060S iGPU, **`gfx1151`** |\n| Memory | 128 GB unified — 64 GiB assigned to the GPU, 62 GiB to the host |\n| OS | Fedora 43, kernel 6.19.10 |\n| Stack | ROCm 7.2, `torch 2.11.0+rocm7.2` |\n\nThe important word is **unified**. There is one memory pool and one GPU. Two GPU\nworkloads running at the same time don't just contend — they've hard-hung the\nwhole machine for me more than once. That constraint shapes everything in part 3.\n\n---\n\n# 1. Getting TRELLIS.2 to run on gfx1151\n\nTRELLIS.2 takes an image, runs a cascade of flow-matching diffusion stages over a\n**sparse voxel** latent (sparse structure → shape SLat → texture SLat), decodes a\nmesh, and bakes a PBR texture atlas onto it. Every one of those stages leans on a\ncustom CUDA-flavoured kernel, which is exactly the kind of code that has never\nbeen compiled for an obscure RDNA 3.5 iGPU.\n\nI hit two deterministic segfaults. Both took a while, and neither was where the\nstack trace pointed.\n\n## Segfault 1 — attention silently falling back to `flash_attn`\n\n`flash_attn` cannot be built for gfx1151. Fine — PyTorch's own\n`scaled_dot_product_attention` works on ROCm, so I set `SPARSE_ATTN_BACKEND=sdpa`\nand expected to move on. It kept crashing at `Sampling shape SLat`.\n\nThe reason is a one-line allow-list in the model's own sparse config, which\nvalidates the env var against a hard-coded set. `sdpa` isn't in it, so my setting\nwas **silently discarded** and the backend stayed on `flash_attn`:\n\n```diff\n--- a/trellis2/modules/sparse/config.py\n+++ b/trellis2/modules/sparse/config.py\n@@ -21,7 +21,7 @@ def __from_env():\n- if env_sparse_attn_backend is not None and env_sparse_attn_backend in ['xformers', 'flash_attn', 'flash_attn_3']:\n+ if env_sparse_attn_backend is not None and env_sparse_attn_backend in ['xformers', 'flash_attn', 'flash_attn_3', 'sdpa']:\n ATTN = env_sparse_attn_backend\n```\n\nWidening the allow-list isn't enough on its own — there also has to *be* an sdpa\nbranch. The sparse attention path works on **variable-length** sequences packed\nback-to-back (one sequence per voxel set), while `sdpa` wants a padded\n`[N, H, L, C]` batch, so the branch pads, masks, runs, and unpads:\n\n```python\nelif config.ATTN == 'sdpa':\n from torch.nn.functional import scaled_dot_product_attention as sdpa\n N, max_q, max_kv = len(q_seqlen), max(q_seqlen), max(kv_seqlen)\n H, CI, CO = q.shape[-2], q.shape[-1], v.shape[-1]\n\n q_padded = q.new_zeros(N, max_q, H, CI)\n k_padded = k.new_zeros(N, max_kv, H, CI)\n v_padded = v.new_zeros(N, max_kv, H, CO)\n attn_mask = q.new_full((N, max_q, max_kv), float('-inf'))\n\n q_offset = kv_offset = 0\n for i in range(N): # unpack the varlen layout\n ql, kvl = q_seqlen[i], kv_seqlen[i]\n q_padded[i, :ql] = q[q_offset:q_offset + ql]\n k_padded[i, :kvl] = k[kv_offset:kv_offset + kvl]\n v_padded[i, :kvl] = v[kv_offset:kv_offset + kvl]\n attn_mask[i, :ql, :kvl] = 0.0 # everything else stays -inf\n q_offset += ql; kv_offset += kvl\n\n out_padded = sdpa(q_padded.permute(0, 2, 1, 3), # [N, H, L, C]\n k_padded.permute(0, 2, 1, 3),\n v_padded.permute(0, 2, 1, 3),\n attn_mask=attn_mask.unsqueeze(1).expand(-1, H, -1, -1))\n out_padded = out_padded.permute(0, 2, 1, 3)\n out = torch.cat([out_padded[i, :q_seqlen[i]] for i in range(N)], dim=0)\n```\n\n**Lesson:** when a backend env var seems to be ignored, check whether the project\nvalidates it against a whitelist. A silently-ignored config is much harder to\ndebug than a rejected one.\n\n## Segfault 2 — nvdiffrast, and two architectures that can't coexist\n\nWith diffusion running, the pipeline died at the very last step —\n`Sampling attributes`, i.e. the texture bake. That step rasterizes the UV atlas\nwith **nvdiffrast**, which has no official ROCm build. There is a good HIP port\n([`ATLAS-0321/nvdiffrast-rocm`](https://github.com/ATLAS-0321/nvdiffrast-rocm)),\nbut it exposed a genuinely nasty property of this box:\n\n- Built for **`gfx1151`** (the real architecture), nvdiffrast's fine-raster kernel\n mis-compiles and faults with a clean `hipError 218`.\n- Built for **`gfx1100`** (RDNA 3 dGPU) it faults *natively* on gfx1151 — but run\n it under `HSA_OVERRIDE_GFX_VERSION=11.0.0`, and it rasterizes **perfectly**.\n\nSo far so good: masquerade as gfx1100. Except the *same function* also calls the\nsparse extensions — `flex_gemm.grid_sample_3d` and `cumesh`'s UV-unwrap/BVH —\nwhich are compiled for **native gfx1151** and throw `hipError 218` under that\nsame override.\n\nOne global override cannot serve both. The fix is to scope the masquerade to a\nprocess: HIP re-initialises per process, so the parent stays native gfx1151 and\n**only** the nvdiffrast rasterize+interpolate is delegated to a child that has the\noverride set.\n\n```python\n# --- Rasterize via a gfx1100-masquerade SUBPROCESS (ROCm/gfx1151 fix) ---\n# nvdiffrast is built for gfx1100 and faults natively on gfx1151, while the\n# sparse extensions (flex_gemm/cumesh) above need NATIVE gfx1151.\nimport subprocess as _sp, tempfile as _tf, os as _os, sys as _sys\n\n_td = _tf.mkdtemp()\n_inp, _outp = _os.path.join(_td, 'in.pt'), _os.path.join(_td, 'out.pt')\ntorch.save({'uvs': out_uvs.detach().cpu(),\n 'faces': out_faces.detach().cpu().int(),\n 'vertices': out_vertices.detach().cpu()}, _inp)\n\n_env = dict(_os.environ)\n_env['HSA_OVERRIDE_GFX_VERSION'] = '11.0.0' # scoped to the child only\n_env.pop('PYTORCH_HIP_ALLOC_CONF', None)\n\n_r = _sp.run([_sys.executable, '/app/_uv_rasterize_helper.py',\n _inp, _outp, str(texture_size)], env=_env)\nif _r.returncode != 0:\n raise RuntimeError(f\"uv_rasterize_helper failed rc={_r.returncode}\")\n\n_d = torch.load(_outp)\nrast, pos = _d['rast'].cuda(), _d['pos'].cuda()\n```\n\nAnd the child is deliberately tiny — import nvdiffrast, rasterize in chunks,\ninterpolate, write tensors back:\n\n```python\n# _uv_rasterize_helper.py — runs under HSA_OVERRIDE_GFX_VERSION=11.0.0\nimport sys, torch\nimport nvdiffrast.torch as dr\n\ninp, outp, ts = sys.argv[1], sys.argv[2], int(sys.argv[3])\nd = torch.load(inp)\nout_uvs, out_faces, out_vertices = d['uvs'].cuda(), d['faces'].cuda().int(), d['vertices'].cuda()\n\nctx = dr.RasterizeCudaContext()\nuvs_rast = torch.cat([out_uvs * 2 - 1,\n torch.zeros_like(out_uvs[:, :1]),\n torch.ones_like(out_uvs[:, :1])], dim=-1).unsqueeze(0)\n\nrast = torch.zeros((1, ts, ts, 4), device='cuda', dtype=torch.float32)\nfor i in range(0, out_faces.shape[0], 100_000): # chunked, keeps VRAM flat\n rast_chunk, _ = dr.rasterize(ctx, uvs_rast, out_faces[i:i+100_000], resolution=[ts, ts])\n mask_chunk = rast_chunk[..., 3:4] > 0\n rast_chunk[..., 3:4] += i\n rast = torch.where(mask_chunk, rast_chunk, rast)\n\npos = dr.interpolate(out_vertices.unsqueeze(0), rast, out_faces)[0][0]\ntorch.save({'rast': rast.cpu(), 'pos': pos.cpu()}, outp)\n```\n\nThe tensor round-trip through `torch.save`/`load` costs a fraction of a second on\na mesh this size. It is an ugly fix and I would happily replace it with a correct\ngfx1151 nvdiffrast build — but it turned a hard blocker into a working pipeline.\n\n**Lesson:** `HSA_OVERRIDE_GFX_VERSION` is a per-process lie you tell the runtime.\nWhen one library needs the lie and another needs the truth, a subprocess boundary\nis the cheapest place to draw the line.\n\n## The red herring: `HF_HUB_OFFLINE=1`\n\nWorth mentioning because it cost me a full session. Early on the box had flaky\nDNS, `from_pretrained` would hang, and the obvious workaround was\n`HF_HUB_OFFLINE=1` / `TRANSFORMERS_OFFLINE=1`. From then on every run segfaulted\nat the shape→texture decode.\n\nFixing the network and loading the model **online** made the crash disappear.\nI never fully root-caused what the offline path resolved differently, but the\ncorrelation was perfect: every crashing run had the flags, the one good run\ndidn't. If you're chasing a segfault in a model you didn't write, take the\noffline flags out of the equation early.\n\n## The environment variable worth 12×\n\ntorch 2.11+rocm7.2 ships an **AOTriton** flash/mem-efficient SDPA path for\ngfx1151 — and gates it behind an undocumented env var. Without it, `sdpa`\nsilently falls back to the slow math kernel. The tell is a `UserWarning` in every\nrun that literally names the variable, which is easy to scroll past.\n\n```bash\nTORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1\n```\n\nMicrobenchmarked on the box, fp16 SDPA:\n\n| seq len | default (math) | AOTriton flash | speedup |\n|--------:|---------------:|---------------:|--------:|\n| 4096 | 62.2 ms/call | 4.67 ms/call | **13.3×** |\n| 8192 | 248.2 ms/call | 20.4 ms/call | **12.1×** |\n\nAttention dominates the SLat sampling stages, so this alone took a\n`texture_size=2048` run from ~22 minutes to ~9–10 minutes. One caveat: the\ngfx1151 AOTriton path has known bf16 numerical bugs at small shapes\n([ROCm#6034](https://github.com/ROCm/ROCm/issues/6034)). TRELLIS runs fp16 here\nand the output validated clean, but check your geometry and texture after\nenabling it.\n\n## Running it\n\nThe pieces above are baked into one image, so a generation is a plain\n`docker run` with a fistful of ROCm flags:\n\n```bash\ndocker run --rm \\\n --device /dev/kfd --device /dev/dri \\\n --group-add video --group-add render \\\n --security-opt seccomp=unconfined --ipc host \\\n -e ATTN_BACKEND=sdpa -e SPARSE_ATTN_BACKEND=sdpa \\\n -e TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 \\\n -e HSA_XNACK=1 \\\n -e PYTORCH_HIP_ALLOC_CONF=garbage_collection_threshold:0.6,max_split_size_mb:128 \\\n -e HF_HOME=/root/.cache/huggingface -e HF_TOKEN=\"$HF_TOKEN\" \\\n -v trellis_hf-cache:/root/.cache/huggingface \\\n -v trellis_miopen:/root/.cache/miopen \\\n -v trellis_triton:/root/.triton \\\n -w /app trellis2-rocm-gfx1151:textured python -u example.py\n```\n\nTwo of those volumes matter more than they look. **MIOpen** and **Triton** kernel\ncaches are not persisted across containers by default, so every fresh container\nrecompiles kernels cold — that's where the mysterious multi-minute first-iteration\nstalls come from. Mount them as volumes and the second run is a different machine.\n\n---\n\n# 2. Getting under two minutes\n\nA working pipeline at ~10 minutes per model is a demo. To actually use the thing\nI wanted a **draft tier** fast enough to iterate on: submit, glance away, come\nback to a model.\n\nI started with a list of plausible culprits. Measurement killed most of them.\n\n## What it wasn't\n\n- **GPU stuck at idle clocks.** A well-documented gfx1151 failure mode where the\n iGPU never leaves its idle DPM state under sustained compute. Ruled out: this\n kernel (6.19.10) has the fix, and the GPU pulls ~104 W under load.\n- **Slow GEMM path.** Native gfx1151 fp16 GEMM measures 28–39 TFLOPS here.\n `HSA_OVERRIDE_GFX_VERSION=11.0.0` and hipBLASLt did **not** help — slightly\n worse at 8192. No lever to pull.\n- **The high-res diffusion cascade.** This is the one I was sure about, and it's\n only half true (see below).\n\n## Where the time actually goes\n\nA run is two costs, and they're independent:\n\n```python\noutputs = pipe.run(image, ...) # 1. diffusion\nglb = o_voxel.postprocess.to_glb(...) # 2. mesh + texture bake\n```\n\nTimestamping the bake's verbose stages was the moment it clicked. On a\n1M-vertex / 2.15M-face mesh:\n\n| bake stage | cost |\n|---|---|\n| remesh + clean | ~1 s |\n| **UV unwrap** | **~480 s** |\n| rasterize + texture bake | ~3.5 s |\n\nThe bake was **the** bottleneck, and inside it, one stage was 99% of the cost.\nThe only lever on UV unwrap is feeding it fewer faces, via `decimation_target` —\nand the cost is wildly super-linear:\n\n| `decimation_target` | bake |\n|---|---|\n| 1,000,000 *(default)* | ~480 s |\n| 200,000 | 13.5 s |\n| 100,000 | **7.1 s** |\n| 50,000 | 5.6 s |\n\nCrucially the **texture is unaffected** — rasterization is only ~3.5 s regardless,\nso a 1024² or 2048² atlas costs the same either way. Dropping `decimation_target`\nby 10× costs you mesh density and nothing else. `texture_size`, which I'd assumed\nwas the expensive knob, barely registers.\n\n## And the diffusion knob\n\nTRELLIS.2's `run()` exposes `pipeline_type` ∈ `512` / `1024` / `1024_cascade`\n(default) / `1536_cascade`. The default runs a 512³ shape flow model and then a\n**1024³ refine** — and that refine is where the ~70 s/step genuinely lives.\nSkipping it with `pipeline_type='512'` gives **~37 s of diffusion warm**\n(~5 s/step × 8 steps). The 1024³ refine adds detail; for a draft it isn't worth\n9 minutes.\n\nNote the sampler-steps parameter is `shape_slat_sampler_params`, *not*\n`slat_sampler_params` — the latter is silently ignored, which is how you spend an\nafternoon \"measuring\" a knob that isn't connected.\n\n## The draft recipe\n\n```python\ngenerate(\n image,\n pipeline_type='512', # skip the 1024³ refine → ~37 s diffusion\n slat_steps=8, # shape-SLat sampler steps (default 12)\n decimation_target=100_000, # the giant bake lever → ~7 s bake\n texture_size=1024, # full-quality atlas, ~free\n)\n```\n\n**41.7 s warm, on the box.** Against ~10 minutes for the default\n`1024_cascade` tier — roughly 15×, for a mesh that's less dense and a texture\nthat's identical.\n\n## Cold starts, and why the pipeline stays resident\n\nThe original service shelled out a fresh `python gen3d.py` per job, re-paying the\n4B model load every single time. Now the pipeline loads **once, lazily**, and\nstays resident in VRAM with a background watcher that unloads it after an idle\nwindow:\n\n```python\ndef ensure_loaded(log=print):\n \"\"\"Return the resident pipeline, loading it (cold) on first use.\"\"\"\n global _pipeline, _last_used\n with _lock:\n if _pipeline is not None:\n _last_used = _now()\n return _pipeline\n log(\"[pipeline] loading microsoft/TRELLIS.2-4B (cold load)…\")\n from trellis2.pipelines import Trellis2ImageTo3DPipeline\n p = Trellis2ImageTo3DPipeline.from_pretrained(\"microsoft/TRELLIS.2-4B\")\n p.cuda()\n _pipeline, _last_used = p, _now()\n return _pipeline\n```\n\nThe unloader never fires mid-job (generation holds a busy flag), and 64 GiB of\nGPU-side unified memory holds a 4B model without complaint.\n\nSo the honest numbers are:\n\n| scenario | time |\n|---|---|\n| Draft, warm pipeline | **~42 s** on-box, ~50–55 s through the service |\n| Draft, cold (box just woken) | ~150 s = 47 s model load + ~76 s cold-autotune diffusion |\n| Draft, end-to-end incl. wake, captioning, background removal, preview render | **~1 min 45 s** |\n| Standard `1024_cascade` tier | ~10 min |\n\nThat last row is a real job I ran while writing this post — the whole chain, cold\nbox included.\n\n## One more: the glb was 40 MB\n\nTRELLIS bakes its atlas against a ~1M-triangle mesh, so shipped models were\n**34–46 MB** — almost entirely geometry; the textures were ~1 MB. Since this runs\nafter generation and needs no GPU, it happens on the always-on host rather than\nthe AI box: **weld → attribute-aware quadric simplify → reorder**, via\n`gltf-transform` + `meshoptimizer`.\n\nThe important detail is `simplifyWithAttributes` — UV/normal-aware quadrics.\nNaïve position-only simplification slides the baked atlas across the surface:\n\n| model | triangles | size | SSIM vs original render |\n|---|---|---|---|\n| gaming chair (hard-surface) | 975k → 244k | 38.3 → 13.5 MB | 0.997 |\n| blue fox (furry/organic) | 995k → 373k | 46.1 → 22.8 MB | 0.988 |\n| lighthouse (architectural) | 984k → 246k | 34.4 → 12.3 MB | 0.996 |\n\nThe `error` bound (max surface deviation as a fraction of the mesh AABB) is the\nquality guarantee: simplification stops before exceeding it, which is why the\nfurry fox auto-refused to go below 37% while the hard-surface models happily hit\nthe full 25%.\n\nIf you're tempted by the standalone `gltfpack` binary because it avoids a Node\ndependency — don't. Its `-si` uses position-only simplification: same 0.25 ratio\nscored **SSIM 0.936 vs 0.997** on the chair, with visible texture sliding on\ntufted surfaces.\n\n---\n\n# 3. Making it a workflow\n\nA model in a container isn't a tool. What I actually wanted was: drop an image in\na browser, walk away, get a titled, tagged, textured, optionally-rigged model.\n\nThat means several models, one after another, on **one GPU that crashes if you\nrun two things on it**.\n\n## The orchestrator\n\nRather than let every service guess whether the GPU is free, there's a single\nscheduler that runs *on* the box and owns all of it. It has a persistent job\nqueue, supervises each model as a worker subprocess (load = spawn, unload = kill,\nwhich is how VRAM actually gets freed), and enforces one invariant above all:\n**at most one GPU-exclusive model resident at a time.**\n\nModels register themselves with a decorator that declares what they need:\n\n```python\nfrom brain_sdk import model, Resources, Residency, Output, FileResult, Job\n\n@model(\n name=\"trellis2\", version=\"2.0\",\n resources=Resources(vram_gb=40, ram_gb=16, gpu_exclusive=True),\n residency=Residency(\"cold\"), # unload as soon as its queue drains\n output=Output(\"file\", media_type=\"model/gltf-binary\", ext=\"glb\"),\n load_timeout_s=300, job_timeout_s=1800,\n)\nclass Trellis2:\n def load(self, ctx): # heavy: bring the stack up, wait healthy\n _evox.compose_up(COMPOSE, ctx.log)\n _evox.wait_health(f\"{BASE}/health\", timeout_s=280, log=ctx.log)\n\n def run(self, job: Job) -> FileResult: ...\n def unload(self, ctx): ... # free VRAM\n```\n\nAdapters stay deliberately thin — each one fronts an existing stack over HTTP\nrather than reimplementing it. Today that's TRELLIS.2, background removal,\nauto-rigging, an LLM server, speech-to-text and video generation.\n\nScheduling is **demand-ranked**, not per-job. All models share one queue and\ncompete for a small pool of execution slots; a slot loads a model, **drains that\nmodel's whole queue**, and only gives the slot up when another model out-demands\nit. Batching a model's queue under a single load/unload is where the churn time\ncomes back — analysis of real job history showed ~60% of run-to-run transitions\nwere model *switches*, each one a multi-GB load and unload.\n\n<img src=\"/assets/posts/Trellis2StrixHalo/brain-dashboard.jpeg\" alt=\"The orchestrator dashboard showing a trellis2 job running, the GPU slot marked busy, live GPU/VRAM/temperature stats, and per-model cards with measured memory footprints\" class=\"w-full rounded-lg shadow-lg\">\n\nThe dashboard is mostly there to answer \"why is nothing happening?\" — the GPU\nslot badge, the live GPU/VRAM/power curve, and per-model cards showing measured\nmemory footprint, crash history and whether a model has drifted slower than its\nown baseline.\n\nTwo things I'd build in from the start on a machine like this:\n\n- **A sleep inhibitor.** The desktop environment kept auto-suspending the box\n mid-job — it suspends on idle *input*, not idle compute. The scheduler now holds\n a logind `block` inhibitor while there's queued work and releases it on drain,\n so the box can still sleep when genuinely idle.\n- **Crash accounting with backoff.** A model whose backend is dead was being\n respawned ~3× a second. Each consecutive death now doubles the wait (2 s → 60 s\n cap), and a backing-off model frees its slot instead of starving everything else.\n\n## The chain\n\nThe front-end service runs on an always-on host, holds the user-facing queue, and\nsubmits every GPU step to the orchestrator:\n\n```\nbrowser ── upload ──► queue service (always-on host)\n │ 1. wake the AI box (Wake-on-LAN)\n │ 2. caption the image → `llm` job (vision LLM)\n │ 3. strip the background → `bgremove` job\n │ 4. generate → `trellis2` job → model.glb\n │ 5. simplify the mesh, render a preview (headless browser)\n ▼\n model.glb · thumbnail.webp · render.png · metadata.json\n```\n\nSteps 2–4 all run on the same single GPU, so the worker is **strictly serial** —\none job in flight, stages one after another. I did once build a two-stage worker\nthat captioned the *next* job while the current one generated. It crashed the box.\n\nWhen several images are submitted at once, the loop drains them and runs the\nbatch in **per-model phases** — all the captions, then all the background\nremovals, then all the generations — so each model loads once per batch instead\nof once per job.\n\nThe vision LLM step is a small quality-of-life thing that turned out to matter a\nlot: it looks at the source image and produces a title, description and tags, so\nthe library is browsable instead of being 200 files called `model.glb`.\n\n<img src=\"/assets/posts/Trellis2StrixHalo/3d-gen-generating.jpeg\" alt=\"The web UI mid-generation, showing a progress bar with an estimated time remaining\" class=\"w-full rounded-lg shadow-lg\">\n\nThe progress bar is honest about which phase it's in: the prep steps run ahead of\nthe timer, and the bar only tracks generation, anchored on the timestamp when\ngeneration actually started.\n\n<img src=\"/assets/posts/Trellis2StrixHalo/3d-gen-gallery.jpeg\" alt=\"The 3d-gen model library — a grid of generated model cards with titles, tags and generation times\" class=\"w-full rounded-lg shadow-lg\">\n\n## And then: rigging\n\nOnce you have a mesh, the obvious next question is whether it can move.\n[RigAnything](https://github.com/Isabella98Liu/RigAnything) (SIGGRAPH TOG 2025)\ntakes a mesh and produces a skeleton plus per-vertex skinning weights, baked into\na rigged `.glb`. I picked it over alternatives specifically because its\ndependencies are portable — no `flash_attn`, no `spconv`, both of which are\nnon-starters here. A full rig is ~16–24 s.\n\nIt did need one genuinely awkward build: RigAnything's steps all `import bpy`\n(Blender as a Python module), and bpy only ships cp311/cp313 Linux wheels, while\nthe ROCm torch in the base image is on py3.10. The answer was a **second Python\n3.11 virtualenv inside the same image**, holding both `bpy` and a cp311 ROCm\ntorch wheel. Watch out for the ordering: installing RigAnything's\n`requirements.txt` drags in a CUDA torch that clobbers ROCm, so the ROCm wheel has\nto be force-reinstalled *last*.\n\nOne surprise: a rigged glb shows **nothing**. An armature is transform-only nodes\n— pointing a `<model-viewer>` at the rigged file looks identical to the plain\nmodel, which reads as a bug. So the skeleton is drawn explicitly: joints become\nemissive spheres, parent→child pairs become cylinders, wrapped in a translucent\ntexture-stripped copy of the mesh.\n\n<img src=\"/assets/posts/Trellis2StrixHalo/3d-gen-skeleton.jpeg\" alt=\"A generated model with its auto-generated skeleton overlaid — joints as spheres, bones as cylinders, inside a translucent mesh\" class=\"w-full rounded-lg shadow-lg\">\n\nJoint placement uses the **bind pose**: a joint's world position is the\ntranslation of `inverse(inverseBindMatrix)`, which is the same space the mesh's\nstored `POSITION` values live in — so joints and the unskinned mesh line up with\nno extra maths.\n\n## Publishing\n\nThe last hop is a public shelf. A button on each model pushes the `.glb` plus its\npreview to a small public gallery service. Two details worth stealing:\n\n- **Send the thumbnail with the model.** The gallery can render its own previews,\n but the generation side already has a proper textured render from the preview\n step — shipping it avoids a second, worse render.\n- **Software-render the fallback.** TRELLIS glbs use Draco mesh compression and\n WebP textures, which many mesh libraries silently decode to *zeroed* vertices.\n Decoding the Draco buffer explicitly, decimating in pure numpy, and matte-shading\n with matplotlib gives a thumbnail on a machine with no GPU at all.\n\n---\n\n# What I'd tell someone starting on Strix Halo\n\nThe box is genuinely good: 64 GiB of GPU-addressable memory at this price point\nmeans models that would need a datacentre card just fit. But the ecosystem\nassumes CUDA, and gfx1151 is far enough off the beaten path that you should\nbudget debugging time, not install time.\n\nConcretely:\n\n- **Suspect silently-ignored configuration first.** Both of my multi-day blockers\n (`sdpa` whitelisted out, `TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL` unset) were\n settings that appeared to be applied and weren't. A warning in the logs naming\n the exact variable is easy to scroll past when you're chasing a segfault.\n- **`HSA_OVERRIDE_GFX_VERSION` is a process-scoped tool.** When one library needs\n gfx1100 and another needs native gfx1151, a subprocess is the boundary.\n- **Persist MIOpen and Triton caches.** Otherwise every fresh container pays a\n cold autotune you'll misread as \"the model is slow\".\n- **Profile the boring stage.** I was certain the 4B diffusion cascade was the\n bottleneck. It was a UV unwrap, and the fix was one parameter.\n- **One GPU means one scheduler.** On unified memory, \"it has 64 GB, surely two\n things fit\" is how you hard-hang the machine. Serialise centrally, once, rather\n than hoping each service behaves.\n\nThe end result is a box that sleeps in a cupboard, wakes on a magic packet, and\nturns a photo into a rigged, textured, published 3D model in about two minutes\nwithout a single API call leaving the house. That still feels slightly unfair.\n"},{"slug":"2026-08-06-ai-agent","category":"blog","title":"ai-agent: the app I built to run coding agents from my phone","description":"A self-hosted agent layer for my homelab — spawn and watch Claude Code sessions from an installable PWA, browse every conversation with real per-turn costs, and edit the whole context that shapes what the agent does. Now extracted into its own open-source repo, by the agent itself.","tags":["AI","Homelab","Web"],"body":"\n<aside class=\"not-prose my-8 flex items-start gap-4 rounded-lg border border-theme-primary/30 bg-theme-primary/5 p-5 dark:border-theme-dark-primary/40 dark:bg-theme-dark-primary/10\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" class=\"mt-0.5 h-7 w-7 shrink-0 text-theme-primary dark:text-theme-dark-primary\">\n <path d=\"M12 2v4\"/>\n <rect x=\"3\" y=\"6\" width=\"18\" height=\"14\" rx=\"3\"/>\n <circle cx=\"8.5\" cy=\"12\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\"/>\n <circle cx=\"15.5\" cy=\"12\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\"/>\n <path d=\"M9 16.5h6\"/>\n <path d=\"M1.5 11v4M22.5 11v4\"/>\n </svg>\n <div class=\"text-sm leading-relaxed\">\n <p class=\"font-bold text-theme-primary dark:text-theme-dark-primary\">AI-generated article</p>\n <p class=\"mt-1 text-gray-700 dark:text-gray-300\">\n This post was written by <strong>Claude Fable 5</strong> — inside the very\n tool it describes, during the session that extracted the project into its\n own repository. The screenshots are of that session. The writing is the\n model's; the code and the numbers are from my machines.\n </p>\n </div>\n</aside>\n\nMost of the software I've shipped this year — games, data sites, this blog's\ndeploy pipeline, half my homelab's services — was built by coding agents. Not\n\"AI-assisted\": I type a paragraph describing what I want, and an agent goes off\nfor twenty minutes, edits files in an isolated git worktree, commits, opens a\nPR, deploys a preview, and sends my phone a push notification when it's done.\n\nThe thing that made this workflow actually *livable* is an app I built for it:\n**ai-agent**, a self-hosted PWA that is the cockpit for everything the agents\ndo. Today it moved out of my homelab monorepo into\n[its own open-source repository](https://git.gabvdl.xyz/gabrielvidal/ai-agent) —\nand since the migration was itself done by an agent, through the app, this post\ngets to be pleasantly recursive.\n\n<img src=\"/assets/posts/AiAgent/home.jpeg\" alt=\"The ai-agent home screen: a running session, the notification feed, and the composer with harness and model chips\" class=\"w-full rounded-lg shadow-lg\">\n\n## What it is\n\nOne installable web app (behind my reverse proxy + SSO) that does four jobs:\n\n**Spawn and steer sessions.** The composer at the bottom of the home screen\nstarts a headless `claude -p` run on the homelab — pick a model (the list is\nfetched live from the Anthropic API, so a new release shows up with no code\nchange), an effort level, attach files, go. The run streams into a live thread\nview within a couple of seconds, over Server-Sent Events. I can interrupt it,\nanswer its questions, or send a correcting message mid-run — from a browser tab\nor from my phone on the couch.\n\n**Remember everything, with receipts.** Every Claude Code transcript — the\nJSONL files the CLI writes — is parsed into a thread view with per-turn token\nusage and real dollar cost, tool cards with durations and diff stats, thinking\nblocks, task lists. About 900 transcripts and 458 MB of history so far. Every\nelement has a visibility switch, because a dense thread you can't declutter is\na thread you stop reading.\n\n<img src=\"/assets/posts/AiAgent/conversation.jpeg\" alt=\"A conversation thread: task checklist, deploy phases, commit panel with diffs, live cost in the corner\" class=\"w-full rounded-lg shadow-lg\">\n\nThat screenshot is the session that performed this migration, mid-deploy: its\ntask checklist top-right, the blue-green deploy's phase dots top-left, the\ncommits it has pushed in the side panel, and the running cost of the\nconversation in the corner.\n\n**Edit the agent's context.** Agents are shaped by files: `CLAUDE.md`\ninstructions, skills, hooks, memories. The Files tab exposes that whole tree as\nan editable list with *real* token counts (computed via the API's\n`count_tokens`, recomputed only when a file's hash changes) and what each file\ncosts per session in dollars. When your system prompt is a filesystem, you want\na token budget view of it.\n\n<img src=\"/assets/posts/AiAgent/files.jpeg\" alt=\"The Files tab: the .claude tree with per-directory token counts and the total context cost\" class=\"w-full rounded-lg shadow-lg\">\n\n**Catalog the output.** A projects gallery (every repo with its git history,\ngoals checklist, and what its conversations have cost) and a services catalog\n(static parsing of my compose + Traefik configs — the viewer never drives\nDocker). The projects page currently knows about 59 projects and 43 services;\nthe ai-agent project card says it has cost **€964 across 874M tokens and 37\ntagged conversations** to build. The tool audits itself.\n\n<img src=\"/assets/posts/AiAgent/projects.jpeg\" alt=\"The projects page showing the new standalone ai-agent project card next to the slimmed-down homelab service entry\" class=\"w-full rounded-lg shadow-lg\">\n\nThere are also **cron agents** — scheduled sessions defined by a cron\nexpression plus a markdown prompt file, with the harness/model/effort declared\nin the prompt's frontmatter. A goal-keeper reads each project's `GOAL.md` every\nfive hours and pushes one item forward; a mail secretary sends me a French\nrecap of my self-hosted mailbox every morning; a watcher polls Hugging Face\ntwice a day for a model release I'm waiting on.\n\n<img src=\"/assets/posts/AiAgent/cron.jpeg\" alt=\"The cron jobs page: goal keeper, cert renewal, mail secretary and a model-release watcher, each with schedule and run config chips\" class=\"w-full rounded-lg shadow-lg\">\n\nAnd it's the **notification hub**: agents finish by posting a \"done\" (or a\nblocking \"ask\" with tappable answer buttons) that fans out to Home Assistant\nand lands on my phone; a desk phone can even read the unread feed aloud through\na read-only API key.\n\n## The architecture, briefly\n\nA FastAPI backend and a React PWA ship in one Docker container. The one piece\nthat runs *outside* it on the homelab is deliberate: a small host-side\n**sidecar** process is the only thing that launches `claude -p`. Sessions\ntherefore run as my user, with my real auth, hooks and skills — a spawned\nsession is *exactly* a session I could have started in a terminal, not a\nsandboxed imitation. The same sidecar module can instead run inside the\ncontainer (the image bundles the CLI), which is what the standalone\n`docker compose` shape uses — the backend can't tell the difference.\n\nThe engineering that took the most iterations isn't the features, it's keeping\na filesystem-polling, transcript-parsing app cheap. The rules that each cost me\na real regression to learn:\n\n- **Never re-parse a whole transcript to see what was appended.** A live\n session appends every couple of seconds; a resumable parser per growing file\n is fed only the new bytes. Re-parsing from byte 0 each tick is O(n²) over a\n session.\n- **Never shell out to git per item.** Both catalogs share one HEAD-keyed\n `git log --name-only` walk, bucketed per directory. The services endpoint\n went from two git forks *per service per request* (11 s) to under a second.\n- **Never send content you don't need.** List endpoints are metadata-only; a\n live SSE ping patches one row instead of refetching the list. The\n conversation list went from 1.3 MB to 89 KB; the file bundle from 12.5 s to\n 26 ms; idle CPU from ~58% to ~7% while a session streams.\n- **Never trust a format you don't own.** The CLI's transcript schema is\n internal and undocumented, so a script diffs a structural footprint of fresh\n transcripts against a committed baseline and cross-checks the parser against\n raw record counts — drift fails loudly instead of rendering nonsense.\n\nSecurity got the same paranoia: the API answers only the reverse proxy, the\nhost, or a caller with a shared token — because on a shared Docker network, a\n`PUT` into `.claude/` (hooks!) or a `POST /api/spawn` is host-level code\nexecution. Deploys are blue-green: a standby container is health-gated before\ntraffic cuts over, so a bad build never touches the live one. That deploy has\nrun mid-session many times — including during the session in the screenshot\nabove, which deployed the very change that moved its own source code.\n\n## The extraction, done by the thing being extracted\n\nThe app started in June as a service folder inside my homelab monorepo and\naccreted 277 commits there. Moving it out was itself a one-paragraph prompt:\nthe agent `git subtree split` the folder into a fresh repo with full history,\ncreated the Gitea repository, slimmed the homelab folder down to a deployment\nshim (compose file pointing its build context at the new checkout, Traefik\nroute, deploy script), re-pointed the systemd unit of the sidecar — carefully,\nsince restarting it could have killed the very session doing the work; it\nchecked the unit's `KillMode` first — rebuilt, and blue-green deployed. Then it\ntook the screenshots you see here, and wrote this post.\n\nThe repo is public at\n[git.gabvdl.xyz/gabrielvidal/ai-agent](https://git.gabvdl.xyz/gabrielvidal/ai-agent).\nThe direction (in its `GOAL.md`): one generic container anyone can run —\nopen-source, intuitive, private, secure — with\n[zipgo](https://github.com/GabrielVidal1/zipgo) under the hood so \"build me a\nsite\" ends in a hosted URL, and self-update handled from inside the container.\nThink Lovable, but it's yours: your hardware, your transcripts, your keys, and\nan audit trail of every token it ever spent.\n"}] |