23 ms·
Show HN: NESFab – Programming language for making NES games
This is a long-running personal project I've had to write an optimizing compiler from scratch. Everything was done by me, including the lexer/parser, SSA-based IR, high-performance data structures, and code generator.
Originally I wasn't targeting the NES. It started as a scripting language, then it morphed into a C++ replacement, and then finally I turned it into what it is today. The large scope of the project and colorful history means it's still a little rough around the edges, but it's now working well enough to post.
- deleted 4y ago[deleted]
- tibbon 4y agoNot to increase your scope, but have you considered making it work across other 6502 systems?
- pubby 4y agoThat was the original plan, but I veered off to focus on doing one system really well. I figured if some company wanted to sponsor a port, I could do it, but otherwise I'd direct people to other compilers like LLVM-MOS. I do think it's possible to port, but it's not trivial. Some areas - like the linker - are very specifically designed for the NES.
- TedDoesntTalk 4y ago> if some company wanted to sponsor a port I don’t think any company in 2023 is going to sponsor port of a 6502 compiler, unfortunately.
- detrites 4y agoYou may be mistaken. Retro hw/sw and gaming is a booming business at the moment. Plenty of crowdfunded projects that have been overfunded and then delivered, and even consumer goods being shipped worldwide in major department stores.
- agiacalone 4y agoThis is a really neat project. It reminds me a lot of Inform, in the sense that a full “modern” language was designed around creating an older-style of game.
- cpeterso 4y agoThere is an experimental LLVM backend for 6502! https://llvm.org/devmtg/2022-05/slides/2022EuroLLVM-LLVM-MOS-6502Backend.pdf https://llvm.org/devmtg/2022-05/slides/2022EuroLLVM-LLVM-MOS...
- benj111 4y agoWhy spaces and not tabs for indents? I know this conversation has probably been had many times before. But if I prefer a 2 space tab, and you prefer a 4 space tab, we both get them how we want them with tabs. With spaces, I have to put up with your stupidly wide indents, and try not to scratch my eyes out from the pain.
- resizeitplz 4y agoA question as old as time (or as old as the existence of code editors). 4k results on Hacker News alone - and surely thousands, if not millions more, across the internet. There is no right answer, just differing preference. https://www.google.com/search?q=spaces+vs+tabs+site%3Anews.ycombinator.com https://www.google.com/search?q=spaces+vs+tabs+site%3Anews.y...
- incrudible 4y agoGetting over these pet peeves is one of the best skills you can acquire as a programmer.
- stcroixx 4y agoAgreed. The next challenge is getting over the annoyance every time someone wants to debate this AGAIN.
- postalrat 4y agoTime to turn off linters. Seriously your linting rules suck.
- incrudible 4y agoIf you're unironically linting whitespace you need to get your priorities straight.
- eru 4y agoWhy? Sticking your whitespace rules in the linter is a great way to keep them from taking up valuable time and attention in (human!) code review.
- Taikonerd 4y agoThis is a really interesting approach. One thing that surprised me from the code snippet: fn play_sound() {$4015}(%100) {$4008}($FF) Are these hard-coded memory addresses meaningful to NES devs? I'm surprised that they don't have more readable aliases, like $SOUND_ROM_BASE_ADDR or whatever.
- nubinetwork 4y agoYes. https://www.nesdev.org/wiki/APU#Registers https://www.nesdev.org/wiki/APU#Registers https://github.com/cc65/cc65/blob/master/asminc/nes.inc#L44 https://github.com/cc65/cc65/blob/master/asminc/nes.inc#L44
- kristiandupont 4y agoStill seems to me like something there should be "standard library" symbols for? Or is there some cultural reason for this?
- nubinetwork 4y agoYou would think so, but between NIH, old unmaintainable code (like nesasm/magickit), or not knowing other code exists, a lot of times you'll see people reinventing the wheel.
- pubby 4y agoThere's standardized names for the graphics registers. The convention for the sound registers is to use the address itself, as there are lots of sound registers and each does multiple things.
- postalrat 4y agoWould you rather have a many different set of constants for the same addresses or just the addresses themselves? Or do you think all NES developers can agree on the same names?
- Drakim 4y agoSome people prefer localhost, others like 127.0.0.1
- nubinetwork 4y agoIts nice that people are still writing code for the NES, but I have to ask what makes this better than using ca65... assembly isn't hard to learn.
- pubby 4y agoI like 6502 assembly a whole lot, but there's definitely limitations to it at scale. Multi-byte arithmetic is a chore, bank switching is a pointless time sink, and managing local variables makes me want to pull my hair out. Those were the deciders in making this.
- karmakaze 4y agoUse an assembler with macros. I spent so much time 'in the zone' of Atari Macro Assembler[0] editor MEDIT, that when someone interrupted me I'd simply respond "I'm in medit" as if in meditation. [0] https://atariwiki.org/wiki/attach/Atari%20Macro%20Assembler/Atari%20Macro%20Assembler-OCR.pdf https://atariwiki.org/wiki/attach/Atari%20Macro%20Assembler/...
- noobermin 4y agoI agree with you, I was worried about doing asm the first time but after you learn it you realize it's not that terrible at all. That said, multiple approaches at different levels benefits everyone.
- algem 4y agothis is really cool, thanks for sharing.
- bmitc 4y agoAs someone trying to undertake some personal projects like this, I have a question. How do you keep motivated enough to work on this for so long and dedicatedly? As a solo developer, how did you manage things like getting stuck on technical issues that were either bugs out of your control or something you didn't have the knowledge to know how to fix?
- MysteryChessBox 4y agoHonestly keeping yourself motivated through the end is a monumental task and I can only do it on smaller games (which coincidentally fit the NES hence why I actually have a finished game on it). It's all too easy to just quit when all the fun stuff gets programmed, leaving only the boring parts left. If there's a magic trick for bigger project motivation I'd like to know, too.
- cloogshicer 4y agoThink of something you really enjoy doing. As a random example I'll pick "playing video games". Now, imagine you've been playing the same game for a couple days in a row. And you're getting kinda sick of it. Not that it's bad, it's got its good moments. You've just had enough of it, for now. Would you continue playing it? The answer is (hopefully): Of course not! You'd move on to something else you enjoy more. If the game was truly engaging, you'll come back to it anyways, maybe a few days/weeks/months later, when it captures your interest again. Since a few years this is my attitude towards side projects. The flipside is that you have to accept the fact that 99% of the things you start will never get finished. But that's ok, as long as you enjoy the work itself. Of course that doesn't mean there aren't difficult moments. But there has to be a genuine, intrinsic motivation, which is independent of shipping something. Keeping a journal/todo-list for every project (I use an infinite bulleted list like Dynalist) really helps, since you can just come back to a project anytime by looking at the end of the list.
- pubby 4y agoMotivation was sporadic. I'd have a few weeks of intense focus, followed by months of inactivity. The nice thing about compilers it that 90% of their code is interesting, so there was always something fun worth coming back to. Besides, I put other projects on hold so I wouldn't get distracted. I don't always know the best way to do something, but so long as I know some way to do it, I can make progress. Tough bugs are just a matter of perseverance. And hey, if it's still not working, just drop the feature. In a personal project, you can do that and nobody can stop you. One more thing: don't talk about your projects until they're 95% done. Seeking early validation is a poison pill.
- AdmiralAsshat 4y agoDoes this language have support for being able to save the game's state into a simulated battery a la Legend of Zelda/Metroid?
- pubby 4y agoThere's no built-in support for that, but you can still do it. Nowadays flash memory is used rather than battery stuff, but the idea remains the same.
- noobermin 4y agoSorry for the nitpick but Metroid did not have a battery pack and used a password system.
- eru 4y agoOn NES. On Famicom it saved to disk.
- a_e_k 4y agoNice work! Pubby, it looks like the markdown for the documentation goes off the rails in 12.3.
- dom96 4y agoReally cool but shouldn't this actually be a library rather than a programming language?
- detaro 4y agoGiven the authors top comment here the answer to that is a very clear "no".
- dom96 4y agoWhat top comment? The one in this submission? it doesn't explain at all why this needs to be a programming language
- detaro 4y agoHow would you learn to write an optimizing compiler by writing a library?
- dom96 4y agoJust because the author wanted to write an optimising compiler doesn't change the fact that this might be better served as a library. The author could have written an optimising compiler for a language with a purpose that couldn't be served by a library.
- eru 4y agoYou could maybe turn this into an embedded domain specific language, if your host language is powerful enough. But seeing that you want to be spitting out optimized 6502 machine code, it's not really useful to do this as a library for most host languages.
- warpspin 4y agoPretty cool project. While I never did NES games, I did assembler on the C64 for a while. The language alone minus the NES parts would already have been super useful back then at least for the high level parts of the programs I wrote.
- agentultra 4y agoThis is very cool. I had wanted to do a project like this once. Nice work!
- foldor 4y agoReally cool stuff! Is there an syntax highlighter available on any IDE's like VSCode available? I'm having a hard time following the documentation without it as it's hard for me to know what is a keyword and what is a variable for example. Single letter types can be hard for me to follow, but I can definitely appreciate their style. Nice work.
- leroy-is-here 4y agoI didn’t take a deep dive, but I’m interested in how you keep the binary size small. Is it simply that LLVM, etc., all have too much function overhead and don’t take advantage of routines over subroutines (function calls)? Or is your back-end just specialized enough to take advantage of architecture-specific features the larger compiler suites don’t have the time to care about? Anyway, this looks dope. Truthfully, we could use more hyper-specific languages like this in all sorts of areas.
- pubby 4y agoLLVM and GCC are great at optimizing until the last leg of the race where they convert IR to assembly code. Their back-ends were designed for modern systems, not the ancient 6502, and so getting them to work involves kludges and clever hacks. I recall there being a video on how LLVM approaches this - search LLVM MOS on youtube. I plan on writing an article on how my own code generator works. If you check my HN profile in a week or two you'll probably see it submitted.
- leroy-is-here 4y agoGood work. I’m looking forward to the article.
- mysterymath 4y agoIn LLVM-MOS, we mainly struggle with its register allocator; the rest of the backend is really quite reasonable. The "Greedy register allocator" in LLVM is a just finely tuned priority allocator with nice live-range splitting. It works great for zero page cache locations, but it's just not tuned very well for tight register classes like those involving the processor's three architectural registers. I've half a mind to implement Hack's SSA-based register allocator in LLVM to use on A, X, and Y; this would clean up the oodles of spurious copies LLVM-MOS spits out in the worst cases.
- mungoman2 4y agoI have long dreamed of a language for 8-bit computers/consoles with a compiler that helps scheduling routines. For example, let's say I have a short snippet of code that needs to run every H blanking period. I should just declare something like @every_hblank Blabla_code() Or scheduling some code for X pixels into a line. Actual scheduling can be done however the compiler wants to, using interrupts or inserting dummy instructions for delays. I guess in the end it's a constraint problem to solve where all these snippets fit in. I imagine this makes it easier to write games/special effects as you remove the tricky cycle counting. Easier, but maybe less fun...
- JohnBooty 4y agoI know that the SGDK (libraries and toolchain for Sega Genesis/Megadrive) lets you do what you're asking for, within plain old C. You just do: SYS_setHIntCallback(foo); SYS_setVIntCallback(bar); Where "foo" and "bar" are the functions you want to be called during hblank and vblank. Here's somebody showing how to pull off some scaling effects using them: https://under-prog.ru/en/sgdk-image-resize/ https://under-prog.ru/en/sgdk-image-resize/ A lot of recent commercial Genesis/MD releases have been made with SGDK; performance seems quite nice. After all, theoretically, if your libraries go hard enough and lean on macros and/or inline assembler C doesn't need to carry a big speed penalty vs. assembler. I think this is also made possible by the Genesis/Megadrive hardware throwing interrupts during hblank/vblank but don't quote me on that. Of course, the really insane effects still require cycle counting. My understanding is that the Overdrive 2 demo uses heaps of it. It's strictly 50hz only because it's that tightly coupled to various specific timings (also obviously 50hz gives you room for a few more instructions during blanking intervals vs. 60hz) https://www.youtube.com/watch?v=OeGdJk5zb6c https://www.youtube.com/watch?v=OeGdJk5zb6c
- hunta2097 4y agoI got a Windows Defender alert when downloading the Windows binary: Detected: Trojan:Script/Wacatac.H!ml Status: Removed Details: This program is dangerous and executes commands from an attacker.
- easrng 4y agoI've never seen a Wacatac detection that wasn't a false positive, but who knows.
- jonny_eh 4y ago> Handling banks is normally a tedious affair for programmers, but NESFab handles it for you. The compiler smartly allocates code and data into banks, with the gritty details abstracted away. Killer feature, but is there a cost to bank switching? Could abstracting it result in a lot of performance cost?
- pubby 4y agoThere is a cost: subroutine calls get wrapped in something called a trampoline, and some data is duplicated across multiple banks. You would do both of these things in assembly language too, and could even do it more optimally, but it's an NP-hard problem and takes a lot of thinking.
- eru 4y agoMost practical instances of most NP-hard problems are quite tractable with something like an Integer Programming solver or an SMT solver.
- pkaye 4y agoYou should make a link to the examples directory prominent on the webpage so others get an quick idea of the language features.
- lastdong 4y agoCongrats on the release! Website and code look super clean, and looks like super useful for anyone getting into NES game development. - What languages did you use as inspiration? - Is it possible to write a game to a physical cartridge and play it on a real snes? Thanks for sharing your amazing work!
- deleted 4y ago[deleted]
- nunobrito 4y agoIt is looking fantastic. Great work!
- Timwi 4y agoOut of curiosity, why did you disallow tabs for indentation, and /.../-style comments in the middle of a line?
- deleted 4y ago[deleted]
- stehtisch123 4y agoAwesome! Is it hard/possible to put the finished game on a cartridge or do you play it with an emulator?
- deleted 4y ago[deleted]
- terrycody 4y agoHey I think a lot of nostalgic people sometimes want to hack the game ROMs instead of making a new one. For example, they may want to add more characters, use enemy roles, add more skills and fun things, does your programming language can also provide such purposes?
- russellsprouts 4y agoJust read through the documentation. There are so many great ideas for generating efficient code. `goto mode` in particular is an awesome idea. Generalizing the program to have multiple "entry" points and overlapping their global variables to save RAM, I've never heard of that technique before. The inline assembly integration looks nice too -- accessing the arguments and return address as fields on the function makes a lot of sense.
- apple4ever 4y agoThis is awesome! I'm currently learning programming NES in assembler with FamicomParty[0], and this look like a great next step!! 0. https://famicom.party https://famicom.party