DotnetSnes: Library allowing to use C# to create SNES ROMs

(github.com)

67 points | by ingve 4 days ago ago

19 comments

  • SideburnsOfDoom 3 days ago ago

    > No dynamic allocations are supported (thus no reference type support)

    No class instances. This is a severe limitation, to the point where it will change the character of the language entirely.

    • KallDrexx 2 days ago ago

      Also, I would point out that there are cases where it doesn't change significant amount of the language or hamper things.

      The transpiler also works for Linux eBPF kernel side applications without unsafe pointers and other odd things I need to do for SNES.

      Since you can have methods on c# struct instances, things can still work pretty idiomatically.

      You also do have statically sized arrays, or heap allocated arrays (the latter is only supported if host C application passes it in before hand, and is responsible for free ing it).

    • KallDrexx 2 days ago ago

      In theory, allocation could be hacked in with an arena allocator. I just need to add in reference type support and custom allocation strategies into the transpiler.

      Then again, that's only viable if you don't use enough memory to require rom bank switching, or have better control over which class gets placed in which rom bank.

  • reverseblade2 3 days ago ago

    Just use F# to Rust via fable. https://github.com/ncave/fable-raytracer

    • KallDrexx 2 days ago ago

      It's not clear that Fable would fit the constraints, especially since F# heavily relies on reference types for discriminated unions and lots of other features. It's possible that Fable wouldn't be able to compile down to lower end embedded platforms, or even things like Linux eBPF applications (like my transpiler can do).

      • throw234234234 2 days ago ago

        You can for some features tag the type with a [<Struct>] attribute and it will compile down to a struct/value type representation. That includes records, DUs, active patterns, etc.

    • no-marxism-thx 2 days ago ago

      [flagged]

  • rawling 3 days ago ago
    • chongli 2 days ago ago

      Someone asked on the previous discussion why there's no heap but no one answered and replies are locked out, so I'll answer here:

      There's no heap on the SNES because the system has only one program running at all times (the game). There's no point using dynamic memory allocation when all of the memory on the system is available to the program, so you can just start writing to any valid, writeable address you want. Some addresses are, of course, not writeable (such as ROM space), and some addresses are not memory (memory-mapped IO) but that's not a problem.

      The really nice thing about the SNES is that it uses a 24-bit address space instead of the 16-bit address space of the NES. Most NES games needed to use mapper chips to swap between different ROM banks since with 16 bits you can only address 64KB of memory at a time and many NES games were larger than that. Having 24 bits allows you to fit your game into far fewer ROM banks, greatly simplifying the programming model and making it much more realistic to use a high-level language like C#.

      • kfuse 2 days ago ago

        Frankly, that doesn't explain much, because that sounds like how modern computing works: every program has its own continuos 32/64 bit address space. With 24 bits you can address 16MB which seems enough to be useful if you throw away reflection and such.

        • chongli 2 days ago ago

          16MB is larger than every single SNES game ever released.

          Modern programs have dynamic memory allocation. You can't just start writing to any address you want. You have to request memory from the operating system with malloc() and then free() it when you're done. Memory-managed programming languages handle this for you but it's still there under the covers.

          On the SNES, you simply have all memory available from the beginning. No malloc/free, just start reading and writing.

          • ninkendo 2 days ago ago

            Malloc and free aren’t handled by the operating system, they’re handled in user space.

            Underneath malloc is mmap(2) (or in older unices, setbrk), which actually requests the memory. And with delayed/lazy allocation in the OS, you can just mmap a huge region up front, and it won’t actually do anything until you write/read to the individual pages.

            Point is, you only need one up front call to mmap to write to any page you want.

            • chongli 2 days ago ago

              The SNES doesn’t have any concept of user space. Your program has full control of the hardware. You can do whatever you want. There is no operating system at all.

              • ninkendo 2 days ago ago

                I was responding to your second paragraph, where you talk about modern programs having to request memory from the OS with malloc and free. This isn’t true, malloc and free are not operating system concepts, they are ways for your program to divide up memory address space that is already mapped to you.

                To bring this back to the SNES, you could totally use malloc and free on the SNES, but it would be just vending pointers to the address space you can already use. But my point is that this is no different from a modern OS, because malloc and free are just managing the address space you already got from the OS using mmap. And plenty of malloc implementations avoid repeated calls to mmap by mapping a large amount of space up front.

                My point is, “having full access to the hardware” is completely orthogonal to whether malloc and free are a good idea. You can use malloc/free on a flat address space, just like you can use them on a big fat mmap() region. Instead, the reason you’d generally avoid malloc/free on SNES is that the amount of physical memory is so tiny that it’s generally a bad idea to do any dynamic memory management. Instead you want fixed regions representing in-game entities and logic, and the memory addresses you use should be managed manually in fixed size buffers.

                (If you’re still not convinced, consider that malloc and free work just fine in DOS, where there’s also no virtual memory and you have total access to the physical memory space in your program. DOS doesn’t have mmap, and malloc implementations on DOS just stick to managing the flat, physical address space. No MMU or virtual memory needed.)

                • orphea 2 days ago ago

                    > If you’re still not convinced, consider that malloc and free work just fine in DOS, where there’s also no virtual memory and you have total access to the physical memory space in your program.
                  
                  Also: any modern microcontroller.
              • NobodyNada 2 days ago ago

                The point is that "the system has only one program running at all times" is not an explanation for why there's no dynamic memory allocation, because modern operating systems use virtual memory to give the illusion of a flat address space that the program is in full control over. You can use the .data/.bss sections of an executable exactly as you would use memory in a SNES game.

                And in fact, on many game consoles newer than the SNES (such as the PS1, N64, GC/Wii, DS/GBA, etc.) there's no operating system and the game is in full control of the hardware and games frequently and extensively use dynamic memory allocation. Whether you manage memory statically or dynamically & whether you have an operating system or not below your program are almost completely orthogonal.

                Rather, the reason why SNES games don't use dynamic memory management is because it's impossible to do efficiently on the SNES's processor. Dynamic memory management requires working with pointers, and the 65816 is really bad at handling pointers for several reasons:

                - Registers are 16 bits while (far) addresses are 24 bits, so pointers to anything besides "data in a specific ROM bank" are awkward and slow.

                - There are only three general-purpose registers, so register pressure is extreme. You can store pointers in the direct page to alleviate this, but addressing modes relative to direct-page pointers are slow and extremely limited.

                - There is no adder integrated into the address-generation unit. Instructions that access memory at an offset from a pointer have to spend an extra clock cycle or two going through the ALU.

                - Stack operations are slow and limited, so parameter passing is a pain and local variables are non-existent.

                All of these factors mean that idiomatic and efficient 65xx code uses static, global variables at fixed addresses for everything. When you need dynamism, you make an array and index into it instead of making general-purpose memory allocations.

                But as you get into more modern 32- or 64-bit processors, this changes. You have more registers and better addressing modes, so the slowness and awkwardness of working with pointers is gone; and addresses are so long that instructions operating on static memory addresses are actually slower due to the increased code size. So, idiomatic code for modern processors is pointer-heavy and can benefit from dynamic memory allocation.