Making a 3D modeler in C in a week
danielchasehooper.com
danielchasehooper.com
Lesson learned, for sure, but I'm too far into the development to swap all of the Raylib stuff out for SDL (or something else) now.
One thing that's always bothered me about Wasm and browser 3d/2d graphics is that I often find minor issues such as scrolling. Look at the example called "Background scrolling & parallax" here: https://www.raylib.com/examples.html
I've tested on several devices and it's definitely not smooth scrolling, unless there's something wrong with my eyes.
How can 2D smooth scrolling not be a solved problem in 2024?
Update: Although, having a closer look at the scene, I see its pixel art, so I bet the author is snapping floating point positions to a pixel point to prevent sub pixel blurring.
Another small update: I was sure requestAnimationFrame was locked to 60fps, but I noticed on Chrome the other day it was 144hz, the full speed of my monitor.
But I think it's more meant to demonstrate drawing parallax layers rather than subpixel scrolling.
TL;DR: if you base your animation- or scroll-speed on the 'raw' measured time between two frames, you'll get micro-stutter because it's pretty much impossible to obtain a non-jittery frame duration on modern operating systems or web browsers, all you can do is try to remove the noise via filtering, or 'align' your measured frame duration with the display refresh interval, which on some platforms cannot be queried.
In web browsers the most important problem is that you can't measure the exact frame duration (which is fallout from Spectre/Meltdown), or obtain a precise 'presentation timestamp', or even query the display refresh frequency.
Even in the native OS APIs that provide a presentation timestamp (like DXGI on Windows or CVDisplayLink on macOS) that timestamp has considerable jitter and has not much to do with the actual presentation time when the frame becomes visible to the user.
And as soon as you base your animation timings on such a jittery timestamp you'll get micro-stutter (the easiest way to get smooth animation is actually to assume a fixed frame duration, but then your animation speed will be tied to the display refresh rate).
It's often possible to eliminate the timing jitter with 'noise removal' filters or just tracking an average over the last couple dozen frames, but those then may behave funny in situations where the frame duration changes drastically (such as moving a window between displays with different refresh rate, or when rendering stops and then resumes because the window or browser tab is fully obscured and then becomes visible again).
PS: Raylib's frame pacing code on the web is also a bit on the crude side [2].
...e.g. it just sleeps for 16 milliseconds, and relies on ASYNCIFY to enable a traditional render loop in browsers. It would actually be better to use a frame callback via requestAnimationFrame (or the Emscripten wrapper function emscripten_request_animation_frame), but this means giving up the cross-platform 'own the game loop' application model. Not that requestAnimationFrame alone solves any of the above mentioned time jitter problems though.
[1] https://medium.com/@alen.ladavac/the-elusive-frame-timing-16...
[2[ https://github.com/raysan5/raylib/blob/f1007554a0a8145060797...
Do you have any comment whether frame blending (actually using a mix of two frame states to produce the rendering) would be a workable solution?
Things Unexpectedly Named After People: https://notes.rolandcrosby.com/posts/unexpectedly-eponymous/
Which is amusing :)
What if Ray the person was named after ray-tracing by his parents?
#pragma once
#include <cstdio>
namespace raylib {
#include <raylib.h>
}My solution to the issue is also full screen borderless combined with resolution scaling.
Hah, good to know I am not the only one.
Like, the whole giving the application maximum priority would work the same?
On Windows, under ideal circumstances borderless-fullscreen performs identically to exclusive-fullscreen as Windows will let the program skip the compositor and present its frames more-or-less directly to the display. (Under really ideal circumstances the same applies to bordered non-fullscreen windows.)
If the compositor can't be skipped, borderless-fullscreen can be a bit brutal on performance: on a 4K 160Hz screen I've experienced an additional 40-milliseconds+ of frame-latency purely from borderless-fullscreen being used.
The Special K wiki has some pages that go into more detail about the situation on Windows: https://wiki.special-k.info/SwapChain, https://wiki.special-k.info/Presentation_Model
There are three different aspects here, each one can be done differently and all can be (and have been) mixed or ignored:
1. Set the video mode. You can either do that or not and use whatever the desktop is running at. You may want to change the video mode not just for resolution but for using a different refresh rate. Both native games (like, e.g. Quake source ports using SDL2) and Wine still do that (you can disable the modesetting in Wine via the registry so that games only see the desktop resolution as the only available one).
2. Create a fullscreen window. There are two ways to do that: either create a window that bypasses redirection ("redirection" here means that the window manager wont manage it, it has nothing to do with compositing - if a compositor is used - though some compositors also use it as a hint for that) and have it cover the entire desktop area (you may want to use randr to figure out the monitor area for multiple monitors) or alternatively use the fullscreen hint on the window and let the window manager handle this. Note that some window managers may not support this or have bugs with it.
3. Handle input. X11 provides an API to capture mouse and/or keyboard input, though strictly speaking this is not really required and TBH i do not see why games ever did that (i can only think of conflicts with other programs but i'd expect this to be something for the user to deal with). In any case, it is rare for games to do that these days. You can just handle input "normally" like any other program. You can provide a (by default disabled) option if you really want to though as the API for capturing is trivial.
For my last game engine i did #1 (with an option to use whatever the desktop resolution is) and #2 (using the fullscreen flag by default with the redirect flag as an alternative for window managers that had issues with the fullscreen flag) but i didn't bother with #3.
> The modern strategy is to just do borderless windowed and pretend true fullscreen doesn't exist.
This explains a lot as to why I experienced some interesting inconsistency going full screen in different applications in Windows ;)
In a composited environment, fullscreen windows are just maximized windows without a border, which triggers some heuristic to unredirect windows (i.e. does not have to pay the price of compositing) when they are the sole thing that is drawn on the screen.
So, on Linux, fullscreen and maximized borderless are the same. Given that Windows is a fully compositing desktop as well, I wonder why the difference still exists.
Some legacy apps (that is, games) react very badly when they're not in full control of their window sizing, priority, input and device polling, ecc.
The current day approach is to instead do the "large borderless window" and then do software scaling of the image in order to match the display resolution. One cool result of this is you can get much better scaling than your display would do naively. Low-res games really benefit from software integer scaling instead of "whatever scaling the display firmware does, usually making all the edges blurry".
Rescaling in software is so fast now that some graphically intense games can do per-frame scaling in order to keep per-frame latencies below a particular goal threshold, at the cost of your game losing fidelity (getting blurier). It's so common that most big-name titles now do this.
We had a professional UI/UX designer react to ShapeUp [1], and one of the things she commented on was the font being hard to visually parse.
I laughed a little when the author yelled "raylib!" to make sure blame was assigned appropriately XD. I'm currently the top GitHub sponsor for raylib, so there's no hate, but I wish he changed some of his defaults.
[0] https://handmadecities.com/seattle
[1] https://vimeo.com/887532756/2972a82e55#t=49m58s (timestamped)
Wow this is kind of insane. About this
> Raylib doesn’t do basic parameter validation, by design. This function segfaults when dataSize is null: (...)
The developer answered this
> For most of the raylib functions is up to the user to validate the inputs, if raylib should consider all possible bad-use scenarios it would require reviewing most of the library functions and it will increase source-code complexity.
True, that is not something a function ought to defend against.
However, the complaint was not about that, though, it was about not checking if the dataSize parameter is NULL.
I don't really have a problem with functions that segfault when given NULL pointer parameters as long as this is clearly documented!
Sometime, however, you just need to be familiar with enough projects in C that common-sense gets built. In the specified example:
unsigned char *LoadFileData(const char *fileName, int *dataSize);
I expected that a function that returns an array needs to tell the caller how large that array is. This specific function is not one I would complain about.These things are usually documented in the headers. In this case it says:
// Load file data as byte array (read)
So, yeah, it's pretty clear to me that the number of bytes has to be returned somewhere, and there's only one parameter called `dataSize`, so this isn't something I consider to be a valid complaint.[EDIT: Escaped pointers, and added last paragraph]
WARNING unpaired | unescaped *'s !! :-)
grrr, markup
And if every C project takes this same philosophy to poor documentation, how exactly is a new person learning C supposed to build that common sense? Brushing off poor documentation with "you just need more experience" is not a valid response. The documentation for that function could be much more clearer without being much longer.
so, while the documentation could and should be more explicit than the single telegraphic line you quote, the function's behavior is fine, and detecting and trying to handle the null pointer would make its behavior worse and harder to debug (except on like an arduino or ms-dos or something)
not every c api description needs to be an introductory tutorial for c
Microsoft has deprecated all pointer validation functions on Windows, because there were plenty of corner cases regarding pointer validation.
It returns an array of bytes. If you, the programmer, wrote a line that called that function, on the very next line you are going to try to use the array, realise that you don't know the length, and realise that the `NULL` that passed in on the line above is probably the output for the length!
In order to actually write a call with `NULL` for the dataSize argument, the programmer needs to be clueless about how to write a for loop.
So, no, I can't easily see a situation where a programmer accidentally uses a `dataSize` parameter of `NULL`, because that would mean they don't know that arrays in C have no length information, which is C 101.
Sure, but they don't carry that length information when being returned from a function.
Not in any sense that is actually useful to the programmer, at runtime at least. You cannot ask an array how big it is.
Pointers actually do have length information as well, otherwise free() would not work. But its the same deal with arrays - there is no way to access it outside of compile time constants, if those are even present.
This is 100% correct. The parent of the parent is confused (or is just using terminology incorrectly) as are the current siblings to my comment.
One post claims pointers carry length information so that free works. This has nothing to do with pointers, it has to do with the implementation of malloc and friends. It's only a promise of a successful malloc that you can free, nothing to do with the pointer other than it was returned by malloc. Pointers can point to all sorts of things, the only information they intrinsically have are the type of the object they point to and its address.
Two posts claim a function can return an array of bytes. This is impossible in C. You can't return an array of bytes. You can return a pointer that might point into a continuous sequence of bytes. You could also return a struct whose member is an array, but that's rarely done. When you return (or pass) what looks like an array syntactically, it is converted to a pointer to its first element.
Arrays, on the other hand, always carry length information via the sizeof operator. A special hand waving case would be you could define a pointer to an array (not a pointer to the first element of an array), so in effect the length is contained within the pointer type. E.g. char (*foo)[42] declared foo as a pointer to an array of char with length 42.
So that is pretty much worthless outside the current scope where they were declared.
That's the best example of avoiding premature optimization I've seen in a while.
It’s easy to forget that many applications probably don’t need dynamic memory management at all. You can often get away with allocating a few fixed size buffers and just handling the edge cases nicely when those buffers are full.
And in such a context, C is indeed a whole lot safer. No memory leaks. Your only concern is buffer overflows, which can be managed through careful use of sizeof when all of your variables are statically allocated. I’m not saying Rust and Go aren’t great options these days, but humble old C still works and doesn’t have to be nightmarishly complex.
Glad to see for the first time a WebAssembly interface where the text does not look blurry. I repeat, it is the first time.
Extending this to programs and some operating systems (such as Windows), in the past few years, there has been a pervasive issue with the text rasterization methods that have become a common trend and default setting.
Unfortunately, users often do not have the option of turning off anti-aliasing to get sharp text, and in the rare cases where this option is available, the interface (menus, etc.) still uses anti-aliasing.
I'm simply increasing awareness among the development community about the problems with text rasterization in applications and OS interfaces. I'm glad not to see blurry text.
Not if you actually tried to achieve that in practice across all browsers and display configurations ;)
For instance Safari had for the longest time a hardwired (e.g. not fixable via CSS) linear filter applied when a WebGL canvas had to be upscaled. It's only been fixed last year or so.
Also unfiltered upscaling done breaks down when fractional scaling outside the browser happens, the result will generally look horrible and there is no good way to fix it. The only workaround is to make hide the scaling artifacts behind a filter, which then makes everything look blurry.
Browsers running on top of Wayland most likely still have all those issues.
What's stopping you from using a fixed array of structs in C#, just as the author has done in C?
Until then, the best way currently, is to make use of Panama to create a C struct like memory layout, and have accessor methods for the low level details.
----
Be aware that since it fundamentally works with SDFs, it is a somewhat different modeling experience (and stores different data) than traditional meshes with triangles, verts, etc.
Transforming it from SDFs into meshes could be done with marching cubes or similar, but you'd likely need to "clean up" such data afterwards in a Blender-style app anyway.
SDFs are great though, if your renderer is SDF-based, too (most are not).
[sorry if you knew this already, wasn't sure]
Thanks for reminding me that one of the best SDF renderers in the industry is stuck behind Sony, deemed a failure because Sony didn't want to publish it to PC (where all the dev scene is).
No disrespect to the author, it's impressive, but Blender gives you extreme control over mesh topology and this doesn't.
You still have to get decent topology out of the modelling package, and marching cubes aren't going to give you that. If you have to retopologize your sculpted model by hand to make it viable for use in a rendering pipeline you might as well have just started with good topology in the first place and save yourself the time.
How do you rig an SDF for animation (eg. skeletal, for a humanoid figure)?
In the case animation, parameterize it by a global clock, and each frame the objects are in slightly different positions dictated by a rough physics model (like a person's skeleton).
Shame that NURBS never really took off in the gaming scene. I get it from a hardware standpoint (GPUs like triangles, and we can cut quads into triangles), but NURBs solve so many problems that trianglular meshes just hack their way around.
>How do you rig an SDF for animation
I imagine with the most unholy mathematical models imaginable. Dreams did it, so it's not impossible. But the results seen definitely don't measure up to a AAA standard (at least in Dreams).
I appreciate all the tangential discussions, but the two key points im trying to make are:
1) SDF's are not likely to supplant detailed hand modelled/cleaned up meshes any time soon, but will continue to be used alongside and in conjunction for all kinds of other cool stuff (soft shadows, ambient occlusion, fonts etc as seen in UE4/UE5)
2) ergo, a modeller based around SDF's is not going to replace mesh based modellers like Blender, Maya etc anytime soon, even to a small degree.
I don't think anyone fundamentally disagrees with you, but an author making a toy 3d modeler in a week usually doesn't have the goal to disrupt an entire industry. It's a nice place to consider on a fundamental level what tools like these could and couldn't do that current work flows cannot.
But I don’t think it’s true. Why do you say SDFs don’t have finite resolution? There are no infinite precision SDF implementations. Meshes as a concept have no resolution limits either, you could in theory store verts in infinite precision, but in practice people usually use floats. It is the same with SDFs. In practice, the resolution limits are identical - it’s whatever you can store in a 32 bit float. It’s extremely common with SDF ray marchers to have a much higher threshold than the smallest fp32 delta, so I would say in practice, meshes probably have higher resolution than SDFs most of the time. And whatever, because it never matters. Nobody is pushing either SDFs or meshes to their resolution limits, and nobody wants that because you get visible quantization artifacts with both meshes and SDFs when you come anywhere close to the resolution limits.
But it's relatively trivial to make one. SDFs are technically polymorphic over the concrete number type used. They are intrinsically infinitely scalable and of infinite resolution, and you then compute an approximation that can be rendered according to the output constraints/requirements.
Meshes on the other hand are intrinsically finite. You've already baked in a finite precision at design time. If you're outputting a mesh, you're throwing away information.
It's like the difference between vector and raster graphics. Vector graphics clearly have superior properties when it comes to scaling and other transformations. Raster images still find more use, but vector graphics are starting to see more use for good reasons.
> whatever, because it never matters.
It's starting to matter more, particularly in CAD modelling where model reuse, parameterization by finite element analysis and other physics models, etc. are starting to align better with SDF.
I won't pretend to know as much about games or CGI in film, but I expect as the tools mature, it will just make sense to start adopting them there too.
> It’s like the difference between vector and raster graphics.
I get what you’re trying to say, but this analogy isn’t great. The difference between vector and raster is more like the difference between an SDF and a voxel grid. A mesh is really, literally, a type of 3d vector graphics, and so is an SDF. The difference between meshes and SDFs is the difference between vector graphics with line segments and vector graphics with CSG and round shapes. There is no inherent quantization of space with either meshes or SDFs like there is with raster images or voxel grids.
SDFs are not intrinsically infinitely scalable any more than meshes are. If you write an SDF to disk, the numbers representing the locations and sizes of it’s components will be rounded to fp32 just like the verts of a mesh typically are.
Meshes are not intrinsically finite, no information is being thrown away during the output process. The implementation of a mesh modeler does usually use floats, and so precision does have limits, but there is nothing about the concept of a mesh that requires using floats. Just think about it: one can, if one wishes, use infinite precision numbers to represent a mesh, there is nothing about the idea of a mesh that precludes infinite precision. The very same is true of SDFs. One can use infinite precision, but it in fact has never yet happened, all SDF implementations so far have used finite precision numbers (both to represent and to render SDFs).
> It’s starting to matter more
You started talking there about something unrelated to my point, and aren’t responding directly to what I said. I wasn’t arguing about the usefulness of SDFs at all, and SDFs having finite resolution doesn’t compromise their utility. SDFs aren’t useful because they have higher resolution, they are useful because they are a different kind of primitive with different properties, they have tradeoffs and so can meet certain goals better than other primitives.
While I fully support liking one language, it wouldnt feel right for me to not mention the benefits other languages bring, such as GC for large dev teams of lower skilled devs, languages with builtin unit tests, languages with templating or less bad macros, etc.
As a novice C programmer, the simplicity and immediacy of results opened my eyes to how C can feel as productive as higher level languages with robust standard libs.
TBH, once you have halfway-good libraries for dealing with `char *` strings as-is, dynamic arrays and hashmaps, you are not going to be much slowed-down using C than using a higher-level language.
You even get much stronger isolation guarantees than most other high-level languages, while getting much more compatibility[1] with any other language you may wish to interface to: https://www.lelanthran.com/chap9/content.html
[1] I did a little Go project, and it annoyed me slightly when I wanted to do performant FFI. For Go, I think the situation has improved since I last checked, though.
That can't possibly be true. Not having to even think about object lifecycles and ownership because all memory is GC'd saves a lot of time all by itself, not even getting into debugging issues when you get it wrong.
Memory allocation takes a very small percentage of my time. I'd guess < 1%. Debugging memory issues used to take me more time, but these days it's basically inconsequential, too.
Having written interpreted languages previously to jumping to C & C++ (>10y programming exp), there's a massive cost to using interpreted and/or GCd languages that comments like this one never seem to acknowledge. Random package bit-rot, high-difficulty memory leaks, high-difficulty AND high consequence manual memory management (to avoid the GC), poor performance tooling, nearly completely opaque performance characteristics, inability to optimize performance past a surface level... etc.
If you're building anything more complex than simple web apps, this shit all adds up to a lot. I've worked at a couple shops that these issues hit like a ton of bricks.
While I actually liked the language, it's complexity is increasing with diminishing returns.
There's no point in getting to a complexity level of, for example, C++ for any language - people who want such levels of complexity will be happy to use C++.
I don't think you could reasonably compare the two in amount of tacit knowledge one has to posses to avoid all kinds of footguns and get best results. The rule of thumb today for C# is to go with simplest way to do something and don't ignore IDE/analyzers' suggestions or warnings. A lot of focus has been put on terseness and simplicity.
Edit: and the most significant evidence for this is in comparing all the CVEs for C/C++ vs. memory safe languages like C#/Java.
> I've worked at a couple shops that these issues hit like a ton of bricks.
What you're missing is that 99% those shops wouldn't have existed at all if they had tried to go the C/C++ route because their products just wouldn't have gotten to a viable state. What your experience shows is that working in memory safe languages is so much easier that even average or mediocre programmers can get a viable product.
But we aren't talking about C/C++.
At least, we weren't, but your comments make a lot more sense in the context of C++.
> Edit: and the most significant evidence for this is in comparing all the CVEs for C/C++ vs. memory safe languages like C#/Java.
Wasn't the most expensive RCE the world has ever seen written in Java?
There are excellent tools for detecting memory leaks/safety issues in C, and you can even write all your own allocators for your own edification / amusement / sanity, but in a GC'd languages, you're pretty much fucked across the board. There's some tooling, but it pales in comparison to the tools available for C.
I would also like to acknowledge the topic of CVEs you brought up. Yes, mistakes in mission critical systems happen. And for those systems, maybe something with better memory safety features is more productive in the long run. The original comment I replied to suggested C can be surprisingly productive with just a few tools, which I stand by supporting.
> What you're missing is that 99% those shops wouldn't have existed at all if they had tried to go the C/C++ route [...] average or mediocre programmers can get a viable product
Hard disagree. The two places I have in mind hired average/mediocre people to do somewhat challenging graphics work. Both had interesting products that may have actually been viable (think matterport, figma) but both failed because the UX sucked .. due to what can only be described as UI jank.
Lastly, it is easy to forget what you spend time on. I ve been tracking all my bugs that took more than 30 minutes for the last 10 years. The vast majority are graphics bugs due to API misuse. Very few are memory safety bugs, especially recently.
EDIT: also, half the costs I listed were related to performance. How the fuck do you justify the statement that those apply to C? What language would you pick to have more control over the assembly the machine is running.. other than assembly I guess..
"Frequent memory leaks" has never happened to me in 20 years of programming in GC'd languages.
> There are excellent tools for detecting memory leaks/safety issues in C
A process you don't even need in GC'd languages. I think I've had maybe a couple of non-critical leaks in those 20 years due to finalizer bugs.
> also, half the costs I listed were related to performance. How the fuck do you justify the statement that those apply to C?
The vast majority of performance issues are related to algorithmic choices. With the right choice of algorithms and data structures, any language will likely get within a constant factor of C.
Sometimes that constant factor matters, most often it does not given the added costs of eliminating that constant factor, eg. in dev time and risk of introducing bugs or security vulnerabilities. And even where it does matter, you're almost certainly better off writing the performance critical kernel in C and then calling into it from a higher level language, as is common in machine learning.
Yep, I have multiple scars and gray hairs due to them.
Look on the bright side: at least you have your hair :-)
I swore off C++ because (maybe coincidentally, maybe not) I started losing hair around the time I was a f/time C++ developer.
I'm liking Go and C# (and some Java and Kotlin, sometimes) these days for programs that are not suitable to be written in plain C.
I've tried liking Python/PHP type languages, but getting a type error only when it is encountered in production gives me the heeby-jeebies.
I think perhaps the context may make it clearer: it was about simplicity.
>>> the simplicity and immediacy of results opened my eyes to how C can feel as productive as higher level languages with robust standard libs.
So, sure, no one is saying that you'll be faster in C, but with such a small cognitive footprint, you can be faster than you'd think.
When programming in C, I don't spend much time thinking about the language, I think about the problem more than the language. I don't think about complex relationships between language features; about what might happen if I use a reference in a lambda. I don't need to remember what the `this` keyword refers to depending on how the function was created. I don't need to puzzle my way out of a painted corner due to colored functions.
It's the simplicity that I was responding to. You go faster than you would expect.
As far as object lifecycles go, there's a small number of idiomatic ways to mitigate the problems. Not foolproof, but with such a simple language, whatever valgrind reports can be quickly fixed.
Regarding ownership: I'm not really aware of how GC languages, by way of being GC, helps there. I'm pretty certain it doesn't. If you pass an object to a method in Java, C#, whatever, and that method starts a new thread with it, you're still going to be boned if the callee starts modifying it at the same time.
Whatever ownership issues you have in C, you'll have in most other GC languages as well.
I would agree that you can be faster than you'd think on problems that C is reasonably good for. This is a fairly small subset of problems though, where your original comment was phrased like a general statement for any sort of problem / general purpose programming. That's what I take issue with.
If you're going to do any kind of programming that depends on interfacing with the world, UTF, protobufs, even rendering to a screen as with this article, you're going to be pulling in those same sorts of dependencies that you denounce from all of those other languages.
> Whatever ownership issues you have in C, you'll have in most other GC languages as well.
I agree you have similar thread safety issues, the ownership issues I was referring to was for managing lifetimes leading to double-frees or leaks. Yes there are some idioms that almost work, but "almost work" is exactly the point, in GC'd languages they actually do just work.
I understand the appeal behind the economy of C but we just shouldn't pretend it's something that it's not.
Which is why the concept of thread-safe types exists. The documentation usually explicitly states if a particular container/service/anything else is safe to call from multiple threads concurrently or not, and the standard library goes to great lengths to either make the types that are expected to be used in multi-threaded scenarios thread-safe or to offer thread-safe alternatives (like ConcurrentDictionary<K, V>).
Worst case, most types are engineered in such a way that should thread-safety be violated, it would either lead to an exception or an invalid state but not to memory safety issues.
got to appreciate the effort to make the irony possible : )
Ah!!! It's the Alanis Morrisette meaning of irony, not the dictionary one!
But the parent commenter dilutes the definition further. A project with 2024 lines of code in 2024 is just an amusing coincidence. There's no reason why you'd expect a project in 2024 to not have 2024 lines of code.
When's the next one?
I love this idea of reinventing wheel with such explicit goal (even if it sounds counterintuitive to some), we can rethink initial assumptions, best case scenario we come out with better implementations and paradigms than existing standards, worst case scenario you learn internals of tools and techniques you use daily!
No. C# structs are C structs. Shape[], Span<Shape> or Shape* would not be an array of pointers. Any of the following would work:
var fromHeap = new Shape[100];
var fromStack = (stackalloc Shape[100]);
var fromPool = ArrayPool<Shape>.Shared.Rent(100);
var fromMalloc = (Shape*)NativeMemory.Alloc(sizeof(Shape) * 100);> Some view C as a language so simple and raw that you’ll spend all your time working around the language’s lack of built in data structures, and fixing pointer bugs. The truth is that C’s simplicity is a strength. It compiles quickly. Its syntax doesn’t hide complex operations. It’s simple enough that I don’t have to constantly look things up. And I can easily compile it to both native and web assembly. While C has its share of quirks, I avoid them by habits developed over 22 years of use.
Which is nothing unreasonable.
I code mostly in a Lisp dialect but I don't take offense at, say, the Linux kernel being mostly C.
[0]: https://www.blender.org/ [1]: https://en.wikipedia.org/wiki/Signed_distance_function [2]: https://en.wikipedia.org/wiki/Constructive_solid_geometry
[1] https://ephtracy.github.io/index.html?page=magicacsg & https://www.patreon.com/magicavoxel
[2] https://www.adobe.com/products/substance3d/apps/modeler.html