[{"data":1,"prerenderedAt":6137},["ShallowReactive",2],{"navigation":3,"index":43,"blogs":68,"work":708},[4,22],{"title":5,"path":6,"stem":7,"children":8,"page":21},"Blog","\u002Fblog","blog",[9,13,17],{"title":10,"path":11,"stem":12},"Anki Baby Steps","\u002Fblog\u002Fanki-baby-steps","blog\u002Fanki-baby-steps",{"title":14,"path":15,"stem":16},"GSoC 2026: a Hopeful Rejection","\u002Fblog\u002Fgsoc-2026-a-hopeful-rejection","blog\u002Fgsoc-2026-a-hopeful-rejection",{"title":18,"path":19,"stem":20},"Pico Router Changes Directions","\u002Fblog\u002Fpico-router-changes-directions","blog\u002Fpico-router-changes-directions",false,{"title":23,"path":24,"stem":25,"children":26,"page":21},"Work","\u002Fwork","work",[27,31,35,39],{"title":28,"path":29,"stem":30},"D-Map Cache","\u002Fwork\u002Fdmap-cache","work\u002Fdmap-cache",{"title":32,"path":33,"stem":34},"Focal Grid","\u002Fwork\u002Ffocal-grid","work\u002Ffocal-grid",{"title":36,"path":37,"stem":38},"Pico Router","\u002Fwork\u002Fpico-router","work\u002Fpico-router",{"title":40,"path":41,"stem":42},"Valhalla","\u002Fwork\u002Fvalhalla","work\u002Fvalhalla",{"id":44,"title":45,"about":46,"blog":49,"body":49,"description":50,"experience":51,"extension":53,"faq":49,"hero":54,"meta":60,"navigation":61,"path":62,"seo":63,"stem":66,"testimonials":49,"__hash__":67},"index\u002Findex.yml","Victor Yanson",{"title":47,"description":48},"About Me","I'm a systems & embedded software engineer that loves exploring the power of computers. I focus on embedded systems, computer architecture, and open-source infrastructure. Currently exploring FPGA acceleration and RISC-V architectures for graph-processing workloads.\n",null,"Hey there, welcome to my website! This is the place to get the latest on what I'm working on and what I'm thinking about.",{"title":52},"Selected Projects","yml",{"links":55},[56],{"label":57,"to":58,"color":59},"Check out Pico Router","https:\u002F\u002Fgithub.com\u002FPico-Router\u002FPico-Router","neutral",{},true,"\u002F",{"title":64,"description":65},"Home","I'm a systems & embedded software engineer that loves exploring the power of computers. This page is made to get to know me and my work a bit better!","index","qDN3SJvux-404LAo7hLlzzZ0_HLgR89fE9hIvPXHncA",[69,112,126,423,511],{"id":70,"title":71,"author":49,"badge":72,"body":73,"date":102,"description":103,"draft":61,"extension":104,"image":105,"meta":106,"minRead":107,"navigation":61,"path":108,"seo":109,"stem":110,"__hash__":111},"blog\u002Fblog\u002Fanki-part-two.md","Anki Part Two","Japanese Study",{"type":74,"value":75,"toc":98},"minimark",[76],[77,78,79,83,86,89,92,95],"ul",{},[80,81,82],"li",{},"Want to start writing by hand for better memorazation",[80,84,85],{},"Started tracking immersion with refold",[80,87,88],{},"Reviews go up but study times stays stable",[80,90,91],{},"Decent comprehension of N5 input materials",[80,93,94],{},"Gonna repeat these core words for a while to get more of them mature before starting sentence mining",[80,96,97],{},"More dedicated study to specific grammar topics (Genki I and Tae Kim + maybe dedicated grammar decks): verb forms, adjactive conjugation, particles",{"title":99,"searchDepth":100,"depth":100,"links":101},"",2,[],"2026-09-25","...","md","\u002Fcontent\u002Fanki-baby-steps-banner.jpeg",{},3,"\u002Fblog\u002Fanki-part-two",{"title":71,"description":103},"blog\u002Fanki-part-two","vg5k1ViTDl3RlK0SjLIFXrArhn3xLSCNTtSnsaNKVo0",{"id":113,"title":114,"author":49,"badge":36,"body":115,"date":119,"description":103,"draft":61,"extension":104,"image":120,"meta":121,"minRead":107,"navigation":61,"path":122,"seo":123,"stem":124,"__hash__":125},"blog\u002Fblog\u002Fdocker-dependency-hell.md","Docker Dependency Hell",{"type":74,"value":116,"toc":117},[],{"title":99,"searchDepth":100,"depth":100,"links":118},[],"2026-08-29","\u002Fcontent\u002Fdocker-dependency-hell-banner.jpeg",{},"\u002Fblog\u002Fdocker-dependency-hell",{"title":114,"description":103},"blog\u002Fdocker-dependency-hell","8AsIAGjqGb_bbjgzM86OV6Y-1Ftm11jTRShYrzsYxqE",{"id":127,"title":18,"author":49,"badge":36,"body":128,"date":416,"description":417,"draft":21,"extension":104,"image":418,"meta":419,"minRead":420,"navigation":61,"path":19,"seo":421,"stem":20,"__hash__":422},"blog\u002Fblog\u002Fpico-router-changes-directions.md",{"type":74,"value":129,"toc":402},[130,135,139,144,147,150,154,157,185,198,202,205,209,212,216,224,228,231,282,285,340,343,347,350,353,357,360,366,369,388,392,395,398],[131,132,134],"h3",{"id":133},"tldr","TL;DR",[136,137,138],"p",{},"Pico Router is narrowing its focus from an ambitious all-in-one disaster-relief firmware project into a reusable embedded C++ routing library. Rather than building custom hardware integrations and tooling, development will prioritize a clean routing abstraction with bounded, predictable memory usage. Near-term work will focus on cache-aware graph storage and scalable tiled map access, potentially borrowing ideas from software-managed caches and spatial partitioning explored in related projects, while keeping disaster relief as one possible application.",[140,141,143],"h2",{"id":142},"the-original-vision","The Original Vision",[136,145,146],{},"Pico Router was started under ambitious conditions with ambitious goals in mind. The pitch from the very start was to build a fully-fledged autonomous offline routing systems for disaster relief purposes. This included all the bells and whistles. My plan was to not only embed a routing engine on an MCU, but also support all kinds of peripherals and tooling (IMU, OLED display, GPS processing).",[136,148,149],{},"As you can probably infer by my tone, I bit off more than I could chew. That's all right though. I had originally homed in on a very particular use case in order to make all the initial planning of the project slightly easier. Besides, disaster relief is something I genuinely care about. However, what was also true was that Pico Router was my first \"big\" embedded firmware undertaking, making it easy for me to underestimate the scale of what I proposed. In the end, I didn't set out to solve every imaginable problem in the world of embedded routing engines. My goal was rather to offer a novel solution for a specific hardware target and hopefully learn lots along the way. In that respect, I think I'm well on my way to achieving my objectives.",[140,151,153],{"id":152},"where-is-pico-router-today","Where Is Pico Router Today?",[136,155,156],{},"Today, Pico Router is at version 0.1.0 with most of basic functionality in place. Here's a quick overview of implemented features:",[77,158,159,162,165,173,176,179,182],{},[80,160,161],{},"Fixed-size graph representation for nodes and weighted edges",[80,163,164],{},"A* pathfinding with Euclidean-distance heuristics",[80,166,167,168,172],{},"Conversion of OSM data into binary graph files (",[169,170,171],"code",{},"osm-convert",")",[80,174,175],{},"Raspberry Pi Pico 2 firmware target with UART output",[80,177,178],{},"Basic GitHub Actions CI",[80,180,181],{},"Devcontainer-based development environment",[80,183,184],{},"GoogleTest unit tests for paths and routing behavior",[186,187,188,191],"callout",{},[136,189,190],{},"Pico Router's current terminal banner",[136,192,193],{},[194,195],"img",{"alt":196,"src":197},"Pico Router Terminal Demo Banner","\u002Fcontent\u002Fpico-router-blog-photo-1.jpeg",[131,199,201],{"id":200},"so-whats-the-problem","So what's the problem?",[136,203,204],{},"The major problem here is scope creep. I derive a lot of satisfaction by doing stuff manually at this abstraction level. Nevertheless, I quickly tied my hands with emulation platforms, custom benchmarking, IO interfaces, etc. This helped me learn that there's a major difference between making a multi-versatile system and \"just making things work\".",[140,206,208],{"id":207},"what-exactly-is-changing","What Exactly Is Changing?",[136,210,211],{},"The main direction change boils down to the project going deeper and narrower rather than the current, wider approach. I want to move away from writing custom firmware for an all-in-one device in favor of a clean abstraction around the edge routing only. In essence, this means Pico Router is going from a full firmware solution to a third-party library.",[131,213,215],{"id":214},"does-that-mean-that-the-disaster-relief-use-case-is-being-left-behind","Does that mean that the disaster relief use case is being left behind?",[136,217,218,219,223],{},"Not necessarily. Perhaps the most effective thing to do is letting other developers decide themselves how embedded routing is best applied within their system, whether that's disaster relief or anything else for that matter. The key is that the fundamental promise remains: ",[220,221,222],"strong",{},"bounded, predictable memory usage",".",[131,225,227],{"id":226},"what-abstraction-level-are-you-going-for","What abstraction level are you going for?",[136,229,230],{},"Especially at first I'd like to keep things as straightforward as possible. Ideally I'd start off with something like:",[232,233,237],"pre",{"className":234,"code":235,"language":236,"meta":99,"style":99},"language-cpp shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","RouteResultObject pico_router::get_route(uint32_t start_node, uint32_t goal_node);\n","cpp",[169,238,239],{"__ignoreMap":99},[240,241,244,248,251,255,259,262,266,270,273,276,279],"span",{"class":242,"line":243},"line",1,[240,245,247],{"class":246},"sBMFI","RouteResultObject",[240,249,250],{"class":246}," pico_router",[240,252,254],{"class":253},"sMK4o","::",[240,256,258],{"class":257},"s2Zo4","get_route",[240,260,261],{"class":253},"(",[240,263,265],{"class":264},"spNyl","uint32_t",[240,267,269],{"class":268},"sHdIc"," start_node",[240,271,272],{"class":253},",",[240,274,275],{"class":264}," uint32_t",[240,277,278],{"class":268}," goal_node",[240,280,281],{"class":253},");\n",[136,283,284],{},"Of course, the actual API will likely become somewhat more involved than that. A route request may need to specify the graph or routing context and the returned result will need to communicate more than simply a list of nodes. At a minimum, I'd expect something resembling:",[232,286,288],{"className":234,"code":287,"language":236,"meta":99,"style":99},"struct RouteResult {\n  RouteStatus status;\n  Path path;\n  uint32_t total_cost;\n};\n",[169,289,290,301,313,323,334],{"__ignoreMap":99},[240,291,292,295,298],{"class":242,"line":243},[240,293,294],{"class":264},"struct",[240,296,297],{"class":246}," RouteResult",[240,299,300],{"class":253}," {\n",[240,302,303,306,310],{"class":242,"line":100},[240,304,305],{"class":246},"  RouteStatus",[240,307,309],{"class":308},"sTEyZ"," status",[240,311,312],{"class":253},";\n",[240,314,315,318,321],{"class":242,"line":107},[240,316,317],{"class":246},"  Path",[240,319,320],{"class":308}," path",[240,322,312],{"class":253},[240,324,326,329,332],{"class":242,"line":325},4,[240,327,328],{"class":264},"  uint32_t",[240,330,331],{"class":308}," total_cost",[240,333,312],{"class":253},[240,335,337],{"class":242,"line":336},5,[240,338,339],{"class":253},"};\n",[136,341,342],{},"The important point however is that the caller shouldn't need to know how the route was calculated.",[131,344,346],{"id":345},"how-will-pico-router-integrate-as-a-third-party-dependency","How will Pico Router integrate as a third-party dependency?",[136,348,349],{},"The goal is to make Pico Router be easily available where embedded developers can conveniently access it. I'd honestly like to once again not go overboard and make it initially accessible as a standalone C++ library through a CMake install. Later I could start releasing pre-bundled binaries if there's any need for that.",[136,351,352],{},"Another thing that does look attractive is releasing Pico Router with a dedicated Zephyr thread to make RTOS usage smoother. Once again though, I think time will tell on this one.",[131,354,356],{"id":355},"what-features-will-need-to-change","What features will need to change?",[136,358,359],{},"Fortunately, not too many of the existing features will need to be outright removed or change significantly. The current progress covers only the most fundamental parts of the system. Still, the short and long term roadmap does change more drastically. Where I was quite intent on getting a hands-on demo as soon as possible not too long ago, the focus now shifts towards creating a foundational stability to build on.",[136,361,362,363,365],{},"In the short-term I'll drop most work on ",[169,364,171],{}," and focus on optimising the graph representation for cache aware accessing. Moreover, doing some 'back of a napkin' calculations it became clear to me that even extremely compressed graph representations will quickly fill a micro-controller's flash storage. I'd therefore also be interested in experimenting with a tiled graph structure to allow for bigger sized maps using partial accessing.",[136,367,368],{},"Following this logic, I see an opportunity to perhaps make some kind of software based tile cache to fetch larger graph chunks into RAM from SD storage before being accessed by the CPU.",[186,370,371,382],{},[136,372,373,374,172],{},"Example of tiled graph hiearchy (",[375,376,381],"a",{"href":377,"rel":378,"target":380},"https:\u002F\u002Fnextcloud.osgeo.org\u002Fs\u002F2P4dfmn5D4xZCno?dir=undefined&path=%2FLumbardhi%2F28.06&openfile=6610351",[379],"nofollow","_blank","source",[136,383,384],{},[194,385],{"alt":386,"src":387},"Tiled Map Germany","\u002Fcontent\u002Fpico-router-blog-gif-1.gif",[140,389,391],{"id":390},"future-projection","Future Projection",[136,393,394],{},"All in all, Pico Router is still evolving and changing and I'm positive about where it's heading. I have deliberately not yet pushed for a formal community introduction of the project seeing as everything is still quite raw. Hopefully by the end of this year I can release an alpha to get some much needed feedback.",[136,396,397],{},"Nonetheless, I'll continue writing on here regarding interesting Pico Router developments.",[399,400,401],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}",{"title":99,"searchDepth":100,"depth":100,"links":403},[404,405,406,409,415],{"id":133,"depth":107,"text":134},{"id":142,"depth":100,"text":143},{"id":152,"depth":100,"text":153,"children":407},[408],{"id":200,"depth":107,"text":201},{"id":207,"depth":100,"text":208,"children":410},[411,412,413,414],{"id":214,"depth":107,"text":215},{"id":226,"depth":107,"text":227},{"id":345,"depth":107,"text":346},{"id":355,"depth":107,"text":356},{"id":390,"depth":100,"text":391},"2026-08-23","The Pico Router project is narrowing its focus and turning into a third-party C++ routing library for constrained embbed hardware targets.","\u002Fcontent\u002Fblog-banner-1.jpeg",{},6,{"title":18,"description":417},"sSzGSrN2b2eer58Zue-Epsl52RZ1BScupFppy6YQXbI",{"id":424,"title":10,"author":49,"badge":72,"body":425,"date":506,"description":507,"draft":21,"extension":104,"image":105,"meta":508,"minRead":325,"navigation":61,"path":11,"seo":509,"stem":12,"__hash__":510},"blog\u002Fblog\u002Fanki-baby-steps.md",{"type":74,"value":426,"toc":500},[427,430,433,437,448,452,455,459,462,466,475,481,484,490,493,497],[428,429,134],"h4",{"id":133},[136,431,432],{},"I've started using Anki for a month straight to build up my Japanese vocabulary. Being relatively new to Spaced Repetition System (SRS) based learning, I found the experience quite surprising. What originally felt like looking at abstract hieroglyphs quickly turned into a daily habit, allowing me to learn about 300 words from scratch.",[428,434,436],{"id":435},"helpful-definition","Helpful Definition",[438,439,440],"blockquote",{},[136,441,442,443],{},"\"Anki is a free and open-source flashcard program. It uses techniques from cognitive science such as active recall testing and spaced repetition to aid the user in memorization.\" — ",[375,444,447],{"href":445,"rel":446},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FAnki",[379],"wikipedia.com",[140,449,451],{"id":450},"wait-youre-learning-japanese","Wait, You're Learning Japanese?",[136,453,454],{},"You heard correctly. I'm currently taking a stab at the Japanese language. It all has to do with some reflection I had regarding my future progression as a person and an engineer. Being a (mostly) autodidact in programming, I've experienced both upsides and downsides to my unorthodox path. Without getting into too much detail, I've set my sights on pursuing graduate research in Japan. This, paired with my longstanding interest in the country, has pushed me to embark on this new language learning journey.",[140,456,458],{"id":457},"why-anki","Why Anki?",[136,460,461],{},"I feel like my inclination towards Anki as my preferred SRS platform is not very unique. Especially among Japanese learners (and even more so for those interested in the JATT learning approach), Anki feels more like a rite of passage than anything else. With such a huge mountain of unknown language structures ahead of you, it only feels natural to start with learning the basics through flashcards. Of course, its FOSS nature also attracted me very much. Who knows, if I find any egregious bugs I might end up submitting a PR someday.",[140,463,465],{"id":464},"the-results","The Results",[136,467,468,469,474],{},"And now for the million-dollar question: how much did I learn in my one month of continuous Anki use? Before I answer, I'd like to share some details around my approach. For anyone interested, I picked the Kaishi 1.5k core deck as my starter deck, which contains 1,500 of the most common Japanese words, with each card containing an example sentence using the word. I started off with 10 new cards a day (as per the recommendation of the YouTuber ",[375,470,473],{"href":471,"rel":472},"https:\u002F\u002Fwww.youtube.com\u002F@Livakivi",[379],"Livakivi","). That felt waaay too easy at the time, so I bumped it up to 20 new cards. Shortly after, I understood the concept of review piling, and suddenly I had to lower it again to 10 new cards. Luckily, I could keep up this pace until the end of the month, occasionally oscillating between 9 and 11 daily new cards. Eventually, my reviews got really comfy, so I've been trying to increase my daily new cards to 14, making my daily total reviews about 120 (about 50 minutes a day).",[136,476,477],{},[194,478],{"alt":479,"src":480},"Anki Screenshot of Reviews Stats","content\u002Fanki-screenshot-1.png",[136,482,483],{},"Above you can see a clear picture of how all my totals grew throughout the month. Funnily enough, my daily time spent actually resembles more of a bell curve. As I got more comfortable reviewing old cards, I started taking drastically less time to review the same number of cards every day.",[136,485,486],{},[194,487],{"alt":488,"src":489},"Anki Screenshot of Cards Stats","content\u002Fanki-screenshot-2.png",[136,491,492],{},"In this breakdown, you can see my total cards learned amounts to 335! Not too bad in my opinion. At this point, I still struggle to understand even the most basic audio and text fragments of Japanese. However, I can now noticeably intuit the general structures of sentences, and once in a while (when I'm lucky enough), I hear a sentence containing mostly familiar vocabulary, allowing me to understand it fully. Moreover, I really enjoyed learning straight away with kanji. Having picked up hiragana and katakana shortly before starting Anki, I now pick up kanji radicals here and there, making learning new words slightly easier.",[140,494,496],{"id":495},"future-goals-aspirations","Future Goals & Aspirations",[136,498,499],{},"If my projections are correct and I can keep up this pace, I will finish this core deck in late October. Until then, I'll supplement my grammar study with my copy of the Genki I textbook and immersion whenever possible. I'm very eager to try sentence mining; however, I'll have to be patient until I have a stronger comprehension base before such an approach will be effective. I'll definitely try to drop another article if and when I finish the entire Kaishi 1.5k deck!",{"title":99,"searchDepth":100,"depth":100,"links":501},[502,503,504,505],{"id":450,"depth":100,"text":451},{"id":457,"depth":100,"text":458},{"id":464,"depth":100,"text":465},{"id":495,"depth":100,"text":496},"2026-07-17","After knowing about Anki and SRS for a while I decided to give it a try for my study of Japanese. These are my experience after my first month of daily use.",{},{"title":10,"description":507},"AAXibFmlwSub2VpH0pr4CaCQJZwlMCNFaQ-W3fbUDQ0",{"id":512,"title":14,"author":49,"badge":513,"body":514,"date":702,"description":703,"draft":21,"extension":104,"image":704,"meta":705,"minRead":336,"navigation":61,"path":15,"seo":706,"stem":16,"__hash__":707},"blog\u002Fblog\u002Fgsoc-2026-a-hopeful-rejection.md","OpenStreetMap",{"type":74,"value":515,"toc":695},[516,518,523,525,535,539,556,560,574,577,580,584,587,595,602,605,615,619,622,632,635,638,644,647,650,655,666,670,679,682,689],[428,517,134],{"id":133},[136,519,520,521,223],{},"I was rejected for GSoC 2026 after months of preparation. The disappointment was tough, but the entire experience integrated me into open-source culture. My prep work involved building a deep understanding of routing engine mechanics and systems engineering principles, giving me the necessary foundation to start my own FOSS project, ",[375,522,36],{"href":37},[428,524,436],{"id":435},[438,526,527],{},[136,528,529,530],{},"\"Google Summer of Code is a global, online program focused on bringing new contributors into open source software development. GSoC Contributors work with an open source organization on a 12+ week programming project under the guidance of mentors.\" — ",[375,531,534],{"href":532,"rel":533},"https:\u002F\u002Fsummerofcode.withgoogle.com\u002F",[379],"summerofcode.withgoogle.com",[140,536,538],{"id":537},"how-it-started","How It Started",[136,540,541,542,547,548,550,551,555],{},"In late 2025, I happened to discover Google Summer of Code while watching a ",[375,543,546],{"href":544,"rel":545},"https:\u002F\u002Fwww.youtube.com\u002F@Green-Code",[379],"Green Code"," video. It caught my eye due to its \"meritocratic\" appeal (very attractive for any self-taught engineer), and let's be honest, the fact that it was backed by Google also had something to do with it :-). Being in the midst of my work on ",[375,549,32],{"href":33}," and just being introduced to the world of GIS technology, I looked through the org list to find a project that could fit me. I ended up landing on ",[375,552,513],{"href":553,"rel":554},"https:\u002F\u002Fwiki.openstreetmap.org\u002F",[379]," thanks to the sheer variety of prominent GIS projects they maintain. I checked the proposed projects page on the official wiki only to find out there was just one project up for grabs at the time. Remembering that \"the early bird gets the worm,\" I scanned the repo and contacted the mentor to share my interest in the project.",[140,557,559],{"id":558},"the-prep-phase","The Prep Phase",[136,561,562,563,568,569,573],{},"My preparations kicked off by exploring target repo (",[375,564,567],{"href":565,"rel":566},"https:\u002F\u002Fgithub.com\u002Fsosm\u002Ftemporary-road-closures",[379],"closure.osm.ch",") more thoroughly. In short, the project consists of a monorepo including a Next.js frontend and a FastAPI backend. As expected, the frontend handles the web UI while the Python backend handles most of the business logic. The main goal of closure.osm.ch is to expose a new routing API that community-sources and aggregates public closure data. At the time, it only featured a landing page plus a \"closure-aware\" routing demo. With the main mentor stated goal being integration with one or more dynamic routing engines, I took a look at what engine was currently being used for the demo. It turned out to be ",[375,570,40],{"href":571,"rel":572},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla",[379],", making my very next step exploring and understanding that repository in order to propose an optimal integration.",[136,575,576],{},"Valhalla, being an enormous decade-plus C++ codebase, was especially challenging to wrap my head around. It was designed with strict OOP principles in mind, which on one hand made it easier for me to focus on studying individual aspects of the system, while on the other hand the countless levels of abstraction and polymorphism made my head spin. Luckily for me, the core maintainer team numbers only four people, making discussions with far-reaching system consequences much easier. I have to give a big thanks to this very team for not only hearing out my (at times naive) ideas but also those of all the other participants equally, without a show of favoritism.",[136,578,579],{},"I'd like to mention that open-source software took a bit of adjusting for me. It was quite intimidating, and I was self-conscious about not fitting in and seeming too much like a clueless noob (maybe something worth delving into in another blog post). In fact, this very feeling of fear made me delve into documentation for hours, scour subreddits, and quintuple-check every message and contribution before sending.",[140,581,583],{"id":582},"pre-submission-contributions","Pre-submission Contributions",[136,585,586],{},"As clearly stated by the GSoC and OSM guidelines, contributions to the projects one applies to are obligatory for consideration. Knowing this, I set out to deliver my first-ever PRs to both closures.osm.ch and Valhalla.",[136,588,589,590,594],{},"To summarize, my ",[375,591,593],{"href":592},"\u002Fwork\u002Fvalhalla#docker-version-logging","main merged PR"," for Valhalla added version logging during the startup of the official Docker image. Besides that, I spent some time helping with issue triage and technical discussions surrounding vector tiles and the live traffic subsystem.",[136,596,597,598,601],{},"On the closures.osm.ch side, I fixed Docker compatibility issues, integrated ",[169,599,600],{},"vitest"," together with an initial unit test suite, improved backend build reliability, and reported a handful of architectural issues I encountered while studying the codebase.",[136,603,604],{},"Lastly, I wanted to experience the OpenStreetMap ecosystem from the perspective of a data contributor as well. To that end, I made a handful of map edits around both Barcelona and Nijmegen, getting familiar with the editing workflow and the review process along the way.",[438,606,607],{},[136,608,609,610,614],{},"If you're interested in seeing a compilation of all my contributions, feel free to check out my ",[375,611,613],{"href":612},"#full-propsal-anchor","full GSoC proposal"," below.",[140,616,618],{"id":617},"summary-of-the-proposal","Summary of the Proposal",[136,620,621],{},"I'll try to keep the breakdown of the proposal short, as you can view the entire submitted document below.",[136,623,624,625,628,629,631],{},"In a nutshell, I identified a handful of problems with the current closure-aware routing approach that were keeping it from being production-ready. Supposedly due to its \"demo\" nature, I encountered a few spots with a very unclear separation of concerns. Firstly, the entire routing logic was located in the Next.js frontend, defeating the purpose of the REST API as the project's focal point. It basically just sent a routing request to a remote Valhalla instance with the ",[169,626,627],{},"exclude_locations"," parameter filled with closures fetched from the accompanying PostGIS database. ",[169,630,627],{}," is limited to 50 location points max, which is way too little for any serious trip, and uses closest-point map matching, leading to unreliable routing accuracy. Moreover, the OpenLR module was built from scratch but was not compliant with the actual open standard.",[136,633,634],{},"The main considerations for the solution were to have a system that could provide native-like integration with routing engines while staying engine-agnostic, adding as little runtime overhead as possible, and integrating seamlessly with the existing closures.osm.ch infrastructure.",[136,636,637],{},"The solution I came up with was the addition of a new sidecar service called Closure Sync. Its design is best illustrated by the diagram below:",[136,639,640],{},[194,641],{"alt":642,"src":643},"Closure Sync Architecture Diagram","content\u002Fclosure-sync-diagram.png",[136,645,646],{},"As you can see, Closure Sync represents an independent service that is meant to be placed in the same host environment as the routing engine. It utilizes a Live Traffic overlay provided by many routing engines, which offers an API to change edge costs dynamically at runtime. In essence, the service polls a remote closures.osm.ch instance every couple of minutes to fetch live data and transforms the fetched data into a readable traffic overlay format, where all closed edges are assigned an infinite cost, effectively closing them.",[136,648,649],{},"I planned to initially write Closure Sync in Python to comply with a realistic milestone-based schedule. However, after a successful PoC, the service would ideally be rewritten in a compiled language to increase native performance and ship as a single binary.",[136,651,652],{},[375,653],{"id":654},"full-propsal-anchor",[656,657,663],"card",{"dataUmamiEvent":658,"dataUmamiEventFile":659,"icon":660,"target":380,"title":661,"to":662},"file-download","gsoc-proposal.pdf","basil:document-solid","PDF","\u002Fcontent\u002Fgsoc-proposal.pdf",[136,664,665],{},"Click Here To View The Full Proposal",[140,667,669],{"id":668},"moving-on","Moving On",[136,671,672,673,678],{},"As suggested by the title of this post, my proposal was unfortunately rejected. It really wasn't the rejection itself that disappointed me, but rather some of the details surrounding it. Prior to submitting my proposal, I had publicly asked for some feedback in the ",[375,674,677],{"href":675,"rel":676},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fdiscussions\u002F5944",[379],"Valhalla discussion on GitHub",". I shortly afterward found out that asking for feedback like that is not really in accordance with the \"GSoC mentality,\" but the maintainers were still patient with me and engaged thoroughly. The point is that a couple of days after the conclusion of the discussion (still a few days before the deadline), another GSoC-aspiring commenter joined, name-dropping the mentor and thanking them because some of his doubts had been answered. Thinking little of it, I moved on with my preparations.",[136,680,681],{},"After getting rejected, I naturally wanted to know what the winning picks were. To my surprise, I found the same commenter presenting a rework of closures.osm.ch that included a \"sidecar service called Closure Sync.\" This is not meant as an exposé of this particular participant. I publicly shared my idea, so it's only natural that he used it in his own preparation, and I did not see his project, so it is not my place to judge it. It did, however, make the rejection sting more.",[136,683,684,685,688],{},"That being said, I in no way regret the time I put in. I knew from the start that the majority of proposals never see the light of day. In exploring Valhalla, I've developed a deep appreciation for routing technology, and I'm determined to grow my status as a trusted contributor to the project (more contributions in ",[375,686,687],{"href":41},"works","). Moreover, I learned from scratch what it means to create open-source software. It deeply resonated with my principles, and I'm looking forward to finding more cool projects to help out on.",[136,690,691,692,694],{},"While working on the GSoC proposal, I also deepened my knowledge of embedded systems, leaving me with an itch to do more impactful work on a real project. In essence, that's where the idea for ",[375,693,36],{"href":37}," was born. Having gained the fundamental knowledge of how one of the most powerful and impactful routing engines works on the inside, I started looking for MCU-compatible alternatives. Sadly (or luckily), no one has yet tried to take on the challenge in a FOSS format, clearing the way for this new adventure!",{"title":99,"searchDepth":100,"depth":100,"links":696},[697,698,699,700,701],{"id":537,"depth":100,"text":538},{"id":558,"depth":100,"text":559},{"id":582,"depth":100,"text":583},{"id":617,"depth":100,"text":618},{"id":668,"depth":100,"text":669},"2026-05-19","A detailed breakdown of my personal experience applying to 2026 edition of Google Summer of Code.","\u002Fcontent\u002Fgsoc-banner.jpeg",{},{"title":14,"description":703},"pjcTALr_QGoVohVSkmHDon6CscU6-vtQjK7fI-gsjkE",[709,2820,4056,4775],{"id":710,"title":28,"body":711,"color":2807,"date":2808,"description":2809,"draft":21,"extension":104,"image":2810,"meta":2811,"navigation":61,"oneliner":2812,"path":29,"seo":2813,"stem":30,"tags":2814,"url":49,"__hash__":2819},"work\u002Fwork\u002Fdmap-cache.md",{"type":74,"value":712,"toc":2788},[713,715,718,723,731,735,756,760,775,781,784,790,793,803,807,810,814,817,821,824,828,831,837,841,844,1129,1155,1158,1162,1165,1171,1175,1178,1198,1268,1271,1277,1281,1284,1290,1293,1296,1340,1343,1347,1366,1372,1386,1391,1394,1426,1442,1446,1449,1452,1621,1760,1764,1771,1774,1787,1793,1797,1800,1834,1837,1843,1862,1865,1869,1876,1919,1922,1925,1985,1988,2192,2272,2276,2279,2282,2516,2519,2539,2542,2745,2762,2782,2785],[131,714,134],{"id":133},[136,716,717],{},"Built a simple direct-mapped data cache for the CFU Proving Ground RISC-V SoC in Verilog, sitting between the CPU and main memory. Implemented a 16-line, 64-byte cache with tag\u002Findex address slicing, hit\u002Fmiss handling, CPU stalling, and write-through\u002Fwrite-no-allocate semantics, then verified the design through Verilator simulation and GTKWave debugging while adding cache performance counters to measure hit rates.",[719,720],"table-of-contents",{"exclude":721,":max-depth":722},"Table of Contents, TL;DR","3",[656,724,728],{"icon":725,"target":380,"title":726,"to":727},"i-simple-icons-github","Code","https:\u002F\u002Fgithub.com\u002FVictorYanson\u002FCFU-Proving-Ground\u002Fblob\u002F48486392679fc3ebdbe8db0b572b6c263bb74cee\u002Fmain.v#L190-L290",[136,729,730],{},"Checkout the code for yourself on GitHub",[140,732,734],{"id":733},"background","Background",[136,736,737,738,743,744,749,750,755],{},"Having gotten extensive practice solving ",[375,739,742],{"href":740,"rel":741},"https:\u002F\u002Fhdlbits.01xz.net\u002Fwiki\u002FMain_Page",[379],"HDLBits"," and ",[375,745,748],{"href":746,"rel":747},"https:\u002F\u002Fasmbits.01xz.net\u002Fwiki\u002FMain_Page",[379],"ASMBits"," problems and reading through Patterson and Hennesy's ",[375,751,754],{"href":752,"rel":753},"https:\u002F\u002Fbooks.google.pl\u002Fbooks\u002Fabout\u002FComputer_Organization_and_Design_RISC_V.html?id=H7wxDQAAQBAJ&source=kp_book_description&redir_esc=y",[379],"Computer Organization and Design RISC-V Edition"," I wanted to get my hands dirty with a project. Exploring different RISC-V implementations I came across Kise Lab's work at Tokyo Insitute of Science on CFU Proving Ground, which with their RVProc RISC-V core gave me the perfect building blocks to add my own hardware component.",[131,757,759],{"id":758},"cfu-proving-ground-in-a-nutshell","CFU Proving Ground in a Nutshell",[136,761,762,763,768,769,774],{},"In essence, ",[375,764,767],{"href":765,"rel":766},"https:\u002F\u002Fgithub.com\u002Farchlab-sciencetokyo\u002FCFU-Proving-Ground",[379],"CFU Proving Ground"," is an RISC-V acceleration framework for FPGA development which iterates on Google's ",[375,770,773],{"href":771,"rel":772},"https:\u002F\u002Fgithub.com\u002Fgoogle\u002FCFU-Playground",[379],"CFU Playground"," project. It describes a relatively simple five stage pipelined CPU containing a programmable Custom Function Unit (CFU) allowing users to write their own accelarated instructions using RISC-V's R-type instruction encoding. The diagram below shows the so-called RVProc 32 bit RISC-V core implementation diagram with the actual CFU circled in red.",[136,776,777],{},[194,778],{"alt":779,"src":780},"CPU Diagram Showing Five Stage Pipeline","\u002Fcontent\u002Fd-map-cache-screenshot-2.png",[136,782,783],{},"Beyond the CPU itself, CFU Playground also offers an SoC built from scratch featuring instruction, data, and video memory modules connected via various interfaces and address decoders. It's important to note the entire SoC was is less than 1,000 lines of Verilog code making it extremely compact, readable, and of course extensible ;).",[136,785,786],{},[194,787],{"alt":788,"src":789},"SoC Diagram","\u002Fcontent\u002Fd-map-cache-photo-2.jpeg",[136,791,792],{},"In this SoC diagram the video I\u002FO is truncated as it is not relevant to this project.",[794,795,796],"tip",{},[136,797,798,799,802],{},"I was absolutely stunned to find out how little lines of code are needed to make a fully ISA compliant RISC-V soft core. Even knowing that that's exactly the charm behind RISC, having a ",[169,800,801],{},"main.c"," file get compiled by GCC and run on a 1,000 line processor feels quite insane!",[140,804,806],{"id":805},"the-plan","The Plan",[136,808,809],{},"Going through the possible extensions of the architecture I considered a handful of options. I thought about extending the existing branch prediction mechanism. Later, I also considered writing a simple UART interface. Finally, I landed on a direct-mapped data cache wich would sit between the CPU and the data memory.",[131,811,813],{"id":812},"why-specifically-a-direct-mapped-cache","Why specifically a direct-mapped cache?",[136,815,816],{},"The cache seemed attractive to me due to it allowing me to study the effect of caching on this particular RISC-V SoC while keeping the implementation simple enough to understand, verify, and modify. Limiting myself to a direct-mapped cache specifically instead of a n-way or fully associative cache made things even more straightforward by not requiring a complex replacement policy. Moreover, I can cover all the data memory with just 16 cachelines containing 32 bit words of data each.",[131,818,820],{"id":819},"whats-the-point-of-adding-such-a-simple-cache","What's the point of adding such a simple cache?",[136,822,823],{},"I think this is valid question to ponder. After all, the cacheless CPU has direct access to the memory allowing for lightning fast reads and writes. Adding a cache that uses one or more clock cycles to fetch and store data after a cache miss would effectivly decrease the entire systems performance. Nonetheless, the cache provides me a simplified model of a cached memory hierarchy allowing experiments with temporal and spatial locality. After completing the implementation I would be able to measure the performance impact of cache-aware versus cache-unaware memory-access patterns in (almost) any arbitrary C\u002FC++ code.",[140,825,827],{"id":826},"the-implementation","The Implementation",[136,829,830],{},"Before diving into the code it is important to understand where the cache controller will actually live on the SoC. Unlike some other CPU caches my cache is placed outside of the CPU itself as a standalone module on the SoC as it is fairily simple and externalizing it helps readability. It effectively acts as a proxy between the CPU and the data memory module.",[136,832,833],{},[194,834],{"alt":835,"src":836},"SoC Diagram with Cache","\u002Fcontent\u002Fd-map-cache-photo-4.jpeg",[131,838,840],{"id":839},"the-interface","The Interface",[136,842,843],{},"As you might expect, this meant that I needed to re-use the current data bus for the CPU to cache connection and create a new bus for communication with the data memory. Both of this should ultimately done without touching any of the CPU\u002Fmemory internals.",[232,845,849],{"className":846,"code":847,"language":848,"meta":99,"style":99},"language-verilog shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","module dmap_cache (\n  input  wire        clk_i,\n  input  wire        re_i,\n  input  wire        we_i,\n  input  wire [31:0] addr_i,\n  input  wire [31:0] wdata_i,\n  input  wire  [3:0] wstrb_i,\n  output reg  [31:0] rdata_o,\n  output wire        stall_o,\n\n  output wire        mem_re_o,\n  output wire        mem_we_o,\n  output wire [31:0] mem_addr_o,\n  output wire [31:0] mem_wdata_o,\n  output wire [3:0]  mem_wstrb_o,\n  input  wire [31:0] mem_rdata_i\n);\n","verilog",[169,850,851,863,878,889,900,926,947,970,994,1007,1013,1025,1037,1059,1081,1104,1124],{"__ignoreMap":99},[240,852,853,856,859],{"class":242,"line":243},[240,854,855],{"class":264},"module",[240,857,858],{"class":246}," dmap_cache",[240,860,862],{"class":861},"swJcz"," (\n",[240,864,865,869,872,875],{"class":242,"line":100},[240,866,868],{"class":867},"sbssI","  input",[240,870,871],{"class":867},"  wire",[240,873,874],{"class":308},"        clk_i",[240,876,877],{"class":861},",\n",[240,879,880,882,884,887],{"class":242,"line":107},[240,881,868],{"class":867},[240,883,871],{"class":867},[240,885,886],{"class":308},"        re_i",[240,888,877],{"class":861},[240,890,891,893,895,898],{"class":242,"line":325},[240,892,868],{"class":867},[240,894,871],{"class":867},[240,896,897],{"class":308},"        we_i",[240,899,877],{"class":861},[240,901,902,904,906,909,912,915,918,921,924],{"class":242,"line":336},[240,903,868],{"class":867},[240,905,871],{"class":867},[240,907,908],{"class":861}," [",[240,910,911],{"class":867},"31",[240,913,914],{"class":253},":",[240,916,917],{"class":867},"0",[240,919,920],{"class":861},"] ",[240,922,923],{"class":308},"addr_i",[240,925,877],{"class":861},[240,927,928,930,932,934,936,938,940,942,945],{"class":242,"line":420},[240,929,868],{"class":867},[240,931,871],{"class":867},[240,933,908],{"class":861},[240,935,911],{"class":867},[240,937,914],{"class":253},[240,939,917],{"class":867},[240,941,920],{"class":861},[240,943,944],{"class":308},"wdata_i",[240,946,877],{"class":861},[240,948,950,952,954,957,959,961,963,965,968],{"class":242,"line":949},7,[240,951,868],{"class":867},[240,953,871],{"class":867},[240,955,956],{"class":861},"  [",[240,958,722],{"class":867},[240,960,914],{"class":253},[240,962,917],{"class":867},[240,964,920],{"class":861},[240,966,967],{"class":308},"wstrb_i",[240,969,877],{"class":861},[240,971,973,976,979,981,983,985,987,989,992],{"class":242,"line":972},8,[240,974,975],{"class":867},"  output",[240,977,978],{"class":867}," reg",[240,980,956],{"class":861},[240,982,911],{"class":867},[240,984,914],{"class":253},[240,986,917],{"class":867},[240,988,920],{"class":861},[240,990,991],{"class":308},"rdata_o",[240,993,877],{"class":861},[240,995,997,999,1002,1005],{"class":242,"line":996},9,[240,998,975],{"class":867},[240,1000,1001],{"class":867}," wire",[240,1003,1004],{"class":308},"        stall_o",[240,1006,877],{"class":861},[240,1008,1010],{"class":242,"line":1009},10,[240,1011,1012],{"emptyLinePlaceholder":61},"\n",[240,1014,1016,1018,1020,1023],{"class":242,"line":1015},11,[240,1017,975],{"class":867},[240,1019,1001],{"class":867},[240,1021,1022],{"class":308},"        mem_re_o",[240,1024,877],{"class":861},[240,1026,1028,1030,1032,1035],{"class":242,"line":1027},12,[240,1029,975],{"class":867},[240,1031,1001],{"class":867},[240,1033,1034],{"class":308},"        mem_we_o",[240,1036,877],{"class":861},[240,1038,1040,1042,1044,1046,1048,1050,1052,1054,1057],{"class":242,"line":1039},13,[240,1041,975],{"class":867},[240,1043,1001],{"class":867},[240,1045,908],{"class":861},[240,1047,911],{"class":867},[240,1049,914],{"class":253},[240,1051,917],{"class":867},[240,1053,920],{"class":861},[240,1055,1056],{"class":308},"mem_addr_o",[240,1058,877],{"class":861},[240,1060,1062,1064,1066,1068,1070,1072,1074,1076,1079],{"class":242,"line":1061},14,[240,1063,975],{"class":867},[240,1065,1001],{"class":867},[240,1067,908],{"class":861},[240,1069,911],{"class":867},[240,1071,914],{"class":253},[240,1073,917],{"class":867},[240,1075,920],{"class":861},[240,1077,1078],{"class":308},"mem_wdata_o",[240,1080,877],{"class":861},[240,1082,1084,1086,1088,1090,1092,1094,1096,1099,1102],{"class":242,"line":1083},15,[240,1085,975],{"class":867},[240,1087,1001],{"class":867},[240,1089,908],{"class":861},[240,1091,722],{"class":867},[240,1093,914],{"class":253},[240,1095,917],{"class":867},[240,1097,1098],{"class":861},"]  ",[240,1100,1101],{"class":308},"mem_wstrb_o",[240,1103,877],{"class":861},[240,1105,1107,1109,1111,1113,1115,1117,1119,1121],{"class":242,"line":1106},16,[240,1108,868],{"class":867},[240,1110,871],{"class":867},[240,1112,908],{"class":861},[240,1114,911],{"class":867},[240,1116,914],{"class":253},[240,1118,917],{"class":867},[240,1120,920],{"class":861},[240,1122,1123],{"class":308},"mem_rdata_i\n",[240,1125,1127],{"class":242,"line":1126},17,[240,1128,281],{"class":861},[136,1130,1131,1132,743,1135,1138,1139,1141,1142,1144,1145,1147,1148,1150,1151,1154],{},"From the CPU side a handful of wires are exposed closely resembling the original memory interface including: ",[169,1133,1134],{},"re_i",[169,1136,1137],{},"we_i"," for read\u002Fwrite enable signals, ",[169,1140,923],{}," for the target address, ",[169,1143,944],{}," for the write data, and ",[169,1146,991],{}," for the data retruned to the CPU. Also ",[169,1149,967],{}," is for requesting bytes and half words through a byte-write mask. The only addition to the interface is the use of a stall output wire ",[169,1152,1153],{},"stall_o",". This wire is simple used to idle the CPU during the cycles when the data is being fetched after a cache miss.",[136,1156,1157],{},"From the memory side things are mostly the same only the direction of the data being reversed.",[131,1159,1161],{"id":1160},"the-internals","The Internals",[136,1163,1164],{},"While reading the explanation of the internals keep the following mental model in mind:",[136,1166,1167],{},[1168,1169,1170],"em",{},"From a high-level my cache works like you would expect any CPU cache to work. The CPU requests data, the cache checks whether the data is already present within its storage, if so (hit), it returns the data immediately. In case the data is not present (miss), it asks main memory for the word, stalls the CPU for one cycle, stores the returned word in the appropriate cache line, and then returns it.",[428,1172,1174],{"id":1173},"cache-storage","Cache Storage",[136,1176,1177],{},"Data is stored within the module in three unpacked arrays of registers:",[77,1179,1180,1186,1192],{},[80,1181,1182,1185],{},[169,1183,1184],{},"cache_data"," represents the 16 cachelines containing a 32 bit word each,",[80,1187,1188,1191],{},[169,1189,1190],{},"cache_tag"," stores the sliced tag of the memory address which is currently associated with the data in the cacheline at one of the 16 indices (more about this slicing in a second),",[80,1193,1194,1197],{},[169,1195,1196],{},"cache_valid"," is initialized at startup to 0 to force compulsory misses when commencing the software boot.",[232,1199,1201],{"className":846,"code":1200,"language":848,"meta":99,"style":99},"reg [31:0] cache_data  [0:15];\nreg [25:0] cache_tag   [0:15];\nreg        cache_valid [0:15];\n",[169,1202,1203,1229,1253],{"__ignoreMap":99},[240,1204,1205,1208,1210,1212,1214,1216,1219,1221,1223,1226],{"class":242,"line":243},[240,1206,1207],{"class":867},"reg",[240,1209,908],{"class":308},[240,1211,911],{"class":867},[240,1213,914],{"class":253},[240,1215,917],{"class":867},[240,1217,1218],{"class":308},"] cache_data  [",[240,1220,917],{"class":867},[240,1222,914],{"class":253},[240,1224,1225],{"class":867},"15",[240,1227,1228],{"class":308},"];\n",[240,1230,1231,1233,1235,1238,1240,1242,1245,1247,1249,1251],{"class":242,"line":100},[240,1232,1207],{"class":867},[240,1234,908],{"class":308},[240,1236,1237],{"class":867},"25",[240,1239,914],{"class":253},[240,1241,917],{"class":867},[240,1243,1244],{"class":308},"] cache_tag   [",[240,1246,917],{"class":867},[240,1248,914],{"class":253},[240,1250,1225],{"class":867},[240,1252,1228],{"class":308},[240,1254,1255,1257,1260,1262,1264,1266],{"class":242,"line":107},[240,1256,1207],{"class":867},[240,1258,1259],{"class":308},"        cache_valid [",[240,1261,917],{"class":867},[240,1263,914],{"class":253},[240,1265,1225],{"class":867},[240,1267,1228],{"class":308},[136,1269,1270],{},"Keeping the cache to just 16 lines meant a tiny hardware footprint which felt right when slotting my module into someone else's SoC. More importantly, a larger cache would yield almost perfect hit rates too easily. A tight 64 byte size intentionally forces cache collisions making the performance difference between cache-aware and cache-unaware code immediately obvious!",[1272,1273,1274],"note",{},[136,1275,1276],{},"This is essentially the part why I picked the direct-mapped approach. I'd love to introduce more dimensions to the cache, however, that would've complicated things a little too much for my first cache.",[428,1278,1280],{"id":1279},"address-slicing","Address Slicing",[136,1282,1283],{},"As alluded to earlier, incoming memory addresses requested by the CPU are sliced to determine each memory addresses association within the cache.",[136,1285,1286],{},[194,1287],{"alt":1288,"src":1289},"Address Slicing Diagram","\u002Fcontent\u002Fd-map-cache-photo-3.jpeg",[136,1291,1292],{},"The top 26 bits determine its tag, while the following 4 bits decide which of 16 lines that specific address belongs to.",[136,1294,1295],{},"Written in Verilog it looks like:",[232,1297,1299],{"className":846,"code":1298,"language":848,"meta":99,"style":99},"index = addr_i[5:2];\ntag   = addr_i[31:6];\n",[169,1300,1301,1322],{"__ignoreMap":99},[240,1302,1303,1306,1309,1312,1315,1317,1320],{"class":242,"line":243},[240,1304,1305],{"class":308},"index ",[240,1307,1308],{"class":253},"=",[240,1310,1311],{"class":308}," addr_i[",[240,1313,1314],{"class":867},"5",[240,1316,914],{"class":253},[240,1318,1319],{"class":867},"2",[240,1321,1228],{"class":308},[240,1323,1324,1327,1329,1331,1333,1335,1338],{"class":242,"line":100},[240,1325,1326],{"class":308},"tag   ",[240,1328,1308],{"class":253},[240,1330,1311],{"class":308},[240,1332,911],{"class":867},[240,1334,914],{"class":253},[240,1336,1337],{"class":867},"6",[240,1339,1228],{"class":308},[136,1341,1342],{},"Now you might have noticed the far right 2 bits, usually meant for byte offsets, are being left unused. This is because the pre-existing byte masking mechanism in the main memory and in the CPU allow my cache to be fully word aligned. No offsets needed!",[428,1344,1346],{"id":1345},"hit-miss-mechanics","Hit & Miss Mechanics",[136,1348,1349,1350,1353,1354,1357,1358,1361,1362,1365],{},"Now we get to the heart of the design. To give the cache time to fetch\u002Fwrite data to the memory on a miss I needed to find a way to temporarily stall the CPU. The most elegant approach I could come up with was a relatively simple 2-state finite state machine contained in a combinational",[169,1351,1352],{},"always"," block. In essence, the two states represent an ",[169,1355,1356],{},"IDLE"," state, where data can immedeatily be written or retrieved, and a ",[169,1359,1360],{},"WAIT"," state where the CPU is stalled via its handy ",[169,1363,1364],{},"stall_i"," input.",[136,1367,1368],{},[194,1369],{"alt":1370,"src":1371},"2-state Finite State Machine Diagram","\u002Fcontent\u002Fd-map-cache-photo-1.jpeg",[136,1373,1374,1375,1378,1379,1382,1383,223],{},"Inside the cache there's a continuously assigned wire called ",[169,1376,1377],{},"hit"," which, as you might expect, detects whether the current request matches a hit condition or not. The two simple questions it asks is ",[1168,1380,1381],{},"\"Is the incoming index valid?\""," ('no' here would mean a compulsory miss) and ",[1168,1384,1385],{},"\"Does the incoming tag match the current tag in its associated cacheline?\"",[1387,1388,1390],"h5",{"id":1389},"read","Read",[136,1392,1393],{},"Perhaps the most straightforward situation is a hit on a read request. In this case the cache simply returns the requested cacheline and that's it!",[232,1395,1397],{"className":846,"code":1396,"language":848,"meta":99,"style":99},"if (hit) begin\n  rdata_o \u003C= cache_data[index];\nend\n",[169,1398,1399,1410,1421],{"__ignoreMap":99},[240,1400,1401,1404,1407],{"class":242,"line":243},[240,1402,1403],{"class":867},"if",[240,1405,1406],{"class":308}," (hit) ",[240,1408,1409],{"class":867},"begin\n",[240,1411,1412,1415,1418],{"class":242,"line":100},[240,1413,1414],{"class":308},"  rdata_o ",[240,1416,1417],{"class":253},"\u003C=",[240,1419,1420],{"class":308}," cache_data[index];\n",[240,1422,1423],{"class":242,"line":107},[240,1424,1425],{"class":867},"end\n",[136,1427,1428,1429,743,1432,1435,1436,1438,1439,1441],{},"A miss on the other hand temporarily stores the requested index and tag in ",[169,1430,1431],{},"miss_index",[169,1433,1434],{},"miss_tag"," vectors. At the same time, the cache's state is switched to ",[169,1437,1360],{}," so CPU stalls. One extra cycle is spent fetching data from the main memory after which it is returned back to the CPU and set to the ",[169,1440,1356],{}," state again.",[1387,1443,1445],{"id":1444},"write","Write",[136,1447,1448],{},"The cache uses write-through\u002Fwrite-no-allocate semantics, meaning in this case that writes are always passed along to the main memory whether there's a cache hit or not. So, in practice a write miss doesn't affect the cache's internal storage.",[136,1450,1451],{},"A hit conversely does write to the cache's storage. Importantly, the CPU uses byte strobes to write individual bytes and halfwords. Luckily I didn't have to reinvent the wheel here and I could basically copy the main memory's approach.",[232,1453,1456],{"className":846,"code":1454,"filename":1455,"language":848,"meta":99,"style":99},"if (we_i) begin\n  if (hit) begin\n    if (wstrb_i[0]) cache_data[index][7:0]   \u003C= wdata_i[7:0];\n    if (wstrb_i[1]) cache_data[index][15:8]  \u003C= wdata_i[15:8];\n    if (wstrb_i[2]) cache_data[index][23:16] \u003C= wdata_i[23:16];\n    if (wstrb_i[3]) cache_data[index][31:24] \u003C= wdata_i[31:24];\n  end\n  ...\nend\n","My cache byte strobe",[169,1457,1458,1467,1476,1512,1544,1576,1607,1612,1617],{"__ignoreMap":99},[240,1459,1460,1462,1465],{"class":242,"line":243},[240,1461,1403],{"class":867},[240,1463,1464],{"class":308}," (we_i) ",[240,1466,1409],{"class":867},[240,1468,1469,1472,1474],{"class":242,"line":100},[240,1470,1471],{"class":867},"  if",[240,1473,1406],{"class":308},[240,1475,1409],{"class":867},[240,1477,1478,1481,1484,1486,1489,1492,1494,1496,1499,1501,1504,1506,1508,1510],{"class":242,"line":107},[240,1479,1480],{"class":867},"    if",[240,1482,1483],{"class":308}," (wstrb_i[",[240,1485,917],{"class":867},[240,1487,1488],{"class":308},"]) cache_data[index][",[240,1490,1491],{"class":867},"7",[240,1493,914],{"class":253},[240,1495,917],{"class":867},[240,1497,1498],{"class":308},"]   ",[240,1500,1417],{"class":253},[240,1502,1503],{"class":308}," wdata_i[",[240,1505,1491],{"class":867},[240,1507,914],{"class":253},[240,1509,917],{"class":867},[240,1511,1228],{"class":308},[240,1513,1514,1516,1518,1521,1523,1525,1527,1530,1532,1534,1536,1538,1540,1542],{"class":242,"line":325},[240,1515,1480],{"class":867},[240,1517,1483],{"class":308},[240,1519,1520],{"class":867},"1",[240,1522,1488],{"class":308},[240,1524,1225],{"class":867},[240,1526,914],{"class":253},[240,1528,1529],{"class":867},"8",[240,1531,1098],{"class":308},[240,1533,1417],{"class":253},[240,1535,1503],{"class":308},[240,1537,1225],{"class":867},[240,1539,914],{"class":253},[240,1541,1529],{"class":867},[240,1543,1228],{"class":308},[240,1545,1546,1548,1550,1552,1554,1557,1559,1562,1564,1566,1568,1570,1572,1574],{"class":242,"line":336},[240,1547,1480],{"class":867},[240,1549,1483],{"class":308},[240,1551,1319],{"class":867},[240,1553,1488],{"class":308},[240,1555,1556],{"class":867},"23",[240,1558,914],{"class":253},[240,1560,1561],{"class":867},"16",[240,1563,920],{"class":308},[240,1565,1417],{"class":253},[240,1567,1503],{"class":308},[240,1569,1556],{"class":867},[240,1571,914],{"class":253},[240,1573,1561],{"class":867},[240,1575,1228],{"class":308},[240,1577,1578,1580,1582,1584,1586,1588,1590,1593,1595,1597,1599,1601,1603,1605],{"class":242,"line":420},[240,1579,1480],{"class":867},[240,1581,1483],{"class":308},[240,1583,722],{"class":867},[240,1585,1488],{"class":308},[240,1587,911],{"class":867},[240,1589,914],{"class":253},[240,1591,1592],{"class":867},"24",[240,1594,920],{"class":308},[240,1596,1417],{"class":253},[240,1598,1503],{"class":308},[240,1600,911],{"class":867},[240,1602,914],{"class":253},[240,1604,1592],{"class":867},[240,1606,1228],{"class":308},[240,1608,1609],{"class":242,"line":949},[240,1610,1611],{"class":867},"  end\n",[240,1613,1614],{"class":242,"line":972},[240,1615,1616],{"class":308},"  ...\n",[240,1618,1619],{"class":242,"line":996},[240,1620,1425],{"class":867},[232,1622,1625],{"className":846,"code":1623,"filename":1624,"language":848,"meta":99,"style":99},"if (we_i) begin\n  if (wstrb_i[0]) dmem[valid_addr][7:0]   \u003C= wdata_i[7:0];\n  if (wstrb_i[1]) dmem[valid_addr][15:8]  \u003C= wdata_i[15:8];\n  if (wstrb_i[2]) dmem[valid_addr][23:16] \u003C= wdata_i[23:16];\n  if (wstrb_i[3]) dmem[valid_addr][31:24] \u003C= wdata_i[31:24];\nend\n","The data memory byte strobe",[169,1626,1627,1635,1666,1696,1726,1756],{"__ignoreMap":99},[240,1628,1629,1631,1633],{"class":242,"line":243},[240,1630,1403],{"class":867},[240,1632,1464],{"class":308},[240,1634,1409],{"class":867},[240,1636,1637,1639,1641,1643,1646,1648,1650,1652,1654,1656,1658,1660,1662,1664],{"class":242,"line":100},[240,1638,1471],{"class":867},[240,1640,1483],{"class":308},[240,1642,917],{"class":867},[240,1644,1645],{"class":308},"]) dmem[valid_addr][",[240,1647,1491],{"class":867},[240,1649,914],{"class":253},[240,1651,917],{"class":867},[240,1653,1498],{"class":308},[240,1655,1417],{"class":253},[240,1657,1503],{"class":308},[240,1659,1491],{"class":867},[240,1661,914],{"class":253},[240,1663,917],{"class":867},[240,1665,1228],{"class":308},[240,1667,1668,1670,1672,1674,1676,1678,1680,1682,1684,1686,1688,1690,1692,1694],{"class":242,"line":107},[240,1669,1471],{"class":867},[240,1671,1483],{"class":308},[240,1673,1520],{"class":867},[240,1675,1645],{"class":308},[240,1677,1225],{"class":867},[240,1679,914],{"class":253},[240,1681,1529],{"class":867},[240,1683,1098],{"class":308},[240,1685,1417],{"class":253},[240,1687,1503],{"class":308},[240,1689,1225],{"class":867},[240,1691,914],{"class":253},[240,1693,1529],{"class":867},[240,1695,1228],{"class":308},[240,1697,1698,1700,1702,1704,1706,1708,1710,1712,1714,1716,1718,1720,1722,1724],{"class":242,"line":325},[240,1699,1471],{"class":867},[240,1701,1483],{"class":308},[240,1703,1319],{"class":867},[240,1705,1645],{"class":308},[240,1707,1556],{"class":867},[240,1709,914],{"class":253},[240,1711,1561],{"class":867},[240,1713,920],{"class":308},[240,1715,1417],{"class":253},[240,1717,1503],{"class":308},[240,1719,1556],{"class":867},[240,1721,914],{"class":253},[240,1723,1561],{"class":867},[240,1725,1228],{"class":308},[240,1727,1728,1730,1732,1734,1736,1738,1740,1742,1744,1746,1748,1750,1752,1754],{"class":242,"line":336},[240,1729,1471],{"class":867},[240,1731,1483],{"class":308},[240,1733,722],{"class":867},[240,1735,1645],{"class":308},[240,1737,911],{"class":867},[240,1739,914],{"class":253},[240,1741,1592],{"class":867},[240,1743,920],{"class":308},[240,1745,1417],{"class":253},[240,1747,1503],{"class":308},[240,1749,911],{"class":867},[240,1751,914],{"class":253},[240,1753,1592],{"class":867},[240,1755,1228],{"class":308},[240,1757,1758],{"class":242,"line":420},[240,1759,1425],{"class":867},[140,1761,1763],{"id":1762},"testing-debugging","Testing & Debugging",[136,1765,1766,1767,1770],{},"Perhaps it's important to mention that I did not have access to a physical FPGA board during the development and testing of my cache. Fortunately, the included ",[169,1768,1769],{},"Makefile"," already featured some configurations to build and run the hardware in a Verilator emulation.",[136,1772,1773],{},"The default demo program featured in CFU Proving Ground is a simple loop written in C which contiuously prints characters in different colors to random coordinates on a mini LCD display.",[186,1775,1776,1781],{},[136,1777,1778],{},[1168,1779,1780],{},"Example display output on physical Digilent FPGA board taken from the original repo",[136,1782,1783],{},[194,1784],{"alt":1785,"src":1786},"FPGA Display Outpu Example Photo","\u002Fcontent\u002Fd-map-cache-photo-5.png",[136,1788,1789,1790,1792],{},"Once again, the ",[169,1791,1769],{}," configuration came to the rescue to allow for a connection with a virtual display emulator letting demo the cache on the demo program.",[131,1794,1796],{"id":1795},"timing-issue","Timing Issue",[136,1798,1799],{},"I was extremely pleased to find out that with only minor compile errors, which I could quickly fix, the SoC succesfully built and was able to run. To my surprise however, I did not see the expected character printing behavior...",[1801,1802,1803,1816],"table",{},[1804,1805,1806],"thead",{},[1807,1808,1809,1813],"tr",{},[1810,1811,1812],"th",{},"Before Cache",[1810,1814,1815],{},"After Cache",[1817,1818,1819],"tbody",{},[1807,1820,1821,1828],{},[1822,1823,1824],"td",{},[194,1825],{"alt":1826,"src":1827},"Reference Demo Gif","\u002Fcontent\u002Fd-map-cache-gif-2.gif",[1822,1829,1830],{},[194,1831],{"alt":1832,"src":1833},"Buggy Demo Gif","\u002Fcontent\u002Fd-map-cache-gif-1.gif",[136,1835,1836],{},"This immedeatly screamed timing bug to me. Nevertheless, I was unable to traceback my logical mistake by solely rereading the HDL code. This meant I'd have dive into the signal traces find out exactly what was happening.",[136,1838,1839],{},[194,1840],{"alt":1841,"src":1842},"GTKWave Signal Trace Screenshot","\u002Fcontent\u002Fd-map-cache-screenshot-1.png",[136,1844,1845,1846,1848,1849,1851,1852,1854,1855,1858,1859,1861],{},"Using GTKWave I managed to find a state (at ~430ps in the screenshot) which reveals the smoking gun! Here we can see that incoming write enable from the CPU ",[169,1847,1137],{}," is high yet ",[169,1850,1377],{}," stays low, indicating a write miss. As you remember, a miss switches the CPU automatically to the ",[169,1853,1360],{}," state which stalls the CPU and writes the incoming ",[169,1856,1857],{},"mem_rdata_i","to its associated cacheline. So what's the problem? This effectivly goes against the write-through\u002Fno-write-allocate principle by writing data on a cache write miss. More importantly, the stored ",[169,1860,1857],{}," input from the main memory is most likely not even the value we want to be storing in the first place.",[136,1863,1864],{},"This would explain the \"miscalculated\" or \"unsynced\" feeling of the buggy output. The CPU was simply reading stale values it never asked for after write misses. And would you believe it, removing the state switch on write misses resulted an identical character printing animation as the reference.",[131,1866,1868],{"id":1867},"measuring-performance","Measuring Performance",[136,1870,1871,1872,1875],{},"But now we're still left with the question: ",[1168,1873,1874],{},"how do we know whether a given program resulted in a high cache hit rate or not?"," To answer this I needed to setup some kind of way to have insight into the amount of hits and misses which were ocurring. Firstly, two 64 bit registers are initialized at startup as performance counters respresented as integers.",[232,1877,1879],{"className":846,"code":1878,"language":848,"meta":99,"style":99},"reg [63:0] cache_access_cnt;    \u002F\u002F Total amount of accesses\nreg [63:0] cache_hit_cnt;       \u002F\u002F Total amount of hits\n",[169,1880,1881,1901],{"__ignoreMap":99},[240,1882,1883,1885,1887,1890,1892,1894,1897],{"class":242,"line":243},[240,1884,1207],{"class":867},[240,1886,908],{"class":308},[240,1888,1889],{"class":867},"63",[240,1891,914],{"class":253},[240,1893,917],{"class":867},[240,1895,1896],{"class":308},"] cache_access_cnt;    ",[240,1898,1900],{"class":1899},"sHwdD","\u002F\u002F Total amount of accesses\n",[240,1902,1903,1905,1907,1909,1911,1913,1916],{"class":242,"line":100},[240,1904,1207],{"class":867},[240,1906,908],{"class":308},[240,1908,1889],{"class":867},[240,1910,914],{"class":253},[240,1912,917],{"class":867},[240,1914,1915],{"class":308},"] cache_hit_cnt;       ",[240,1917,1918],{"class":1899},"\u002F\u002F Total amount of hits\n",[136,1920,1921],{},"Why 64 bits? Because we're talking about enormous numbers here. In my SoC design the cache is accessed every other cycle or so. Go figure how many total accesses that amounts to in any program with a decent runtime.",[136,1923,1924],{},"As any rudementary counter, they are simply incremented when the performance condition is met.",[232,1926,1928],{"className":846,"code":1927,"language":848,"meta":99,"style":99},"if (re_i || we_i) begin\n  cache_access_cnt \u003C= cache_access_cnt + 1;\n  if (hit) cache_hit_cnt \u003C= cache_hit_cnt + 1;\nend\n",[169,1929,1930,1945,1963,1981],{"__ignoreMap":99},[240,1931,1932,1934,1937,1940,1943],{"class":242,"line":243},[240,1933,1403],{"class":867},[240,1935,1936],{"class":308}," (re_i ",[240,1938,1939],{"class":253},"||",[240,1941,1942],{"class":308}," we_i) ",[240,1944,1409],{"class":867},[240,1946,1947,1950,1952,1955,1958,1961],{"class":242,"line":100},[240,1948,1949],{"class":308},"  cache_access_cnt ",[240,1951,1417],{"class":253},[240,1953,1954],{"class":308}," cache_access_cnt ",[240,1956,1957],{"class":253},"+",[240,1959,1960],{"class":867}," 1",[240,1962,312],{"class":308},[240,1964,1965,1967,1970,1972,1975,1977,1979],{"class":242,"line":107},[240,1966,1471],{"class":867},[240,1968,1969],{"class":308}," (hit) cache_hit_cnt ",[240,1971,1417],{"class":253},[240,1973,1974],{"class":308}," cache_hit_cnt ",[240,1976,1957],{"class":253},[240,1978,1960],{"class":867},[240,1980,312],{"class":308},[240,1982,1983],{"class":242,"line":325},[240,1984,1425],{"class":867},[136,1986,1987],{},"This allowed me to add the metrics to the existing list of Verilator termination logs defined in the top level module of the emulation shell. For a tiny bit more insight I also printed the hit ratio which is just the proportion of hits to accesses.",[232,1989,1992],{"className":846,"code":1990,"highlights":1991,"language":848,"meta":99,"style":99},"final begin\n  $write(\"\\n\");\n  $write(\"===> mcycle                                 : %10d\\n\", mcycle);\n  $write(\"===> minstret                               : %10d\\n\", minstret);\n  $write(\"===> Total number of branch predictions     : %10d\\n\", br_pred_cntr);\n  $write(\"===> Total number of branch mispredictions  : %10d\\n\", br_misp_cntr);\n  $write(\"===> Total number of cache hits             : %10d\\n\", m0.cache.cache_hit_cnt);\n  $write(\"===> Total number of cache accesses         : %10d\\n\", m0.cache.cache_access_cnt);\n  $write(\"===> Cache hit ratio                        : %6.2f%%\\n\",\n    (m0.cache.cache_access_cnt == 0) ? 0.0 :\n    100.0 * m0.cache.cache_hit_cnt \u002F m0.cache.cache_access_cnt);\n  $write(\"===> simulation finish!!\\n\");\n  $write(\"\\n\");\nend\n",[949,972,996,1009,1015],[169,1993,1994,2001,2017,2031,2045,2059,2073,2089,2104,2118,2142,2163,2176,2188],{"__ignoreMap":99},[240,1995,1996,1999],{"class":242,"line":243},[240,1997,1998],{"class":308},"final ",[240,2000,1409],{"class":867},[240,2002,2003,2006,2010,2013,2015],{"class":242,"line":100},[240,2004,2005],{"class":308},"  $write(",[240,2007,2009],{"class":2008},"sfazB","\"",[240,2011,2012],{"class":308},"\\n",[240,2014,2009],{"class":2008},[240,2016,281],{"class":308},[240,2018,2019,2021,2024,2026,2028],{"class":242,"line":107},[240,2020,2005],{"class":308},[240,2022,2023],{"class":2008},"\"===> mcycle                                 : %10d",[240,2025,2012],{"class":308},[240,2027,2009],{"class":2008},[240,2029,2030],{"class":308},", mcycle);\n",[240,2032,2033,2035,2038,2040,2042],{"class":242,"line":325},[240,2034,2005],{"class":308},[240,2036,2037],{"class":2008},"\"===> minstret                               : %10d",[240,2039,2012],{"class":308},[240,2041,2009],{"class":2008},[240,2043,2044],{"class":308},", minstret);\n",[240,2046,2047,2049,2052,2054,2056],{"class":242,"line":336},[240,2048,2005],{"class":308},[240,2050,2051],{"class":2008},"\"===> Total number of branch predictions     : %10d",[240,2053,2012],{"class":308},[240,2055,2009],{"class":2008},[240,2057,2058],{"class":308},", br_pred_cntr);\n",[240,2060,2061,2063,2066,2068,2070],{"class":242,"line":420},[240,2062,2005],{"class":308},[240,2064,2065],{"class":2008},"\"===> Total number of branch mispredictions  : %10d",[240,2067,2012],{"class":308},[240,2069,2009],{"class":2008},[240,2071,2072],{"class":308},", br_misp_cntr);\n",[240,2074,2077,2079,2082,2084,2086],{"class":2075,"line":949},[242,2076],"highlight",[240,2078,2005],{"class":308},[240,2080,2081],{"class":2008},"\"===> Total number of cache hits             : %10d",[240,2083,2012],{"class":308},[240,2085,2009],{"class":2008},[240,2087,2088],{"class":308},", m0.cache.cache_hit_cnt);\n",[240,2090,2092,2094,2097,2099,2101],{"class":2091,"line":972},[242,2076],[240,2093,2005],{"class":308},[240,2095,2096],{"class":2008},"\"===> Total number of cache accesses         : %10d",[240,2098,2012],{"class":308},[240,2100,2009],{"class":2008},[240,2102,2103],{"class":308},", m0.cache.cache_access_cnt);\n",[240,2105,2107,2109,2112,2114,2116],{"class":2106,"line":996},[242,2076],[240,2108,2005],{"class":308},[240,2110,2111],{"class":2008},"\"===> Cache hit ratio                        : %6.2f%%",[240,2113,2012],{"class":308},[240,2115,2009],{"class":2008},[240,2117,877],{"class":308},[240,2119,2121,2124,2127,2130,2133,2135,2137,2139],{"class":2120,"line":1009},[242,2076],[240,2122,2123],{"class":308},"    (m0.cache.cache_access_cnt ",[240,2125,2126],{"class":253},"==",[240,2128,2129],{"class":867}," 0",[240,2131,2132],{"class":308},") ? ",[240,2134,917],{"class":867},[240,2136,223],{"class":308},[240,2138,917],{"class":867},[240,2140,2141],{"class":308}," :\n",[240,2143,2145,2148,2150,2152,2155,2158,2160],{"class":2144,"line":1015},[242,2076],[240,2146,2147],{"class":867},"    100",[240,2149,223],{"class":308},[240,2151,917],{"class":867},[240,2153,2154],{"class":253}," *",[240,2156,2157],{"class":308}," m0.cache.cache_hit_cnt ",[240,2159,62],{"class":253},[240,2161,2162],{"class":308}," m0.cache.cache_access_cnt);\n",[240,2164,2165,2167,2170,2172,2174],{"class":242,"line":1027},[240,2166,2005],{"class":308},[240,2168,2169],{"class":2008},"\"===> simulation finish!!",[240,2171,2012],{"class":308},[240,2173,2009],{"class":2008},[240,2175,281],{"class":308},[240,2177,2178,2180,2182,2184,2186],{"class":242,"line":1039},[240,2179,2005],{"class":308},[240,2181,2009],{"class":2008},[240,2183,2012],{"class":308},[240,2185,2009],{"class":2008},[240,2187,281],{"class":308},[240,2189,2190],{"class":242,"line":1061},[240,2191,1425],{"class":867},[232,2193,2199],{"className":2194,"code":2195,"filename":2196,"highlights":2197,"language":2198,"meta":99,"style":99},"language-plaintext shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","- top.v:47: Verilog $finish\n\n===> mcycle                                 :     614492\n===> minstret                               :     581925\n===> Total number of branch predictions     :     164858\n===> Total number of branch mispredictions  :       7511\n===> Total number of cache hits             :      26061\n===> Total number of cache accesses         :      26506\n===> Cache hit ratio                        :     98.32%\n===> simulation finish!!\n\n- S i m u l a t i o n   R e p o r t: Verilator 5.050 2026-07-01\n- Verilator: $finish at 6us; walltime 0.118 s; speed 51.972 us\u002Fs\n- Verilator: cpu 0.117 s on 1 threads; allocated 2 MB\n","Verilator Runtime Logs",[949,972,996],"plaintext",[169,2200,2201,2206,2210,2215,2220,2225,2230,2236,2242,2248,2253,2257,2262,2267],{"__ignoreMap":99},[240,2202,2203],{"class":242,"line":243},[240,2204,2205],{},"- top.v:47: Verilog $finish\n",[240,2207,2208],{"class":242,"line":100},[240,2209,1012],{"emptyLinePlaceholder":61},[240,2211,2212],{"class":242,"line":107},[240,2213,2214],{},"===> mcycle                                 :     614492\n",[240,2216,2217],{"class":242,"line":325},[240,2218,2219],{},"===> minstret                               :     581925\n",[240,2221,2222],{"class":242,"line":336},[240,2223,2224],{},"===> Total number of branch predictions     :     164858\n",[240,2226,2227],{"class":242,"line":420},[240,2228,2229],{},"===> Total number of branch mispredictions  :       7511\n",[240,2231,2233],{"class":2232,"line":949},[242,2076],[240,2234,2235],{},"===> Total number of cache hits             :      26061\n",[240,2237,2239],{"class":2238,"line":972},[242,2076],[240,2240,2241],{},"===> Total number of cache accesses         :      26506\n",[240,2243,2245],{"class":2244,"line":996},[242,2076],[240,2246,2247],{},"===> Cache hit ratio                        :     98.32%\n",[240,2249,2250],{"class":242,"line":1009},[240,2251,2252],{},"===> simulation finish!!\n",[240,2254,2255],{"class":242,"line":1015},[240,2256,1012],{"emptyLinePlaceholder":61},[240,2258,2259],{"class":242,"line":1027},[240,2260,2261],{},"- S i m u l a t i o n   R e p o r t: Verilator 5.050 2026-07-01\n",[240,2263,2264],{"class":242,"line":1039},[240,2265,2266],{},"- Verilator: $finish at 6us; walltime 0.118 s; speed 51.972 us\u002Fs\n",[240,2268,2269],{"class":242,"line":1061},[240,2270,2271],{},"- Verilator: cpu 0.117 s on 1 threads; allocated 2 MB\n",[131,2273,2275],{"id":2274},"effects-of-cache-aware-vs-unaware-code","Effects of Cache Aware vs Unaware Code",[136,2277,2278],{},"To make sure both the cache and the performance counters actually make sense let's compare some cache aware C code and compare its metrics to cache unaware code.",[136,2280,2281],{},"Let's take a look at the following example:",[232,2283,2287],{"className":2284,"code":2285,"language":2286,"meta":99,"style":99},"language-c shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","int array[64];\n\nint main() {\n  int sum = 0;\n\n  for (int i = 0; i \u003C 64; i++)\n    array[i] = i;\n\n  for (int k = 0; k \u003C 10000; k++) {\n    sum += array[0];\n    sum += array[1];\n    sum += array[2];\n    sum += array[3];\n  }\n\n  return sum;\n}\n","c",[169,2288,2289,2305,2309,2321,2335,2339,2376,2396,2400,2434,2450,2464,2478,2492,2497,2501,2511],{"__ignoreMap":99},[240,2290,2291,2294,2297,2300,2303],{"class":242,"line":243},[240,2292,2293],{"class":264},"int",[240,2295,2296],{"class":308}," array",[240,2298,2299],{"class":253},"[",[240,2301,2302],{"class":867},"64",[240,2304,1228],{"class":253},[240,2306,2307],{"class":242,"line":100},[240,2308,1012],{"emptyLinePlaceholder":61},[240,2310,2311,2313,2316,2319],{"class":242,"line":107},[240,2312,2293],{"class":264},[240,2314,2315],{"class":257}," main",[240,2317,2318],{"class":253},"()",[240,2320,300],{"class":253},[240,2322,2323,2326,2329,2331,2333],{"class":242,"line":325},[240,2324,2325],{"class":264},"  int",[240,2327,2328],{"class":861}," sum ",[240,2330,1308],{"class":253},[240,2332,2129],{"class":867},[240,2334,312],{"class":253},[240,2336,2337],{"class":242,"line":336},[240,2338,1012],{"emptyLinePlaceholder":61},[240,2340,2341,2345,2348,2350,2353,2355,2357,2360,2362,2365,2368,2370,2373],{"class":242,"line":420},[240,2342,2344],{"class":2343},"s7zQu","  for",[240,2346,2347],{"class":253}," (",[240,2349,2293],{"class":264},[240,2351,2352],{"class":861}," i ",[240,2354,1308],{"class":253},[240,2356,2129],{"class":867},[240,2358,2359],{"class":253},";",[240,2361,2352],{"class":861},[240,2363,2364],{"class":253},"\u003C",[240,2366,2367],{"class":867}," 64",[240,2369,2359],{"class":253},[240,2371,2372],{"class":861}," i",[240,2374,2375],{"class":253},"++)\n",[240,2377,2378,2381,2383,2386,2389,2392,2394],{"class":242,"line":949},[240,2379,2380],{"class":308},"    array",[240,2382,2299],{"class":253},[240,2384,2385],{"class":861},"i",[240,2387,2388],{"class":253},"]",[240,2390,2391],{"class":253}," =",[240,2393,2372],{"class":861},[240,2395,312],{"class":253},[240,2397,2398],{"class":242,"line":972},[240,2399,1012],{"emptyLinePlaceholder":61},[240,2401,2402,2404,2406,2408,2411,2413,2415,2417,2419,2421,2424,2426,2429,2432],{"class":242,"line":996},[240,2403,2344],{"class":2343},[240,2405,2347],{"class":253},[240,2407,2293],{"class":264},[240,2409,2410],{"class":861}," k ",[240,2412,1308],{"class":253},[240,2414,2129],{"class":867},[240,2416,2359],{"class":253},[240,2418,2410],{"class":861},[240,2420,2364],{"class":253},[240,2422,2423],{"class":867}," 10000",[240,2425,2359],{"class":253},[240,2427,2428],{"class":861}," k",[240,2430,2431],{"class":253},"++)",[240,2433,300],{"class":253},[240,2435,2436,2439,2442,2444,2446,2448],{"class":242,"line":1009},[240,2437,2438],{"class":861},"    sum ",[240,2440,2441],{"class":253},"+=",[240,2443,2296],{"class":308},[240,2445,2299],{"class":253},[240,2447,917],{"class":867},[240,2449,1228],{"class":253},[240,2451,2452,2454,2456,2458,2460,2462],{"class":242,"line":1015},[240,2453,2438],{"class":861},[240,2455,2441],{"class":253},[240,2457,2296],{"class":308},[240,2459,2299],{"class":253},[240,2461,1520],{"class":867},[240,2463,1228],{"class":253},[240,2465,2466,2468,2470,2472,2474,2476],{"class":242,"line":1027},[240,2467,2438],{"class":861},[240,2469,2441],{"class":253},[240,2471,2296],{"class":308},[240,2473,2299],{"class":253},[240,2475,1319],{"class":867},[240,2477,1228],{"class":253},[240,2479,2480,2482,2484,2486,2488,2490],{"class":242,"line":1039},[240,2481,2438],{"class":861},[240,2483,2441],{"class":253},[240,2485,2296],{"class":308},[240,2487,2299],{"class":253},[240,2489,722],{"class":867},[240,2491,1228],{"class":253},[240,2493,2494],{"class":242,"line":1061},[240,2495,2496],{"class":253},"  }\n",[240,2498,2499],{"class":242,"line":1083},[240,2500,1012],{"emptyLinePlaceholder":61},[240,2502,2503,2506,2509],{"class":242,"line":1106},[240,2504,2505],{"class":2343},"  return",[240,2507,2508],{"class":861}," sum",[240,2510,312],{"class":253},[240,2512,2513],{"class":242,"line":1126},[240,2514,2515],{"class":253},"}\n",[136,2517,2518],{},"The is a textbook example of cache friendly code. The same four elements are accessed repeatedly in a tight loop, meaning that after the initial accesses the cache can serve almost every request directly.",[232,2520,2522],{"className":2194,"code":2521,"language":2198,"meta":99,"style":99},"===> Total number of cache hits             :      39996\n===> Total number of cache accesses         :      40064\n===> Cache hit ratio                        :     99.83%\n",[169,2523,2524,2529,2534],{"__ignoreMap":99},[240,2525,2526],{"class":242,"line":243},[240,2527,2528],{},"===> Total number of cache hits             :      39996\n",[240,2530,2531],{"class":242,"line":100},[240,2532,2533],{},"===> Total number of cache accesses         :      40064\n",[240,2535,2536],{"class":242,"line":107},[240,2537,2538],{},"===> Cache hit ratio                        :     99.83%\n",[136,2540,2541],{},"As expected, the performance counters show a very high hit rate of 99.83%, with only 68 misses out of more than 40,000 accesses.",[232,2543,2545],{"className":2284,"code":2544,"language":2286,"meta":99,"style":99},"int array[64];\n\nint main() {\n  int sum = 0;\n\n  for (int i = 0; i \u003C 64; i++)\n    array[i] = i;\n\n  for (int k = 0; k \u003C 10000; k++) {\n    sum += array[0];\n    sum += array[16];\n    sum += array[32];\n    sum += array[48];\n  }\n\n  return sum;\n}\n",[169,2546,2547,2559,2563,2573,2585,2589,2617,2633,2637,2667,2681,2695,2710,2725,2729,2733,2741],{"__ignoreMap":99},[240,2548,2549,2551,2553,2555,2557],{"class":242,"line":243},[240,2550,2293],{"class":264},[240,2552,2296],{"class":308},[240,2554,2299],{"class":253},[240,2556,2302],{"class":867},[240,2558,1228],{"class":253},[240,2560,2561],{"class":242,"line":100},[240,2562,1012],{"emptyLinePlaceholder":61},[240,2564,2565,2567,2569,2571],{"class":242,"line":107},[240,2566,2293],{"class":264},[240,2568,2315],{"class":257},[240,2570,2318],{"class":253},[240,2572,300],{"class":253},[240,2574,2575,2577,2579,2581,2583],{"class":242,"line":325},[240,2576,2325],{"class":264},[240,2578,2328],{"class":861},[240,2580,1308],{"class":253},[240,2582,2129],{"class":867},[240,2584,312],{"class":253},[240,2586,2587],{"class":242,"line":336},[240,2588,1012],{"emptyLinePlaceholder":61},[240,2590,2591,2593,2595,2597,2599,2601,2603,2605,2607,2609,2611,2613,2615],{"class":242,"line":420},[240,2592,2344],{"class":2343},[240,2594,2347],{"class":253},[240,2596,2293],{"class":264},[240,2598,2352],{"class":861},[240,2600,1308],{"class":253},[240,2602,2129],{"class":867},[240,2604,2359],{"class":253},[240,2606,2352],{"class":861},[240,2608,2364],{"class":253},[240,2610,2367],{"class":867},[240,2612,2359],{"class":253},[240,2614,2372],{"class":861},[240,2616,2375],{"class":253},[240,2618,2619,2621,2623,2625,2627,2629,2631],{"class":242,"line":949},[240,2620,2380],{"class":308},[240,2622,2299],{"class":253},[240,2624,2385],{"class":861},[240,2626,2388],{"class":253},[240,2628,2391],{"class":253},[240,2630,2372],{"class":861},[240,2632,312],{"class":253},[240,2634,2635],{"class":242,"line":972},[240,2636,1012],{"emptyLinePlaceholder":61},[240,2638,2639,2641,2643,2645,2647,2649,2651,2653,2655,2657,2659,2661,2663,2665],{"class":242,"line":996},[240,2640,2344],{"class":2343},[240,2642,2347],{"class":253},[240,2644,2293],{"class":264},[240,2646,2410],{"class":861},[240,2648,1308],{"class":253},[240,2650,2129],{"class":867},[240,2652,2359],{"class":253},[240,2654,2410],{"class":861},[240,2656,2364],{"class":253},[240,2658,2423],{"class":867},[240,2660,2359],{"class":253},[240,2662,2428],{"class":861},[240,2664,2431],{"class":253},[240,2666,300],{"class":253},[240,2668,2669,2671,2673,2675,2677,2679],{"class":242,"line":1009},[240,2670,2438],{"class":861},[240,2672,2441],{"class":253},[240,2674,2296],{"class":308},[240,2676,2299],{"class":253},[240,2678,917],{"class":867},[240,2680,1228],{"class":253},[240,2682,2683,2685,2687,2689,2691,2693],{"class":242,"line":1015},[240,2684,2438],{"class":861},[240,2686,2441],{"class":253},[240,2688,2296],{"class":308},[240,2690,2299],{"class":253},[240,2692,1561],{"class":867},[240,2694,1228],{"class":253},[240,2696,2697,2699,2701,2703,2705,2708],{"class":242,"line":1027},[240,2698,2438],{"class":861},[240,2700,2441],{"class":253},[240,2702,2296],{"class":308},[240,2704,2299],{"class":253},[240,2706,2707],{"class":867},"32",[240,2709,1228],{"class":253},[240,2711,2712,2714,2716,2718,2720,2723],{"class":242,"line":1039},[240,2713,2438],{"class":861},[240,2715,2441],{"class":253},[240,2717,2296],{"class":308},[240,2719,2299],{"class":253},[240,2721,2722],{"class":867},"48",[240,2724,1228],{"class":253},[240,2726,2727],{"class":242,"line":1061},[240,2728,2496],{"class":253},[240,2730,2731],{"class":242,"line":1083},[240,2732,1012],{"emptyLinePlaceholder":61},[240,2734,2735,2737,2739],{"class":242,"line":1106},[240,2736,2505],{"class":2343},[240,2738,2508],{"class":861},[240,2740,312],{"class":253},[240,2742,2743],{"class":242,"line":1126},[240,2744,2515],{"class":253},[136,2746,2747,2748,2751,2752,2751,2755,743,2758,2761],{},"The second example does pretty much the opposite. By jumping between ",[169,2749,2750],{},"array[0]",", ",[169,2753,2754],{},"array[16]",[169,2756,2757],{},"array[32]",[169,2759,2760],{},"array[48]",", every access maps to the same cache index and continuously evicts the previous value.",[232,2763,2765],{"className":2194,"code":2764,"language":2198,"meta":99,"style":99},"===> Total number of branch mispredictions  :          6\n===> Total number of cache hits             :          0\n===> Total number of cache accesses         :         68\n",[169,2766,2767,2772,2777],{"__ignoreMap":99},[240,2768,2769],{"class":242,"line":243},[240,2770,2771],{},"===> Total number of branch mispredictions  :          6\n",[240,2773,2774],{"class":242,"line":100},[240,2775,2776],{},"===> Total number of cache hits             :          0\n",[240,2778,2779],{"class":242,"line":107},[240,2780,2781],{},"===> Total number of cache accesses         :         68\n",[136,2783,2784],{},"The result is exactly what we'd expect from a tiny direct-mapped cache: 0 cache hits across 68 accesses. This makes the effect of spatial locality (and cache collisions) particularly obvious.",[399,2786,2787],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":99,"searchDepth":100,"depth":100,"links":2789},[2790,2791,2794,2798,2802],{"id":133,"depth":107,"text":134},{"id":733,"depth":100,"text":734,"children":2792},[2793],{"id":758,"depth":107,"text":759},{"id":805,"depth":100,"text":806,"children":2795},[2796,2797],{"id":812,"depth":107,"text":813},{"id":819,"depth":107,"text":820},{"id":826,"depth":100,"text":827,"children":2799},[2800,2801],{"id":839,"depth":107,"text":840},{"id":1160,"depth":107,"text":1161},{"id":1762,"depth":100,"text":1763,"children":2803},[2804,2805,2806],{"id":1795,"depth":107,"text":1796},{"id":1867,"depth":107,"text":1868},{"id":2274,"depth":107,"text":2275},"#8c1826","2026-08-01","Designed, developed and verified a simple Direct Mapped CPU cache for the CFU Proving Ground RISC-V implementation.","\u002Fcontent\u002Fd-map-cache-banner.jpeg",{},"RISC-V Cache",{"title":28,"description":2809},[2815,2816,2817,2818],"RISC-V","Verilog","FPGA","Architecture","egBwE_OqfcRQullVBh5mvHKl7x5njJo1M-GQsWaJG3M",{"id":2821,"title":36,"body":2822,"color":4044,"date":4045,"description":4046,"draft":21,"extension":104,"image":4047,"meta":4048,"navigation":61,"oneliner":4049,"path":37,"seo":4050,"stem":38,"tags":4051,"url":49,"__hash__":4055},"work\u002Fwork\u002Fpico-router.md",{"type":74,"value":2823,"toc":4025},[2824,2826,2829,2835,2839,2841,2843,2854,2862,2865,2869,2872,2875,2927,2933,2938,2942,2945,2959,2962,2998,3002,3011,3026,3030,3033,3046,3107,3111,3118,3123,3126,3139,3146,3158,3161,3165,3172,3194,3217,3226,3245,3745,3749,3759,3776,3787,3797,3818,3822,3825,3830,3833,3860,3867,3878,3881,3885,3888,3895,3906,3909,3938,3945,3951,3955,3962,3965,3969,3975,3980,3984,3987,4022],[131,2825,134],{"id":133},[136,2827,2828],{},"Pico Router is an open-source, deterministic C++17 pathfinding engine designed for memory-constrained embedded systems like the Raspberry Pi Pico. Originating from a GSoC project, it provides an A* routing library that runs within 64–128 KB of SRAM using compact Compressed Sparse Row graph representations.",[2830,2831,2832],"warning",{},[136,2833,2834],{},"This article is based on Pico Router v0.2.0",[656,2836,2837],{"icon":725,"target":380,"title":726,"to":58},[136,2838,730],{},[719,2840],{"exclude":721,":max-depth":722},[140,2842,734],{"id":733},[136,2844,2845,2846,2849,2850,2853],{},"Pico Router grew out of my preparations for ",[375,2847,2848],{"href":15},"GSoC 2026",". During this period I spent several months exploring routing engines and the OpenStreetMap ecosystem. After being rejected I thought ",[1168,2851,2852],{},"\"What's stopping me from making my own FOSS project?\"",". Knowing I wanted to maintain my routing\u002FGIS trajectory while also interested in embedded systems I came up with the idea for Pico Router.",[136,2855,2856,2857,2861],{},"Originally setting out to build an exhaustive routing firmware ecosystem, I later scaled back my ambitions slightly by first perfecting the routing core (read ",[1168,2858,2859],{},[375,2860,18],{"href":19}," for more details). This effectively turned Pico Router into an embedded C++ routing library.",[136,2863,2864],{},"Today, I see Pico Router as both an educational project and a genuinely useful piece of software. The goal is to build something that can stand on its own as a reusable routing library while continuing to explore what is possible on constrained hardware.",[140,2866,2868],{"id":2867},"pico-router-in-a-nutshell","Pico Router in a Nutshell",[136,2870,2871],{},"As it stands, Pico Router is a C++ routing engine library which offers deterministic and configurable memory usage. It offers optimized pathfinding capabilities using a compact internal graph representation. While it is mainly aimed at the Raspberry Pi Pico lineup (as the name suggests), I'm planning support for a myriad of other boards as well.",[136,2873,2874],{},"The bulk of the library is abstracted behind a relatively simple interface.",[232,2876,2878],{"className":234,"code":2877,"language":236,"meta":99,"style":99},"Path calculatePath(\n  const Graph& graph,\n  uint32_t start_node,\n  uint32_t goal_node\n)\n",[169,2879,2880,2891,2907,2915,2922],{"__ignoreMap":99},[240,2881,2882,2885,2888],{"class":242,"line":243},[240,2883,2884],{"class":246},"Path",[240,2886,2887],{"class":257}," calculatePath",[240,2889,2890],{"class":253},"(\n",[240,2892,2893,2896,2899,2902,2905],{"class":242,"line":100},[240,2894,2895],{"class":264},"  const",[240,2897,2898],{"class":246}," Graph",[240,2900,2901],{"class":264},"&",[240,2903,2904],{"class":268}," graph",[240,2906,877],{"class":253},[240,2908,2909,2911,2913],{"class":242,"line":107},[240,2910,328],{"class":264},[240,2912,269],{"class":268},[240,2914,877],{"class":253},[240,2916,2917,2919],{"class":242,"line":325},[240,2918,328],{"class":264},[240,2920,2921],{"class":268}," goal_node\n",[240,2923,2924],{"class":242,"line":336},[240,2925,2926],{"class":253},")\n",[136,2928,2929,2930,2932],{},"Here the returned ",[169,2931,2884],{}," struct is simply an array nodes found to be the optimal path.",[2934,2935],"video-gif",{"alt":2936,"src":2937},"high-level a* explainer gif","\u002Fcontent\u002Fpico-router-video-1.webm",[131,2939,2941],{"id":2940},"the-algorithm","The Algorithm",[136,2943,2944],{},"While Pico Router will most likely feature a handful of different pathfinding algorithms in the future, it was initially built around A*. My focus on disaster relief applications made traversable road networks a natural starting point. Since these usually form sparse directed graphs, A* seemed like a good algorithm to build on.",[136,2946,2947,2948,2953,2954,2958],{},"The positional nature of road network nodes gave me an easy starting point for the heuristic function, namely the ",[375,2949,2952],{"href":2950,"rel":2951,"target":380},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FEuclidean_distance",[379],"Euclidean distance"," function. Having this computation encapsulated gave me a great surface to experiment on using A\u002FB methods (see ",[375,2955,2957],{"href":2956},"#benchmarking-and-profiling","Benchmarking and Profiling",").",[136,2960,2961],{},"Speaking of encapsulation, early on I made the decision to abstract common graph operations behind an interface. This allowed the pathfinding algorithm to stay mostly separate from the underlying graph representation thereby giving me even more room to play around with different parts of the workload without disturbing others.",[186,2963,2964,2971],{},[136,2965,2966,2967,2970],{},"Some ",[169,2968,2969],{},"Graph"," member functions",[232,2972,2974],{"className":234,"code":2973,"language":236,"meta":99,"style":99},"getNeighbors();\ngetCoordinates();\ngetNodeCount();\n",[169,2975,2976,2984,2991],{"__ignoreMap":99},[240,2977,2978,2981],{"class":242,"line":243},[240,2979,2980],{"class":257},"getNeighbors",[240,2982,2983],{"class":253},"();\n",[240,2985,2986,2989],{"class":242,"line":100},[240,2987,2988],{"class":257},"getCoordinates",[240,2990,2983],{"class":253},[240,2992,2993,2996],{"class":242,"line":107},[240,2994,2995],{"class":257},"getNodeCount",[240,2997,2983],{"class":253},[131,2999,3001],{"id":3000},"why-c","Why C++?",[136,3003,3004,3005,3010],{},"Firstly, my choices were quickly limited to C and C++ due to their native compatibility with the official ",[375,3006,3009],{"href":3007,"rel":3008},"https:\u002F\u002Fgithub.com\u002Fraspberrypi\u002Fpico-sdk",[379],"Raspberry Pi Pico SDK",". The decision to go with C++ over C came down to the fact that C++ offered the right abstraction model I was looking for. Needless to say, I'm also more familiar with routing systems in C++ than C after my work with\u002Fon Valhalla.",[136,3012,3013,3014,3017,3018,3021,3022,3025],{},"Of course, I knew from the very start I'd have to limit myself to only a subset of language features given the constrained and delicate environment (no dynamic heap allocations and such). This need for stability and reliability was also why I went with C++17 specifically. The Pico SDK uses ",[169,3015,3016],{},"arm-none-eabi-gcc"," as its main ARM compiler, which at this point works fantastically with C++17. Moreover, missing some of the newest features has some educational perks. Lacking ",[169,3019,3020],{},"std::span"," (introduced in C++20) allowed me to roll my own contiguous pointer ",[169,3023,3024],{},"EdgeRange"," to return a node's connected neighbors.",[140,3027,3029],{"id":3028},"designing-a-router-for-the-pico","Designing a Router for the Pico",[136,3031,3032],{},"Choosing the Pi Pico as my hardware starting point was a great choice in retrospect. It offered me the opportunity to feel the very real effects of making something run on a constrained 32-bit system while still having access to the entire Pico SDK toolchain. This allowed me to focus more of my time on the code and less on the build configuration.",[136,3034,3035,3036,3041,3042,3045],{},"Up until now I've been a bit ambigious with singling out either the Pico 1 or 2. The truth of the matter is that I'm taking both into account. This also has to do with some situational limitations I'm dealing with. Namely, I bought, and therefore have physical access to, the Pi Pico 2 (RP2350). However, for the hardware simulation the only somewhat complete ",[375,3037,3040],{"href":3038,"rel":3039,"target":380},"https:\u002F\u002Fgithub.com\u002Fmatgla\u002FRenode_RP2040",[379],"platform description"," is for the Pi Pico (RP2040). Point being, my mindset was \"",[1168,3043,3044],{},"make it run on the RP2040, see what it's capable of on the RP2350","\". Notably, with offline capabilities in mind, I opted for the regular boards as opposed to the W boards featuring wireless networking capabilities.",[1801,3047,3048,3061],{},[1804,3049,3050],{},[1807,3051,3052,3055,3058],{},[1810,3053,3054],{},"Specs",[1810,3056,3057],{},"RP2040",[1810,3059,3060],{},"RP2350",[1817,3062,3063,3074,3085,3096],{},[1807,3064,3065,3068,3071],{},[1822,3066,3067],{},"CPU",[1822,3069,3070],{},"Dual-core Cortex-M0+",[1822,3072,3073],{},"Dual-core Cortex-M33 or Hazard3 RISC-V",[1807,3075,3076,3079,3082],{},[1822,3077,3078],{},"Clock",[1822,3080,3081],{},"133 MHz",[1822,3083,3084],{},"150 MHz",[1807,3086,3087,3090,3093],{},[1822,3088,3089],{},"SRAM",[1822,3091,3092],{},"264 KB",[1822,3094,3095],{},"520 KB",[1807,3097,3098,3101,3104],{},[1822,3099,3100],{},"External flash",[1822,3102,3103],{},"Up to 16 MB",[1822,3105,3106],{},"Up to 32 MB",[131,3108,3110],{"id":3109},"the-problem-and-constraints","The Problem and Constraints",[136,3112,3113,3114,3117],{},"From the very start I saw memory usage as a kind of promise to the user. When using Pico Router you will ",[220,3115,3116],{},"know"," exactly how much RAM will be dedicated to routing. Keeping this in mind, let's look at the constraints we're working with. Both microcontrollers are ARM dual-core systems with an average of ~300 KB of SRAM. Importantly, both systems offer a couple megabytes of external flash memory. This will be important for graph storage and loading later on.",[1272,3119,3120],{},[136,3121,3122],{},"The Pico 2 notably also offers RISC-V support through their dual Hazard3 cores, however, the SDK doesn't fully support running all four cores at once.",[136,3124,3125],{},"Naturally, I can't expect Pico Router to have access to all the memory all the time. I therefore set myself a baseline of approximately 64 to 128 KB of runtime memory usage. That would mean that Pico Router router would use about 35% of the total memory budget. To be very honest, these numbers are not set in stone nor do they come from an authoritative source. Still, I think having a baseline in mind will pay dividends once other people actually try using Pico Router for their own applications",[186,3127,3128,3131],{},[136,3129,3130],{},"Estimated RP2040 SRAM Budget Breakdown (264 KB Total)",[3132,3133],"chart",{":datasets":3134,":labels":3135,":type":3136,":donut":3137,":show-grid":3138,":show-tooltip":3138},"[{\"data\":[15,35,35,15],\"backgroundColor\":[\"#f9a04a\",\"#ff8100\",\"#d96f00\",\"#b85a00\"]}]","[\"System & Pico SDK\",\"User Application\",\"Pico Router Peak Overhead\",\"Safety Margin\"]","pie","true","false",[136,3140,3141,3142,3145],{},"I would like to make it clear that this memory constraint poses a ",[220,3143,3144],{},"serious"," challenge. Maybe the best way to understand how little memory Pico Router has available let's compare it with some traditional open-source server routing engines. Please note that the chart below uses a logarithmic scale. Were you to use a linear scale you'd hardly be able to see Pico Router's usage.",[186,3147,3148,3151],{},[136,3149,3150],{},"Routing engine runtime memory usage comparison",[3132,3152],{":datasets":3153,":labels":3154,":type":3155,"index-axis":3156,"x-title":3157},"[{\"label\":\"Memory Usage\",\"data\":[6,7,20,24.17,25,26.91]}]","[\"Pico Router\",\"OsmAnd\",\"GraphHopper\",\"Valhalla\",\"OSRM\"]","bar","y","log₂(memory \u002F KiB)",[136,3159,3160],{},"Besides spatial metrics, there are of course also timing metrics. Nevertheless, at this moment in time I don't enforce strict performance limits on myself as long as resource usage is under control. This does not mean I don't care about performance at all. I have my benchmarking suite to make sure I don't unknowingly impose heavy performance penalties on myself.",[131,3162,3164],{"id":3163},"the-graph-representation","The Graph Representation",[136,3166,3167,3168,3171],{},"Now that you understand the inherent challenges with respect to the available resource budget, let's take a look at what Pico Router's internal graph looks like. As of ",[169,3169,3170],{},"v0.2.0"," I've taken three-ish attempts at creating the graph data structure with each iteration improving on the previous.",[136,3173,3174,3175,2751,3177,2751,3180,3183,3184,3186,3187,3190,3191,3193],{},"The very first attempt (version 0) was by far the simplest with the only goal being to make the A* algorithm work in any form. It was composed of three separate structs (",[169,3176,2969],{},[169,3178,3179],{},"Edge",[169,3181,3182],{},"Node",") containing all the graph's state. The ",[169,3185,3182],{}," struct was notably large containing F-score, G-score, XY coordinates, and heuristic fields. On top of that, it had both an ",[169,3188,3189],{},"edge_count"," property and a hardcoded 4-index ",[169,3192,3179],{}," array as member data (I can not remember why it had this but I assume it was meant to be temporary xD).",[136,3195,3196,3197,3199,3200,3203,3204,3206,3207,3209,3210,3213,3214,3216],{},"The second try (version 1) maintained the same rough outline by keeping the three structs while changing their relations. Many of the ",[169,3198,3182],{}," fields were moved to the ",[169,3201,3202],{},"pathfind::Astar"," class and ",[169,3205,2969],{}," adopted a fixed-size parallel array structure. What's perhaps most interesting is that I tried to save significant space by making the graph traversable as a linked list. ",[169,3208,3182],{}," contains ",[169,3211,3212],{},"first_edge_index"," which points at the its first edge. Then each consecutive ",[169,3215,3179],{}," will point to the next edge in line until reaching a 'no next edge' sentinel value.",[186,3218,3219,3222],{},[136,3220,3221],{},"This animation gives a pretty good idea of how my linked list edge exploration works conceptually",[2934,3223],{"alt":3224,"src":3225},"linked edge graph explainer gif","\u002Fcontent\u002Fpico-router-video-2.mp4",[136,3227,3228,3229,3234,3235,3237,3238,3241,3242,614],{},"The third, and for now final, go at the design (version 2) increased slightly in size in exchange for a dramatic performance increase. Only after first setting up my benchmarks I started pondering the cache friendliness of my data structures. The main problem was that my linked list approach exacerbated many of the pathfinding memory access unpredictabilities found in these kinds of systems. This prompted me to give the graph a ",[375,3230,3233],{"href":3231,"rel":3232,"target":380},"https:\u002F\u002Fwww.boost.org\u002Fdoc\u002Flibs\u002F1_61_0\u002Flibs\u002Fgraph\u002Fdoc\u002Fcompressed_sparse_row.html",[379],"compressed sparse row"," adjacent design to allow for neighboring edges to be stored contiguously in memory. This is also where I introduced the graph interface, which is why the ",[169,3236,3182],{}," coordinates are now stored in a separate ",[169,3239,3240],{},"Coords"," struct. To see the performance boost I mention earlier check out ",[375,3243,2957],{"href":3244},"#bench-comparison",[3246,3247,3248,3408,3572],"code-group",{},[232,3249,3252],{"className":234,"code":3250,"filename":3251,"language":236,"meta":99,"style":99},"struct Graph {\n  uint32_t graph_id;\n};\n\nstruct Edge {\n  uint32_t edge_id;\n  uint16_t to; \n  uint16_t cost;\n};\n\nstruct Node {\n  int32_t node_id;\n  int32_t x, y;\n  int32_t g, h, f;\n  Edge edges[4];\n  uint8_t edge_count;\n};\n","Version 0",[169,3253,3254,3262,3271,3275,3279,3288,3297,3310,3319,3323,3327,3336,3346,3360,3379,3394,3404],{"__ignoreMap":99},[240,3255,3256,3258,3260],{"class":242,"line":243},[240,3257,294],{"class":264},[240,3259,2898],{"class":246},[240,3261,300],{"class":253},[240,3263,3264,3266,3269],{"class":242,"line":100},[240,3265,328],{"class":264},[240,3267,3268],{"class":308}," graph_id",[240,3270,312],{"class":253},[240,3272,3273],{"class":242,"line":107},[240,3274,339],{"class":253},[240,3276,3277],{"class":242,"line":325},[240,3278,1012],{"emptyLinePlaceholder":61},[240,3280,3281,3283,3286],{"class":242,"line":336},[240,3282,294],{"class":264},[240,3284,3285],{"class":246}," Edge",[240,3287,300],{"class":253},[240,3289,3290,3292,3295],{"class":242,"line":420},[240,3291,328],{"class":264},[240,3293,3294],{"class":308}," edge_id",[240,3296,312],{"class":253},[240,3298,3299,3302,3305,3307],{"class":242,"line":949},[240,3300,3301],{"class":264},"  uint16_t",[240,3303,3304],{"class":308}," to",[240,3306,2359],{"class":253},[240,3308,3309],{"class":861}," \n",[240,3311,3312,3314,3317],{"class":242,"line":972},[240,3313,3301],{"class":264},[240,3315,3316],{"class":308}," cost",[240,3318,312],{"class":253},[240,3320,3321],{"class":242,"line":996},[240,3322,339],{"class":253},[240,3324,3325],{"class":242,"line":1009},[240,3326,1012],{"emptyLinePlaceholder":61},[240,3328,3329,3331,3334],{"class":242,"line":1015},[240,3330,294],{"class":264},[240,3332,3333],{"class":246}," Node",[240,3335,300],{"class":253},[240,3337,3338,3341,3344],{"class":242,"line":1027},[240,3339,3340],{"class":264},"  int32_t",[240,3342,3343],{"class":308}," node_id",[240,3345,312],{"class":253},[240,3347,3348,3350,3353,3355,3358],{"class":242,"line":1039},[240,3349,3340],{"class":264},[240,3351,3352],{"class":308}," x",[240,3354,272],{"class":253},[240,3356,3357],{"class":308}," y",[240,3359,312],{"class":253},[240,3361,3362,3364,3367,3369,3372,3374,3377],{"class":242,"line":1061},[240,3363,3340],{"class":264},[240,3365,3366],{"class":308}," g",[240,3368,272],{"class":253},[240,3370,3371],{"class":308}," h",[240,3373,272],{"class":253},[240,3375,3376],{"class":308}," f",[240,3378,312],{"class":253},[240,3380,3381,3384,3387,3389,3392],{"class":242,"line":1083},[240,3382,3383],{"class":246},"  Edge",[240,3385,3386],{"class":308}," edges",[240,3388,2299],{"class":253},[240,3390,3391],{"class":867},"4",[240,3393,1228],{"class":253},[240,3395,3396,3399,3402],{"class":242,"line":1106},[240,3397,3398],{"class":264},"  uint8_t",[240,3400,3401],{"class":308}," edge_count",[240,3403,312],{"class":253},[240,3405,3406],{"class":242,"line":1126},[240,3407,339],{"class":253},[232,3409,3412],{"className":234,"code":3410,"filename":3411,"language":236,"meta":99,"style":99},"struct Edge {\n  uint16_t target;\n  uint16_t cost;\n  uint32_t next_edge_index;\n};\n\nstruct Node {\n  int32_t x = 0;\n  int32_t y = 0;\n  uint32_t first_edge_index = 0;\n};\n\nstruct Graph {\n  std::array\u003CNode, MAX_NODES> nodes{};\n  std::array\u003CEdge, MAX_EDGES> edges{};\n};\n","Version 1",[169,3413,3414,3422,3431,3439,3448,3452,3456,3464,3476,3488,3501,3505,3509,3517,3545,3568],{"__ignoreMap":99},[240,3415,3416,3418,3420],{"class":242,"line":243},[240,3417,294],{"class":264},[240,3419,3285],{"class":246},[240,3421,300],{"class":253},[240,3423,3424,3426,3429],{"class":242,"line":100},[240,3425,3301],{"class":264},[240,3427,3428],{"class":308}," target",[240,3430,312],{"class":253},[240,3432,3433,3435,3437],{"class":242,"line":107},[240,3434,3301],{"class":264},[240,3436,3316],{"class":308},[240,3438,312],{"class":253},[240,3440,3441,3443,3446],{"class":242,"line":325},[240,3442,328],{"class":264},[240,3444,3445],{"class":308}," next_edge_index",[240,3447,312],{"class":253},[240,3449,3450],{"class":242,"line":336},[240,3451,339],{"class":253},[240,3453,3454],{"class":242,"line":420},[240,3455,1012],{"emptyLinePlaceholder":61},[240,3457,3458,3460,3462],{"class":242,"line":949},[240,3459,294],{"class":264},[240,3461,3333],{"class":246},[240,3463,300],{"class":253},[240,3465,3466,3468,3470,3472,3474],{"class":242,"line":972},[240,3467,3340],{"class":264},[240,3469,3352],{"class":308},[240,3471,2391],{"class":253},[240,3473,2129],{"class":867},[240,3475,312],{"class":253},[240,3477,3478,3480,3482,3484,3486],{"class":242,"line":996},[240,3479,3340],{"class":264},[240,3481,3357],{"class":308},[240,3483,2391],{"class":253},[240,3485,2129],{"class":867},[240,3487,312],{"class":253},[240,3489,3490,3492,3495,3497,3499],{"class":242,"line":1009},[240,3491,328],{"class":264},[240,3493,3494],{"class":308}," first_edge_index",[240,3496,2391],{"class":253},[240,3498,2129],{"class":867},[240,3500,312],{"class":253},[240,3502,3503],{"class":242,"line":1015},[240,3504,339],{"class":253},[240,3506,3507],{"class":242,"line":1027},[240,3508,1012],{"emptyLinePlaceholder":61},[240,3510,3511,3513,3515],{"class":242,"line":1039},[240,3512,294],{"class":264},[240,3514,2898],{"class":246},[240,3516,300],{"class":253},[240,3518,3519,3522,3524,3527,3529,3531,3533,3536,3539,3542],{"class":242,"line":1061},[240,3520,3521],{"class":246},"  std",[240,3523,254],{"class":253},[240,3525,3526],{"class":246},"array",[240,3528,2364],{"class":253},[240,3530,3182],{"class":246},[240,3532,272],{"class":253},[240,3534,3535],{"class":246}," MAX_NODES",[240,3537,3538],{"class":253},">",[240,3540,3541],{"class":308}," nodes",[240,3543,3544],{"class":253},"{};\n",[240,3546,3547,3549,3551,3553,3555,3557,3559,3562,3564,3566],{"class":242,"line":1083},[240,3548,3521],{"class":246},[240,3550,254],{"class":253},[240,3552,3526],{"class":246},[240,3554,2364],{"class":253},[240,3556,3179],{"class":246},[240,3558,272],{"class":253},[240,3560,3561],{"class":246}," MAX_EDGES",[240,3563,3538],{"class":253},[240,3565,3386],{"class":308},[240,3567,3544],{"class":253},[240,3569,3570],{"class":242,"line":1106},[240,3571,339],{"class":253},[232,3573,3576],{"className":234,"code":3574,"filename":3575,"language":236,"meta":99,"style":99},"struct Edge {\n  uint32_t target;\n  uint32_t cost;\n};\n\nstruct Coords {\n  int32_t x;\n  int32_t y;\n};\n\nstruct Node {\n  Coords coordinates;\n  uint32_t edge_offset;\n  uint32_t edge_count;\n};\n\nstruct Graph {\n  std::array\u003CNode, MAX_NODES> nodes{};\n  std::array\u003CEdge, MAX_EDGES> edges{};\n};\n","Version 2",[169,3577,3578,3586,3594,3602,3606,3610,3619,3627,3635,3639,3643,3651,3661,3670,3678,3682,3686,3694,3717,3740],{"__ignoreMap":99},[240,3579,3580,3582,3584],{"class":242,"line":243},[240,3581,294],{"class":264},[240,3583,3285],{"class":246},[240,3585,300],{"class":253},[240,3587,3588,3590,3592],{"class":242,"line":100},[240,3589,328],{"class":264},[240,3591,3428],{"class":308},[240,3593,312],{"class":253},[240,3595,3596,3598,3600],{"class":242,"line":107},[240,3597,328],{"class":264},[240,3599,3316],{"class":308},[240,3601,312],{"class":253},[240,3603,3604],{"class":242,"line":325},[240,3605,339],{"class":253},[240,3607,3608],{"class":242,"line":336},[240,3609,1012],{"emptyLinePlaceholder":61},[240,3611,3612,3614,3617],{"class":242,"line":420},[240,3613,294],{"class":264},[240,3615,3616],{"class":246}," Coords",[240,3618,300],{"class":253},[240,3620,3621,3623,3625],{"class":242,"line":949},[240,3622,3340],{"class":264},[240,3624,3352],{"class":308},[240,3626,312],{"class":253},[240,3628,3629,3631,3633],{"class":242,"line":972},[240,3630,3340],{"class":264},[240,3632,3357],{"class":308},[240,3634,312],{"class":253},[240,3636,3637],{"class":242,"line":996},[240,3638,339],{"class":253},[240,3640,3641],{"class":242,"line":1009},[240,3642,1012],{"emptyLinePlaceholder":61},[240,3644,3645,3647,3649],{"class":242,"line":1015},[240,3646,294],{"class":264},[240,3648,3333],{"class":246},[240,3650,300],{"class":253},[240,3652,3653,3656,3659],{"class":242,"line":1027},[240,3654,3655],{"class":246},"  Coords",[240,3657,3658],{"class":308}," coordinates",[240,3660,312],{"class":253},[240,3662,3663,3665,3668],{"class":242,"line":1039},[240,3664,328],{"class":264},[240,3666,3667],{"class":308}," edge_offset",[240,3669,312],{"class":253},[240,3671,3672,3674,3676],{"class":242,"line":1061},[240,3673,328],{"class":264},[240,3675,3401],{"class":308},[240,3677,312],{"class":253},[240,3679,3680],{"class":242,"line":1083},[240,3681,339],{"class":253},[240,3683,3684],{"class":242,"line":1106},[240,3685,1012],{"emptyLinePlaceholder":61},[240,3687,3688,3690,3692],{"class":242,"line":1126},[240,3689,294],{"class":264},[240,3691,2898],{"class":246},[240,3693,300],{"class":253},[240,3695,3697,3699,3701,3703,3705,3707,3709,3711,3713,3715],{"class":242,"line":3696},18,[240,3698,3521],{"class":246},[240,3700,254],{"class":253},[240,3702,3526],{"class":246},[240,3704,2364],{"class":253},[240,3706,3182],{"class":246},[240,3708,272],{"class":253},[240,3710,3535],{"class":246},[240,3712,3538],{"class":253},[240,3714,3541],{"class":308},[240,3716,3544],{"class":253},[240,3718,3720,3722,3724,3726,3728,3730,3732,3734,3736,3738],{"class":242,"line":3719},19,[240,3721,3521],{"class":246},[240,3723,254],{"class":253},[240,3725,3526],{"class":246},[240,3727,2364],{"class":253},[240,3729,3179],{"class":246},[240,3731,272],{"class":253},[240,3733,3561],{"class":246},[240,3735,3538],{"class":253},[240,3737,3386],{"class":308},[240,3739,3544],{"class":253},[240,3741,3743],{"class":242,"line":3742},20,[240,3744,339],{"class":253},[131,3746,3748],{"id":3747},"keeping-memory-predictable","Keeping Memory Predictable",[136,3750,3751,3752,3755,3756,3758],{},"While I already briefly touched upon the fixed-arrays which make up the graph, I'd like to delve slightly deeper into my efforts to make memory deterministic. The biggest bulk of memory usage besides the graph itself is the algorithm's runtime state (think of the priority queue, open\u002Fclosed lists, etc.). Virtually all of this data once again consists of fixed-sized arrays used for different purposes. \"",[1168,3753,3754],{},"Arrays of what","?\" I hear you ask. The answer is that they all contain node ID's, which are just type alias of a ",[169,3757,265],{},". Because they all essentially serve as pointers to other node indices, they can be unified under the same type. Also, because they're just indices they'll never be negative allowing me to increase the available range using unsigned ints instead of signed ints.",[794,3760,3761],{},[136,3762,3763,3764,3767,3768,3771,3772,3775],{},"In the near future I'd like to look into evaluating the maximum amount of nodes at compile time to lower ",[169,3765,3766],{},"node_id"," to ",[169,3769,3770],{},"uint16_t"," or maybe even ",[169,3773,3774],{},"uint8_t"," for tiny graphs, saving the user a boatload of space.",[136,3777,3778,3779,3782,3783,3786],{},"The next question you might ask is \"",[1168,3780,3781],{},"Then how many items does each array have?","\". It is once again of great convenience that every array describes one aspect of the same abstract object. We just have to know what the maximum size of the graph is by the quantity of its nodes and make each array that size. While it would probably be good to at some point have another compile time evaluation of the graph size (assuming the graph doesn't change during runtime), at this point the user manually enters the max amount of nodes and edges they require via a ",[169,3784,3785],{},"config.json",". This config file gets compiled by a tiny Python script into a C++ header file and is subsequently included and used in the build as the size of the parallel arrays.",[136,3788,3789,3790,3792,3793,3796],{},"In order to get some insight into how much memory is actually being used, I took advantage of this determinism. Using a compiled C++ script that does nothing but read and pretty print the sizes of all these runtime objects including their collective sum. What's more is that using ",[169,3791,3016],{}," allows me to track the static memory usage by running ",[169,3794,3795],{},"arm-none-eabi-size"," on the compiled ELF path. This then also gets printed alongside the other objects memory numbers.",[186,3798,3799,3812],{},[136,3800,3801,3802,2751,3805,3808,3809],{},"Memory report result when ",[169,3803,3804],{},"MAX_NODES = 1000",[169,3806,3807],{},"MAX_EDGES = 4000",", and ",[169,3810,3811],{},"MAX_PATH_LENGTH = 100",[136,3813,3814],{},[194,3815],{"alt":3816,"src":3817},"memory report output screenshot","\u002Fcontent\u002Fpico-router-screenshot-3.jpeg",[140,3819,3821],{"id":3820},"making-the-first-version-work","Making the First Version Work",[136,3823,3824],{},"Now let's talk about what it actually took to get the first running version on the Pi Pico. The first big hurdle came in the form of the dev environment setup. I knew from the very start that I didn't want to task potential users and contributors with installing a laundry list of versioned dependencies to get started. This naturally led me to the creation of a devcontainer containing the toolchain.",[1272,3826,3827],{},[136,3828,3829],{},"I won't go into detail here, but setting up the multi-arch container featuring all the required dependencies and some quality of life features was a real nightmare (stayed tuned for a blog post on this process). At this point I'm working with a functional multi-stage 10 GB build with various pre\u002Fpost-install scripts which will definitely have be streamlined in the future.",[136,3831,3832],{},"I subsequently worked in this environment until fulfilling handful of checkboxes:",[3834,3835,3836,3839,3846,3853],"ol",{},[80,3837,3838],{},"Make a graph, any graph that is, as long as its compatible with the algorithm;",[80,3840,3841,3842,3845],{},"Get a functional version of an ",[169,3843,3844],{},"Astar"," class;",[80,3847,3848,3849,3852],{},"Make the ",[169,3850,3851],{},"main"," loop execute a path traversal demo on static graph fixture;",[80,3854,3855,3856,3859],{},"Print a pretty banner and some basic ",[169,3857,3858],{},"std::chrono"," benchmarks from the demo run.",[136,3861,3862,3863,3866],{},"So far, so good. Trying to actually move the code from the devcontainer to the Pico 2 without creating a giant mess prompted me to create ",[169,3864,3865],{},"platform"," directory, allowing me to separate the host and Pico hardware abstractions. I have to say that the Pico SDK really helped out here by offering headerfiles with excellent APIs for writing over USB UART and accessing the BOOTSEL button's state.",[136,3868,3869,3870,3873,3874,3877],{},"Finally, I hooked up the Pico 2 via micro-USB, flashed the demo onto it, and opened a PuTTY window and..... nothing. Turns out, debugging is pretty tough without any kind of error logging. Luckily, I could identify the problem quite quickly, being that I set the ",[169,3871,3872],{},"MAX_NODES"," and  ",[169,3875,3876],{},"MAX_EDGES"," constants way too high causing the program to allocate out-of-bounds memory and instantly killing the Pico.",[136,3879,3880],{},"With that fixed, I was able succesfully run the demo you see here below:",[2934,3882],{"alt":3883,"src":3884},"first on-device demo gif","\u002Fcontent\u002Fpico-router-video-4.webm",[140,3886,2957],{"id":3887},"benchmarking-and-profiling",[136,3889,3890,3891,3894],{},"The first appearance of any micro-benchmarking in the project were in the aforementioned version 0 demo. For this, I wrote an very rudimentary timer using ",[169,3892,3893],{},"std::chrono::steady_clock"," to loop over the main demo traversal several thousand times and return the average wall clock time.",[136,3896,3897,3898,3901,3902,3905],{},"While this was a solid start, I needed to get more insight into caching and scaling behavior. This led to me making a separate Google Benchmark target which would run on the host hardware. Using ",[169,3899,3900],{},"ifdef"," blocks in the A* path I could additionally measure custom metrics like the amount of node discovered and edges expanded. Even more interesting is how I setup a dynamic graph fixture generator to be able to test the performance of the program on various graph sizes of various densities. Here, the Mersenne Twister pseudo-random generator (",[169,3903,3904],{},"std::mt19937",") allowed me to make generations random yet deterministic using a seed int.",[136,3907,3908],{},"Of course, these benches are not absolute measures of performance, they don't even run on the target hardware. They do however help measuring relative improvements experimenting with various optimizations.",[186,3910,3911,3917,3923],{},[136,3912,3913,3914],{},"Benchmark results on Apple M4\n",[375,3915],{"id":3916},"bench-comparison",[136,3918,3919],{},[194,3920],{"alt":3921,"src":3922},"benchmarks output screenshot","\u002Fcontent\u002Fpico-router-screenshot-1.jpeg",[1272,3924,3925],{},[136,3926,3927,3928,2751,3931,3934,3935,3937],{},"In ",[169,3929,3930],{},"BM_Astar_Grid\u002F[x]\u002F[y]",[169,3932,3933],{},"x"," denotes the grid dimension, i.e. the number of nodes along each dimension. ",[169,3936,3156],{}," stands for the obstacle density. The fact that the expanded nodes and edges get shorter as the density grows means the algorithm runs out of possible paths and returns an incomplete path.",[136,3939,3940,3941,3944],{},"To measure how different iterations of the graph and algorithm stack up against each other I set up historical benchmarking. In short, I keep a checkout of each significant version in ",[169,3942,3943],{},"benchmarks\u002Fhistoric\u002Fv*"," subdirectories. Than a Python bench runner executes them and prints a neat matplotlib diagram comparing the performances. See below how the move from linked-list traversal to the CSR-like graph improved performance more than 200%.",[136,3946,3947],{},[194,3948],{"alt":3949,"src":3950},"historical benchmarks chart","\u002Fcontent\u002Fpico-router-screenshot-2.jpeg",[140,3952,3954],{"id":3953},"whats-left","What's Left?",[136,3956,3957,3958,3961],{},"My broader ambitions are to continue growing and expanding Pico Router indefinitely. I've found it to be the perfect playground to test ideas and deepen my embedded systems intuitions. What's nice about Pico Router is that it occupies a unique space between open source GIS and embedded system technologies. Once the project hits the quality threshold I'm comfortable with (marked with a ",[169,3959,3960],{},"v1.0.0"," release), I'd love to formally introduce it to the wider community. As things stand, I hope for that to happen somewhere during start to mid 2027.",[136,3963,3964],{},"I'll take things one step at a time for now, but I'd ideally like to move toward a native Zephyr RTOS integration at some point. Pico Router being somewhat of a computational black box would make for a good Zephyr thread IMO.",[131,3966,3968],{"id":3967},"using-arbitrary-osm-data","Using arbitrary OSM data",[136,3970,3971,3972,3974],{},"Moving toward an actual community introduction of the project, accesibility is top of mind for me. Making Pico Router useful as a library to anyone outside of the project would require a painless way for people to inject their own custom graphs. An easy start would definitely be integrating OSM as a data source, not least because this all started with my involvement with OSM routing engines. Needless to say, even the tiniest fully featured OSM map is completely off the table for the Pico in terms of graph size. Having already though of this, I started work on an internal Python-based CLI tool called ",[169,3973,171],{}," which would allow users to convert arbitrary geo bounding boxes into Pico Router graphs. However, I have temporarily paused work on that to focus on the routing internals. All in all, this is for sure something that is left to be continued.",[1272,3976,3977],{},[136,3978,3979],{},"Despite being less familiar with it, I'd also love to explore non-road network graphs. I'd for instance see Pico Router potentially being useful for robotics or something of those sorts.",[131,3981,3983],{"id":3982},"flash-storage-and-tile-caching","Flash Storage and Tile Caching",[136,3985,3986],{},"A separate yet equally important hurdle to overcome is the storage and retrieval of large-sized graphs. For my current purposes I've been able to get away stack-allocations only, which is obviously not viable for production. Considering the speed of SRAM, I'd consider placing medium sized graphs on the heap, large graphs in flash memory, and giant graphs in persistent SD storage. Sounds simple in principle but a smooth execution would be tricky (although Zephyr unified HAL APIs would make portability much easier).",[3988,3989,3990],"accordion",{},[3991,3992,3994,4001,4004,4019],"accordion-item",{"label":3993},"If you're interested, here's a deep-dive into some architectural ideas for multi-tiered graph storage.",[136,3995,3996,3997,4000],{},"Perhaps easiest of all is the allocation of medium-sized graphs on the heap. I'm thinking returning a simple ",[169,3998,3999],{},"std::unique_ptr"," will only fragment the graph into a few chunks and it will nicely clean up after itself of course.",[136,4002,4003],{},"Large graphs in Pico's External XIP flash memory would sadly no longer enjoy single clock cycle SRAM loads. Instead, they would have to pass through a 2-way set-associative cache first. To minimize the performance penalty involved here I'd like to implement cacheline-sized graph tiles. Those tiles would for now be arranged in a flat array, although a hierarchical setup would eventually be very interesting as well. Then, to encourage cache hits, the tiles are rearranged either at compile time or during bootup using Morton's Z-order curve to increase the likelhood of cache hits by placing geographically near coordinates closer together in memory.",[186,4005,4006,4013],{},[136,4007,4008,4009,172],{},"Visualization of Morton's Z-order space-filling curve (",[375,4010,381],{"href":4011,"rel":4012,"target":380},"https:\u002F\u002Ftex.stackexchange.com\u002Fquestions\u002F347601\u002Ftikz-lebesgue-curve-z-curve",[379],[136,4014,4015],{},[194,4016],{"alt":4017,"src":4018},"z-order curve explainer gif","\u002Fcontent\u002Fpico-router-video-3.gif",[136,4020,4021],{},"Lastly, loading a giant graph from SD storage would have to be done very carefully to not keep the CPU waiting for hundreds of cycles. Perhaps at that point I could subdivide the graph once more in even larger chunks and implement a thin layer of software-based caching. For now that's just food for thought though.",[399,4023,4024],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":99,"searchDepth":100,"depth":100,"links":4026},[4027,4028,4029,4033,4038,4039,4040],{"id":133,"depth":107,"text":134},{"id":733,"depth":100,"text":734},{"id":2867,"depth":100,"text":2868,"children":4030},[4031,4032],{"id":2940,"depth":107,"text":2941},{"id":3000,"depth":107,"text":3001},{"id":3028,"depth":100,"text":3029,"children":4034},[4035,4036,4037],{"id":3109,"depth":107,"text":3110},{"id":3163,"depth":107,"text":3164},{"id":3747,"depth":107,"text":3748},{"id":3820,"depth":100,"text":3821},{"id":3887,"depth":100,"text":2957},{"id":3953,"depth":100,"text":3954,"children":4041},[4042,4043],{"id":3967,"depth":107,"text":3968},{"id":3982,"depth":107,"text":3983},"#FF4B84","2026-05-01","Creator and maintainer of a deterministic pathfinding firmware for embedded hardware. Made for predictable performance and minimal memory overhead on microcontrollers without requiring wireless connectivity.","\u002Fcontent\u002Fpico-router-banner.png",{},"Navigation Firmware",{"title":36,"description":4046},[4052,4053,513,4054],"Embedded Systems","C++","Python","_0VmfGZ8Lx92pgjE95cJqtqvJxjxze4n1Krimd_DAAU",{"id":4057,"title":40,"body":4058,"color":4763,"date":4764,"description":4765,"draft":21,"extension":104,"image":4766,"meta":4767,"navigation":61,"oneliner":4768,"path":41,"seo":4769,"stem":42,"tags":4770,"url":49,"__hash__":4774},"work\u002Fwork\u002Fvalhalla.md",{"type":74,"value":4059,"toc":4753},[4060,4062,4065,4067,4069,4082,4750],[140,4061,134],{"id":133},[136,4063,4064],{},"Contributed to Valhalla, an OSM routing engine, while preparing my GSoC 2026 application. Started with a small Docker improvement before moving into OpenLR Python bindings and the wider Python binding architecture. Along the way, I participated in upstream design discussions, reviewed changes, investigated CI failures, and learned how to contribute to an established FOSS project.",[719,4066],{"exclude":721,":max-depth":1319},[140,4068,734],{"id":733},[136,4070,4071,4072,4077,4078,4081],{},"During GSoC 2026 I applied for a ",[375,4073,4076],{"href":4074,"rel":4075,"target":380},"https:\u002F\u002Fwiki.openstreetmap.org\u002Fwiki\u002FGoogle_Summer_of_Code\u002F2026\u002FProject_ideas#Routing",[379],"project suggestion"," by OSM member Simon Poole. The gist was to integrate an existing platform (",[169,4079,4080],{},"closures.osm.ch",") more closely with one of the OSM routing engines. With the project already using Valhalla as its main routing engine, it made it the obvious place to start. Although Valhalla wasn't the target project of my GSoC application, I decided to contribute upstream alongside my preparations. I did this partly to become familiar with the codebase, partly to contribute something useful, and partly to prove to myself that I could actually navigate an established FOSS project rather than just build things in isolation.",[4083,4084,4085,4089,4097,4115,4121,4231,4237,4256,4260,4263,4266,4273,4276,4292,4296,4302,4305,4312,4315,4355,4358,4381,4385,4388,4391,4427,4430,4433,4437,4444,4450,4457,4727],"steps",{"level":1319},[140,4086,4088],{"id":4087},"docker-version-logging","Docker Version Logging",[136,4090,4091,4092,4096],{},"My first contribution came in the form of a 6-line patch. Looking back it was nothing much, at the time however, following the then-unfamiliar FOSS ettiquette was quite stressful. The patch was an enhancement to an earlier ",[4093,4094,4095],"badge",{},"good-first-issue"," regarding a quality of life feature during the Docker container startup. The idea being that background a daemon that can be queried through a CLI which can return the running Valhalla version.",[232,4098,4102],{"className":4099,"code":4100,"language":4101,"meta":99,"style":99},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","valhalla_service --version # prints \"x.x.x-commit_hash\"\n","bash",[169,4103,4104],{"__ignoreMap":99},[240,4105,4106,4109,4112],{"class":242,"line":243},[240,4107,4108],{"class":246},"valhalla_service",[240,4110,4111],{"class":2008}," --version",[240,4113,4114],{"class":1899}," # prints \"x.x.x-commit_hash\"\n",[136,4116,4117,4118,914],{},"My patch added the following lines to the start of ",[169,4119,4120],{},"docker\u002Fscripts\u002Fdocker-entrypoint.sh",[232,4122,4124],{"className":4099,"code":4123,"language":4101,"meta":99,"style":99},"# logging valhalla version\nVALHALLA_VERSION=$(valhalla_service --version | tr -d '\\n')\n\necho \"\"\necho \"====================\"\necho \"= Valhalla version =\"\necho \"====================\"\necho \"Valhalla version: ${VALHALLA_VERSION}\"\necho \"\"\n",[169,4125,4126,4131,4162,4166,4174,4187,4198,4208,4225],{"__ignoreMap":99},[240,4127,4128],{"class":242,"line":243},[240,4129,4130],{"class":1899},"# logging valhalla version\n",[240,4132,4133,4136,4139,4141,4143,4146,4149,4152,4155,4157,4160],{"class":242,"line":100},[240,4134,4135],{"class":308},"VALHALLA_VERSION",[240,4137,4138],{"class":253},"=$(",[240,4140,4108],{"class":246},[240,4142,4111],{"class":2008},[240,4144,4145],{"class":253}," |",[240,4147,4148],{"class":246}," tr",[240,4150,4151],{"class":2008}," -d",[240,4153,4154],{"class":253}," '",[240,4156,2012],{"class":2008},[240,4158,4159],{"class":253},"'",[240,4161,2926],{"class":253},[240,4163,4164],{"class":242,"line":107},[240,4165,1012],{"emptyLinePlaceholder":61},[240,4167,4168,4171],{"class":242,"line":325},[240,4169,4170],{"class":257},"echo",[240,4172,4173],{"class":253}," \"\"\n",[240,4175,4176,4178,4181,4184],{"class":242,"line":336},[240,4177,4170],{"class":257},[240,4179,4180],{"class":253}," \"",[240,4182,4183],{"class":2008},"====================",[240,4185,4186],{"class":253},"\"\n",[240,4188,4189,4191,4193,4196],{"class":242,"line":420},[240,4190,4170],{"class":257},[240,4192,4180],{"class":253},[240,4194,4195],{"class":2008},"= Valhalla version =",[240,4197,4186],{"class":253},[240,4199,4200,4202,4204,4206],{"class":242,"line":949},[240,4201,4170],{"class":257},[240,4203,4180],{"class":253},[240,4205,4183],{"class":2008},[240,4207,4186],{"class":253},[240,4209,4210,4212,4214,4217,4220,4222],{"class":242,"line":972},[240,4211,4170],{"class":257},[240,4213,4180],{"class":253},[240,4215,4216],{"class":2008},"Valhalla version: ",[240,4218,4219],{"class":253},"${",[240,4221,4135],{"class":308},[240,4223,4224],{"class":253},"}\"\n",[240,4226,4227,4229],{"class":242,"line":996},[240,4228,4170],{"class":257},[240,4230,4173],{"class":253},[136,4232,4233,4234,223],{},"I know, spectacular. I nonetheless, got some feedback from the maintainers pointing out that repeating \"Valhalla\" in the heading and the body would make the file slightly less \"greppable\". This gave me the opportunity to edit my work and follow up on my submission. After deleting the heading altogether the PR was approved and merged into the master branch for release in ",[169,4235,4236],{},"3.6.3",[4238,4239,4240,4248],"card-group",{},[656,4241,4245],{"icon":4242,"title":4243,"to":4244,"target":380},"octicon:issue-opened-16","Issue #5721","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fissues\u002F5721",[136,4246,4247],{},"Include Valhalla version in Docker container startup logs",[656,4249,4253],{"icon":4250,"title":4251,"to":4252,"target":380},"boxicons:git-pull-request","Pull Request #5884","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F5884",[136,4254,4255],{},"docker-entrypoint: display Valhalla version on container start",[140,4257,4259],{"id":4258},"gsoc-proposal","GSoC Proposal",[136,4261,4262],{},"As mentioned before, Valhalla played a crucial role in my GSoC proposal despite it not being the target project. In short, my idea involved creating a containerized sidecar service to run alongside Valhalla which would fetch, decode, and inject live traffic data into Valhalla's live traffic layer. Having scoured previous issues and discussions on the topic and having a rough proposal in mind I decided to float it to the maintainers on the GitHub discussions tab.",[136,4264,4265],{},"To my relief, the maintainers reacted cordially, asking genuine technical questions and pointing out important constraints. Importantly however, they mentioned that they like to see GSoC candidates reason independently, suggesting I should elaborate my proposal without expecting too much of their input. Honestly, the whole thing was quite humbling in a good way. I can say it shed a positive light on the FOSS community to me. Where else do domain experts give you truly engaging feedback for free? All in all, I got what I needed which allowed me to continue the preparations.",[136,4267,4268,4269,4272],{},"From a technical perspective, the pipeline contained in the proposed ",[169,4270,4271],{},"closure-sync"," sidecar service would: ingest live traffic data, decode the provider-specific feed into OpenLR location references, resolve those references against Valhalla’s OSM-derived graph, and inject the resulting speed\u002Fclosure information into Valhalla’s live-traffic layer. My goal was to keep the traffic ingestion and decoding logic isolated from the routing engine itself while providing Valhalla with continuously updated, graph-referenced traffic data.",[136,4274,4275],{},"My blog post linked below contains a link to the full GSoC proposal.",[4238,4277,4278,4285],{},[656,4279,4282],{"icon":4280,"title":4281,"to":15},"tabler:article","Blog Post",[136,4283,4284],{},"GSoC 2026: A Hopeful Rejection",[656,4286,4289],{"icon":4287,"title":4288,"to":675,"target":380},"octicon:comment-discussion-16","Discussion #5944",[136,4290,4291],{},"Native Dynamic Road Closure Support",[140,4293,4295],{"id":4294},"python-openlr-module","Python OpenLR Module",[136,4297,4298,4299,2958],{},"After the GSoC rejection, I had steadily grown familiar with the OpenLR format. Wanting to keep contributing to the project, I decided to go through with one of my GSoC milestones and propose an OpenLR module for the Python API. The main motivation was not work against the depedency hell that is the Python ecosystem, and expose the internal encoding\u002Fdecoding OpenLR functionality already present in Valhalla (",[169,4300,4301],{},"baldr\u002Fopenlr.h",[136,4303,4304],{},"First I brought up the idea in a separate \"ideas\" discussion. Once again, I got very insightful feedback, going back and forth on architectural considerations and how the whole thing should fit into the existing Python bindings. With the idea validated, I opened an issue for continuity sake and started work on my PR.",[136,4306,4307,4308,4311],{},"The bindings would live in ",[169,4309,4310],{},"src\u002Fbindings\u002Fpython\u002Fsrc\u002Fopenlr.cc"," and would expose the OpenLR class and its member data and functions. The entire thing was done with nanobind as was the pre-established Valhalla standard for Python bindings.",[136,4313,4314],{},"Some notable bindings include:",[77,4316,4317,4333,4342],{},[80,4318,4319,743,4322,4325,4326,743,4329,4332],{},[169,4320,4321],{},"to_base64",[169,4323,4324],{},"to_binary"," encoding functions (returning ",[169,4327,4328],{},"str",[169,4330,4331],{},"bytes"," respectively)",[80,4334,4335,743,4338,4341],{},[169,4336,4337],{},"LocationReferencePoint",[169,4339,4340],{},"PointLL"," data structures",[80,4343,4344,4347,4348,3808,4351,4354],{},[169,4345,4346],{},"SideOfTheRoad"," ",[169,4349,4350],{},"Orientation",[169,4352,4353],{},"FormOfWay"," enums",[136,4356,4357],{},"While the PR was conceptually approved, it sparked a discussion on a potential redo of the internal Python binding structure as a whole. Because of this the PR was left unmerged and the focus was moved to the breaking redesign of the Python bindings. At this point, I will get back to the PR and adapt the OpenLR bindings to the newly established structure.",[4238,4359,4360,4367,4374],{},[656,4361,4364],{"icon":4287,"title":4362,"to":4363,"target":380},"Discussion #6035","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fdiscussions\u002F6035",[136,4365,4366],{},"Python bindings for OpenLR",[656,4368,4371],{"icon":4242,"title":4369,"to":4370,"target":380},"Issue #6037","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fissues\u002F6037",[136,4372,4373],{},"OpenLR Python bindings",[656,4375,4378],{"icon":4250,"title":4376,"to":4377,"target":380},"Pull Request #6087","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F6087",[136,4379,4380],{},"feat: openlr python bindings",[140,4382,4384],{"id":4383},"python-binding-restructuring","Python Binding Restructuring",[136,4386,4387],{},"After being encouraged by the maintainers, I started the conversation on what the Python binding rework could look like. The main problem I identified was that the Python bindings weren't thought out from the start to accommodate increasingly more utilities being exposed through the API. Every new addition therefore felt bolted on and implementations started to diverge and feel disjointed.",[136,4389,4390],{},"My proposed solution was to unify the bindings extension approach by grouping them by their corresponding C++ namespaces.",[232,4392,4395],{"className":2194,"code":4393,"filename":4394,"language":2198,"meta":99,"style":99},".  \n├── midgard\n│ ├── PointLL  \n│ ├── Heading  \n│ ├── Curvature  \n│ └── ...   \n","Example: Midgard Python module exposes bindings to the data structures as organized in the wider repository",[169,4396,4397,4402,4407,4412,4417,4422],{"__ignoreMap":99},[240,4398,4399],{"class":242,"line":243},[240,4400,4401],{},".  \n",[240,4403,4404],{"class":242,"line":100},[240,4405,4406],{},"├── midgard\n",[240,4408,4409],{"class":242,"line":107},[240,4410,4411],{},"│ ├── PointLL  \n",[240,4413,4414],{"class":242,"line":325},[240,4415,4416],{},"│ ├── Heading  \n",[240,4418,4419],{"class":242,"line":336},[240,4420,4421],{},"│ ├── Curvature  \n",[240,4423,4424],{"class":242,"line":420},[240,4425,4426],{},"│ └── ...\n",[136,4428,4429],{},"My proposal was generally well received but led to some interesting discussions as to what extent the Valhalla bindings should mirror the C++ structure. I argued that bindings are an opportunity to provide useful language-specific abstractions, rather than necessarily exposing every operation exactly as it exists internally in C++. More specifically, I defended convenience functions where the underlying C++ operation is cumbersome to express from Python. The maintainer initially preferred keeping the bindings almost entirely aligned with the C++ API to minimize maintenance and conceptual overhead. After discussing the trade-offs however, they reconsidered and agreed that the API should be consistent in what it exposes rather than having some functionality arbitrarily available only through the bindings.",[136,4431,4432],{},"During the actual PR review I was able to point out some minor improvements, and after approval from another org member the PR was merged.",[131,4434,4436],{"id":4435},"segfault-bug-identification","Segfault Bug Identification",[136,4438,4439,4440,4443],{},"During the review I also noticed that the new Python test suite was causing process-killing segfaults on my macOS ARM64 setup, particularly around the ",[169,4441,4442],{},"test_get_tile_ids_from_ring_*"," tests. Initially I wasn't sure whether this was related to an mmap\u002FBUS errors the maintainers had already been dealing with. Just to make sure I wasn't falsely attributing the errors to the PR I kept investigating.",[136,4445,4446,4447,223],{},"Eventually I found that the segfaults were already acknowledged elsewhere in the project, meaning they weren't caused by the restructuring itself. Rather than blocking the PR, I suggested adding platform-specific guardrails to prevent the tests from killing the entire Python process. More importantly, I checked the OSX CI logs and found that the exact same failure was happening there. The Python test process was silently terminating halfway through the test suite, but the CI job wasn't detecting the failure because the test output was piped through ",[169,4448,4449],{},"tee",[136,4451,4452,4453,4456],{},"I also found a separate Python related ",[169,4454,4455],{},"KeyError"," error in the CI. This turned out to expose a broader CI problem, namely the Python tests were not reliably surfacing process-level crashes on OSX. The maintainer subsequently acknowledged the issue and opened a separate issue to address Python test execution in CI.",[4458,4459,4460,4467],"collapsible",{},[136,4461,4462],{},[1168,4463,4464],{},[220,4465,4466],{},"VictorYanson commented on Jun 19",[438,4468,4469,4480,4508,4516,4724],{},[136,4470,4471,4472,4475,4476,4479],{},"Ok so after investigating I couldn’t find out why the segfault were happening in the first place, however I did find them in the CI logs. Look at line 1205 of the ",[169,4473,4474],{},"Run Tests"," job in >the ",[169,4477,4478],{},"Build OSX & Python wheels"," check. It starts running all the tests and quits silently half way through right in the same spot as on my local system.",[232,4481,4486],{"className":4482,"code":4483,"language":4484,"meta":4485,"style":99},"language-plain shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","2026-06-14T19:32:46.5585100Z test_get_tile_ids_from_bbox (test_graph_utils.TestBindings.test_get_tile_ids_from_bbox) ... ok\n2026-06-14T19:32:46.5586200Z test_get_tile_ids_from_ring_auto_close (test_graph_utils.TestBindings.test_get_tile_ids_from_ring_auto_close)\n2026-06-14T19:32:46.5586790Z Open ring (last != first) should be auto-closed. ... [ 34%] Built target gurka_closure_penalty\n2026-06-14T19:32:46.5587250Z [ 34%] Built target run-python_valhalla\n","plain","text",[169,4487,4488,4493,4498,4503],{"__ignoreMap":99},[240,4489,4490],{"class":242,"line":243},[240,4491,4492],{},"2026-06-14T19:32:46.5585100Z test_get_tile_ids_from_bbox (test_graph_utils.TestBindings.test_get_tile_ids_from_bbox) ... ok\n",[240,4494,4495],{"class":242,"line":100},[240,4496,4497],{},"2026-06-14T19:32:46.5586200Z test_get_tile_ids_from_ring_auto_close (test_graph_utils.TestBindings.test_get_tile_ids_from_ring_auto_close)\n",[240,4499,4500],{"class":242,"line":107},[240,4501,4502],{},"2026-06-14T19:32:46.5586790Z Open ring (last != first) should be auto-closed. ... [ 34%] Built target gurka_closure_penalty\n",[240,4504,4505],{"class":242,"line":325},[240,4506,4507],{},"2026-06-14T19:32:46.5587250Z [ 34%] Built target run-python_valhalla\n",[136,4509,4510,4511,4515],{},"I guess because the test runner pipes the output through ",[375,4512,4449],{"href":4513,"rel":4514},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fblob\u002F8315d324f8d179b6477c3152e994e134a54df50f\u002Ftest\u002FCMakeLists.txt#L480",[379]," the CI only >checks if tee succeeded while completely missing the fact that python crashed.",[4517,4518,4519,4523,4526,4695,4704],"details",{},[4520,4521,4522],"summary",{},"I also spotted a separate python key error locally during the config load which is reflected in the CI logs as well.",[136,4524,4525],{},"Local:",[232,4527,4529],{"className":4099,"code":4528,"language":4101,"meta":99,"style":99},"======================================================================\nERROR: test_change_config (test_actor.TestBindings.test_change_config)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"...\u002Ftest_actor.py\", line 186, in test_change_config\n   config = get_config(self.extract_path, self.tiles_path)\n File \"...\u002Fvalhalla\u002Fconfig.py\", line 57, in get_config\n   config[\"mjolnir\"][\"logging\"][\"type\"] = \"std_out\" if verbose else \"\"\nKeyError: 'logging'\n",[169,4530,4531,4536,4547,4552,4572,4598,4618,4641,4682],{"__ignoreMap":99},[240,4532,4533],{"class":242,"line":243},[240,4534,4535],{"class":2008},"======================================================================\n",[240,4537,4538,4541,4544],{"class":242,"line":100},[240,4539,4540],{"class":246},"ERROR:",[240,4542,4543],{"class":2008}," test_change_config",[240,4545,4546],{"class":308}," (test_actor.TestBindings.test_change_config)\n",[240,4548,4549],{"class":242,"line":107},[240,4550,4551],{"class":246},"----------------------------------------------------------------------\n",[240,4553,4554,4557,4560,4563,4566,4569],{"class":242,"line":325},[240,4555,4556],{"class":246},"Traceback",[240,4558,4559],{"class":308}," (most ",[240,4561,4562],{"class":2008},"recent",[240,4564,4565],{"class":2008}," call",[240,4567,4568],{"class":2008}," last",[240,4570,4571],{"class":308},"):\n",[240,4573,4574,4577,4579,4582,4584,4586,4589,4592,4595],{"class":242,"line":336},[240,4575,4576],{"class":246}," File",[240,4578,4180],{"class":253},[240,4580,4581],{"class":2008},"...\u002Ftest_actor.py",[240,4583,2009],{"class":253},[240,4585,272],{"class":2008},[240,4587,4588],{"class":2008}," line",[240,4590,4591],{"class":2008}," 186,",[240,4593,4594],{"class":2008}," in",[240,4596,4597],{"class":2008}," test_change_config\n",[240,4599,4600,4603,4605,4608,4610,4613,4616],{"class":242,"line":420},[240,4601,4602],{"class":246},"   config",[240,4604,2391],{"class":2008},[240,4606,4607],{"class":2008}," get_config",[240,4609,261],{"class":253},[240,4611,4612],{"class":246},"self.extract_path,",[240,4614,4615],{"class":2008}," self.tiles_path",[240,4617,2926],{"class":253},[240,4619,4620,4622,4624,4627,4629,4631,4633,4636,4638],{"class":242,"line":949},[240,4621,4576],{"class":246},[240,4623,4180],{"class":253},[240,4625,4626],{"class":2008},"...\u002Fvalhalla\u002Fconfig.py",[240,4628,2009],{"class":253},[240,4630,272],{"class":2008},[240,4632,4588],{"class":2008},[240,4634,4635],{"class":2008}," 57,",[240,4637,4594],{"class":2008},[240,4639,4640],{"class":2008}," get_config\n",[240,4642,4643,4646,4649,4652,4655,4657,4660,4662,4664,4666,4669,4671,4674,4677,4680],{"class":242,"line":972},[240,4644,4645],{"class":246},"   config[",[240,4647,4648],{"class":246},"\"mjolnir\"",[240,4650,4651],{"class":246},"][",[240,4653,4654],{"class":246},"\"logging\"",[240,4656,4651],{"class":246},[240,4658,4659],{"class":246},"\"type\"",[240,4661,2388],{"class":246},[240,4663,2391],{"class":2008},[240,4665,4180],{"class":253},[240,4667,4668],{"class":2008},"std_out",[240,4670,2009],{"class":253},[240,4672,4673],{"class":2008}," if",[240,4675,4676],{"class":2008}," verbose",[240,4678,4679],{"class":2008}," else",[240,4681,4173],{"class":253},[240,4683,4684,4687,4689,4692],{"class":242,"line":996},[240,4685,4686],{"class":246},"KeyError:",[240,4688,4154],{"class":253},[240,4690,4691],{"class":2008},"logging",[240,4693,4694],{"class":253},"'\n",[136,4696,4697,4698,4700,4701,4703],{},"CI (lines 1149-1151 in ",[169,4699,4478],{}," > ",[169,4702,4474],{},"):",[232,4705,4707],{"className":4482,"code":4706,"language":4484,"meta":4485,"style":99},"2026-06-14T19:32:46.0710400Z test_change_config (test_actor.TestBindings.test_change_config) ... ERROR\n2026-06-14T19:32:46.0810630Z test_config (test_actor.TestBindings.test_config) ... ERROR\n2026-06-14T19:32:46.0912660Z test_config_actor (test_actor.TestBindings.test_config_actor) ... ERROR\n",[169,4708,4709,4714,4719],{"__ignoreMap":99},[240,4710,4711],{"class":242,"line":243},[240,4712,4713],{},"2026-06-14T19:32:46.0710400Z test_change_config (test_actor.TestBindings.test_change_config) ... ERROR\n",[240,4715,4716],{"class":242,"line":100},[240,4717,4718],{},"2026-06-14T19:32:46.0810630Z test_config (test_actor.TestBindings.test_config) ... ERROR\n",[240,4720,4721],{"class":242,"line":107},[240,4722,4723],{},"2026-06-14T19:32:46.0912660Z test_config_actor (test_actor.TestBindings.test_config_actor) ... ERROR\n",[136,4725,4726],{},"Anyways, the code changes in last commit look totally fine to me. I’ll mark it for approval!",[4238,4728,4729,4736,4743],{},[656,4730,4733],{"icon":4287,"title":4731,"to":4732,"target":380},"Discussion #6069","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fdiscussions\u002F6096",[136,4734,4735],{},"re-design python binding structure",[656,4737,4740],{"icon":4250,"title":4738,"to":4739,"target":380},"Pull Request #6133","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F6133",[136,4741,4742],{},"BREAKING(python): change layout of bindings, no signature change",[656,4744,4747],{"icon":4250,"title":4745,"to":4746,"target":380},"Pull Request #6179","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F6179",[136,4748,4749],{},"fix(release): 3.8.0 didn't land on pypi",[399,4751,4752],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}",{"title":99,"searchDepth":100,"depth":100,"links":4754},[4755,4756,4757,4758,4759,4760],{"id":133,"depth":100,"text":134},{"id":733,"depth":100,"text":734},{"id":4087,"depth":100,"text":4088},{"id":4258,"depth":100,"text":4259},{"id":4294,"depth":100,"text":4295},{"id":4383,"depth":100,"text":4384,"children":4761},[4762],{"id":4435,"depth":107,"text":4436},"#64748B","2026-02-01","Contributor to Valhalla, a high-performance routing engine built on OpenStreetMap data. My work focused on Python interoperability, OpenLR support, developer experience, and infrastructure improvements.","\u002Fcontent\u002Fvalhalla-banner.png",{},"OSM Routing Engine",{"title":40,"description":4765},[4771,4772,4773,4053,4054],"OSM","GIS","Routing Engine","K8w0XE83IFjxe9YpAdeihbSYomrrGh8QYfnrlVSkZos",{"id":4776,"title":32,"body":4777,"color":6124,"date":6125,"description":6126,"draft":21,"extension":104,"image":6127,"meta":6128,"navigation":61,"oneliner":6129,"path":33,"seo":6130,"stem":34,"tags":6131,"url":49,"__hash__":6136},"work\u002Fwork\u002Ffocal-grid.md",{"type":74,"value":4778,"toc":6113},[4779,4781,4784,4787,4791,4799,4801,4804,4808,4816,4819,4822,4825,4829,4833,4840,4854,4857,4860,4863,4867,4870,4873,5074,5077,5081,5100,5103,5571,5577,5646,5649,5652,5667,5678,5681,5769,5772,5838,5852,5862,5973,5976,6050,6059,6110],[131,4780,134],{"id":133},[136,4782,4783],{},"Built a full-stack GIS platform for visualizing Dutch energy grid data using Nuxt, Mapbox, Supabase, and PostGIS. Implemented a GeoJSON ingestion pipeline, custom map interactions, and a performant vector tile backend that serves geospatial data directly from PostGIS while keeping Mapbox purely as the rendering engine.",[719,4785],{"exclude":4786,":max-depth":3391},"Table of Contents, TL;DR, Mini Demo",[140,4788,4790],{"id":4789},"mini-demo","Mini Demo",[186,4792,4793],{},[136,4794,4795],{},[194,4796],{"alt":4797,"src":4798},"Focal Grid Demo GIF","content\u002Ffocal-grid-demo.gif",[140,4800,734],{"id":733},[136,4802,4803],{},"While working full-time on freelance full-stack projects I got in contact with a client looking to build a GIS-based visualisation platform. The pitch was straightfoward, in service of energy grid buildout in the Netherlands many private providers are looking to efficiently aggregate data in order make decisions on planning, contracts, budgets, etc. The goal was to build a minimum viable web-based prototype that demonstrates the basic desired visualisation and interaction funcionallity to show to stakeholders. Taking on the challenge as a solo developer, this was one of my most impactful freelance projects seeing as it steered me to GIS engineering for the very first time.",[140,4805,4807],{"id":4806},"tech-stack","Tech Stack",[186,4809,4810],{},[136,4811,4812],{},[194,4813],{"alt":4814,"src":4815},"Focal Grid Architecture Overview","content\u002Ffocal-grid-architecture.jpg",[136,4817,4818],{},"The application follows a fairly straightforward full-stack architecture. The frontend is built with Nuxt and Vue, which pair nicely with the Mapbox GL JS SDK while also giving me a framework I'm comfortable moving quickly in. Since almost all application data is user-specific and fetched on demand, there was little reason to lean on SSR beyond the initial page shell.",[136,4820,4821],{},"On the backend I used Supabase as the primary BaaS solution. Besides hosting a PostGIS-enabled PostgreSQL database, it also provided authentication and Edge Functions for serving vector tiles directly from the database. This let Mapbox handle rendering while keeping the actual geospatial data entirely outside of the Mapbox ecosystem.",[136,4823,4824],{},"The project itself was deployed on Vercel, with TypeScript used throughout both the frontend and server code. Finally, GSAP was sprinkled in for a handful of subtle interface animations, mostly around menus and transitions without becoming a central part of the application.",[140,4826,4828],{"id":4827},"key-features","Key Features",[131,4830,4832],{"id":4831},"custom-ui-map-interactions","Custom UI & Map Interactions",[136,4834,4835,4836,4839],{},"Perhaps this might be the easiest part to grasp intuitively seeing the ",[375,4837,4790],{"href":4838},"#mini-demo"," above already reveals most of the built functionality. Still, it might be good to list out the available features after concluding my work. The app largely consists of, number one, a interactive map render component, and two, a header component offering different control options. Controls offered by the popover menu include:",[77,4841,4842,4845,4848,4851],{},[80,4843,4844],{},"Toggling different energy grid layers on and off which were seperated by their voltage;",[80,4846,4847],{},"Toggling energy substations connected to each kind of voltage level;",[80,4849,4850],{},"On-map polygon drawing and deletion for grouping data points geographically;",[80,4852,4853],{},"Drag-and-drop icons for projecting energy asset placement;",[136,4855,4856],{},"Furthermore, the top bar additionally includes simple sattelite view and pitched angle toggle buttons.",[136,4858,4859],{},"When talking about the map itself, the user is able to zoom and inspect a limited geographical area (in this case Amersfoort in the Netherlands) as you might expect from any other map-based web application. On top of that, user can click on substations to dynamically load and view the stations metadata.",[136,4861,4862],{},"From a technical standpoint, most of the interaction logic lives in small Vue composables that bridge UI state to Mapbox through some SDK-provided hooks. The trickiest part for me was definetily keeping all of this stable across style reloads. Switching between the custom basemap and satellite view tears down and rebuilds Mapbox layers. This forced me to add special hooks to re-sync restore connection layers and persist drawn points in memory before re-injecting them after the style loads.",[131,4864,4866],{"id":4865},"geojson-to-postgis-pipeline","GeoJSON to PostGIS Pipeline",[136,4868,4869],{},"The data source for the energy grid related data was open data provided by the municipality of Amersfoort. At the time, only data in GeoJSON format was available. This meant that I needed to find a reliable way in which import this data directly into a Postgres table in Supabase while maintaining the usability of the data.",[136,4871,4872],{},"I landed on the following helper function. Provided with a local file path, desired table name and Supabase client object it automatically loads the GeoJSON into a PostGIS compatible Supabase table.",[4874,4875,4876],"code-collapse",{},[232,4877,4882],{"className":4878,"code":4879,"filename":4880,"language":4881,"meta":99,"style":99},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","async function importGeoJSON(\n  filePath, \n  tableName, \n  supabaseClient\n) {\n  try {\n    const geojson = JSON.parse(fs.readFileSync(filePath, 'utf8'))\n    \n    \u002F\u002F Process in batches to avoid timeouts\n    const batchSize = 1000\n    const features = geojson.features.map(feature => ({\n      geom: feature.geometry,\n      properties: feature.properties || {}\n    }))\n\n    console.log(`Importing ${features.length} features...`)\n    \n    for (let i = 0; i \u003C features.length; i += batchSize) {\n      const batch = features.slice(i, i + batchSize)\n      const { data, error } = await supabaseClient\n        .from(tableName)\n        .insert(batch)\n      \n      if (error) {\n        console.error('Error inserting batch:', error)\n        break\n      }\n      \n      console.log(`Inserted batch ${Math.floor(i\u002FbatchSize) + 1}\u002F${Math.ceil(features.length\u002FbatchSize)}`)\n    }\n    \n    console.log('Import completed!')\n  } catch (error) {\n    console.error('Import failed:', error)\n  }\n}\n","JavaScript","js",[169,4883,4884,4889,4894,4899,4904,4909,4914,4919,4924,4929,4934,4939,4944,4949,4954,4958,4963,4967,4972,4977,4982,4988,4994,5000,5006,5012,5018,5024,5029,5035,5041,5046,5052,5058,5064,5069],{"__ignoreMap":99},[240,4885,4886],{"class":242,"line":243},[240,4887,4888],{},"async function importGeoJSON(\n",[240,4890,4891],{"class":242,"line":100},[240,4892,4893],{},"  filePath, \n",[240,4895,4896],{"class":242,"line":107},[240,4897,4898],{},"  tableName, \n",[240,4900,4901],{"class":242,"line":325},[240,4902,4903],{},"  supabaseClient\n",[240,4905,4906],{"class":242,"line":336},[240,4907,4908],{},") {\n",[240,4910,4911],{"class":242,"line":420},[240,4912,4913],{},"  try {\n",[240,4915,4916],{"class":242,"line":949},[240,4917,4918],{},"    const geojson = JSON.parse(fs.readFileSync(filePath, 'utf8'))\n",[240,4920,4921],{"class":242,"line":972},[240,4922,4923],{},"    \n",[240,4925,4926],{"class":242,"line":996},[240,4927,4928],{},"    \u002F\u002F Process in batches to avoid timeouts\n",[240,4930,4931],{"class":242,"line":1009},[240,4932,4933],{},"    const batchSize = 1000\n",[240,4935,4936],{"class":242,"line":1015},[240,4937,4938],{},"    const features = geojson.features.map(feature => ({\n",[240,4940,4941],{"class":242,"line":1027},[240,4942,4943],{},"      geom: feature.geometry,\n",[240,4945,4946],{"class":242,"line":1039},[240,4947,4948],{},"      properties: feature.properties || {}\n",[240,4950,4951],{"class":242,"line":1061},[240,4952,4953],{},"    }))\n",[240,4955,4956],{"class":242,"line":1083},[240,4957,1012],{"emptyLinePlaceholder":61},[240,4959,4960],{"class":242,"line":1106},[240,4961,4962],{},"    console.log(`Importing ${features.length} features...`)\n",[240,4964,4965],{"class":242,"line":1126},[240,4966,4923],{},[240,4968,4969],{"class":242,"line":3696},[240,4970,4971],{},"    for (let i = 0; i \u003C features.length; i += batchSize) {\n",[240,4973,4974],{"class":242,"line":3719},[240,4975,4976],{},"      const batch = features.slice(i, i + batchSize)\n",[240,4978,4979],{"class":242,"line":3742},[240,4980,4981],{},"      const { data, error } = await supabaseClient\n",[240,4983,4985],{"class":242,"line":4984},21,[240,4986,4987],{},"        .from(tableName)\n",[240,4989,4991],{"class":242,"line":4990},22,[240,4992,4993],{},"        .insert(batch)\n",[240,4995,4997],{"class":242,"line":4996},23,[240,4998,4999],{},"      \n",[240,5001,5003],{"class":242,"line":5002},24,[240,5004,5005],{},"      if (error) {\n",[240,5007,5009],{"class":242,"line":5008},25,[240,5010,5011],{},"        console.error('Error inserting batch:', error)\n",[240,5013,5015],{"class":242,"line":5014},26,[240,5016,5017],{},"        break\n",[240,5019,5021],{"class":242,"line":5020},27,[240,5022,5023],{},"      }\n",[240,5025,5027],{"class":242,"line":5026},28,[240,5028,4999],{},[240,5030,5032],{"class":242,"line":5031},29,[240,5033,5034],{},"      console.log(`Inserted batch ${Math.floor(i\u002FbatchSize) + 1}\u002F${Math.ceil(features.length\u002FbatchSize)}`)\n",[240,5036,5038],{"class":242,"line":5037},30,[240,5039,5040],{},"    }\n",[240,5042,5044],{"class":242,"line":5043},31,[240,5045,4923],{},[240,5047,5049],{"class":242,"line":5048},32,[240,5050,5051],{},"    console.log('Import completed!')\n",[240,5053,5055],{"class":242,"line":5054},33,[240,5056,5057],{},"  } catch (error) {\n",[240,5059,5061],{"class":242,"line":5060},34,[240,5062,5063],{},"    console.error('Import failed:', error)\n",[240,5065,5067],{"class":242,"line":5066},35,[240,5068,2496],{},[240,5070,5072],{"class":242,"line":5071},36,[240,5073,2515],{},[136,5075,5076],{},"This function can work perfectly as an Edge Function. However, for the sake of the \"minimum viable\" aspect of the project I just ran it as a local Node.JS application",[131,5078,5080],{"id":5079},"dynamic-vector-tile-querying","Dynamic Vector Tile Querying",[136,5082,5083,5084,5087,5088,5093,5094,5099],{},"Early on the client mentioned the desire to use Mapbox styling on the frontend while avoiding vendor lock-in risks on the data storage. This posed an interesting challenge to me, namely, ",[1168,5085,5086],{},"'how can I render map layers natively on the frontend while keeping the data completely external to Mapbox?'",". Lucky for me, Mapbox allows for ",[375,5089,5092],{"href":5090,"rel":5091},"https:\u002F\u002Fdocs.mapbox.com\u002Fmapbox-gl-js\u002Fapi\u002Fsources\u002F#vectortilesource",[379],"querying external sources"," by sending simple HTTP requests to a given endpoint using the users current bounding box in the query parameters (example later on). The response has to conform to the Mapbox ",[375,5095,5098],{"href":5096,"rel":5097},"https:\u002F\u002Fmapbox.github.io\u002Fvector-tile-spec\u002F",[379],"Vector Tile specification"," (basically Google's Protocol Buffer format).",[136,5101,5102],{},"Using Supabase made things slighlty easier. Using their Edge Functions I could quickly setup a read-only endpoint to receive the incoming Mapbox requests. This Edge Function internally calls an RPC function which does the heavy lifting in terms of geometry fetching and processing. Below you can see a simplified outline main Edge Function body. Notice how I added a custom query parameter allowing for a dynamic dataset selection.",[4874,5104,5105],{},[232,5106,5111],{"className":5107,"code":5108,"filename":5109,"language":5110,"meta":99,"style":99},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","serve(async (req)=>{\n  \u002F\u002F Parses query parameters from url \n  \u002F\u002F example: \u002Ffunctions\u002Fmvt\u002Fdataset123\u002F{z}\u002F{x}\u002F{y}\n  const url = new URL(req.url);\n  const parts = url.pathname.split('\u002F').filter((p)=>p.length > 0);\n  const mvtIndex = parts.indexOf('mvt');\n  const dataset = parts[mvtIndex + 1];\n  const z = Number(parts[mvtIndex + 2]);\n  const x = Number(parts[mvtIndex + 3]);\n  const y = Number(parts[mvtIndex + 4]);\n\n  \u002F\u002F Calls supabase RPC function to query the actual geo data\n  \u002F\u002F Args are parsed earlier from query parameters\n  const { data: vectorTileBinary, error } = await supabase.rpc('get_dynamic_lines_mvt', {\n    dataset,\n    z,\n    x,\n    y\n  });\n \n  \u002F\u002F Returns mvt data in raw bytes\n  return new Response(vectorTileBinary, {\n    status: 200,\n    headers: {\n      ...corsHeaders,\n      'Content-Type': 'application\u002Fvnd.mapbox-vector-tile'\n    }\n  });\n});\n","Supabase Edge Function","typescript",[169,5112,5113,5136,5141,5146,5174,5236,5265,5290,5321,5348,5375,5379,5384,5389,5438,5445,5452,5459,5464,5473,5477,5482,5500,5512,5521,5531,5550,5554,5562],{"__ignoreMap":99},[240,5114,5115,5118,5120,5123,5125,5128,5130,5133],{"class":242,"line":243},[240,5116,5117],{"class":257},"serve",[240,5119,261],{"class":308},[240,5121,5122],{"class":264},"async",[240,5124,2347],{"class":253},[240,5126,5127],{"class":268},"req",[240,5129,172],{"class":253},[240,5131,5132],{"class":264},"=>",[240,5134,5135],{"class":253},"{\n",[240,5137,5138],{"class":242,"line":100},[240,5139,5140],{"class":1899},"  \u002F\u002F Parses query parameters from url \n",[240,5142,5143],{"class":242,"line":107},[240,5144,5145],{"class":1899},"  \u002F\u002F example: \u002Ffunctions\u002Fmvt\u002Fdataset123\u002F{z}\u002F{x}\u002F{y}\n",[240,5147,5148,5150,5153,5155,5158,5161,5163,5165,5167,5170,5172],{"class":242,"line":325},[240,5149,2895],{"class":264},[240,5151,5152],{"class":308}," url",[240,5154,2391],{"class":253},[240,5156,5157],{"class":253}," new",[240,5159,5160],{"class":257}," URL",[240,5162,261],{"class":861},[240,5164,5127],{"class":308},[240,5166,223],{"class":253},[240,5168,5169],{"class":308},"url",[240,5171,172],{"class":861},[240,5173,312],{"class":253},[240,5175,5176,5178,5181,5183,5185,5187,5190,5192,5195,5197,5199,5201,5203,5205,5207,5210,5212,5214,5216,5218,5220,5222,5224,5227,5230,5232,5234],{"class":242,"line":336},[240,5177,2895],{"class":264},[240,5179,5180],{"class":308}," parts",[240,5182,2391],{"class":253},[240,5184,5152],{"class":308},[240,5186,223],{"class":253},[240,5188,5189],{"class":308},"pathname",[240,5191,223],{"class":253},[240,5193,5194],{"class":257},"split",[240,5196,261],{"class":861},[240,5198,4159],{"class":253},[240,5200,62],{"class":2008},[240,5202,4159],{"class":253},[240,5204,172],{"class":861},[240,5206,223],{"class":253},[240,5208,5209],{"class":257},"filter",[240,5211,261],{"class":861},[240,5213,261],{"class":253},[240,5215,136],{"class":268},[240,5217,172],{"class":253},[240,5219,5132],{"class":264},[240,5221,136],{"class":308},[240,5223,223],{"class":253},[240,5225,5226],{"class":308},"length",[240,5228,5229],{"class":253}," >",[240,5231,2129],{"class":867},[240,5233,172],{"class":861},[240,5235,312],{"class":253},[240,5237,5238,5240,5243,5245,5247,5249,5252,5254,5256,5259,5261,5263],{"class":242,"line":420},[240,5239,2895],{"class":264},[240,5241,5242],{"class":308}," mvtIndex",[240,5244,2391],{"class":253},[240,5246,5180],{"class":308},[240,5248,223],{"class":253},[240,5250,5251],{"class":257},"indexOf",[240,5253,261],{"class":861},[240,5255,4159],{"class":253},[240,5257,5258],{"class":2008},"mvt",[240,5260,4159],{"class":253},[240,5262,172],{"class":861},[240,5264,312],{"class":253},[240,5266,5267,5269,5272,5274,5276,5278,5281,5284,5286,5288],{"class":242,"line":949},[240,5268,2895],{"class":264},[240,5270,5271],{"class":308}," dataset",[240,5273,2391],{"class":253},[240,5275,5180],{"class":308},[240,5277,2299],{"class":861},[240,5279,5280],{"class":308},"mvtIndex",[240,5282,5283],{"class":253}," +",[240,5285,1960],{"class":867},[240,5287,2388],{"class":861},[240,5289,312],{"class":253},[240,5291,5292,5294,5297,5299,5302,5304,5307,5309,5311,5313,5316,5319],{"class":242,"line":972},[240,5293,2895],{"class":264},[240,5295,5296],{"class":308}," z",[240,5298,2391],{"class":253},[240,5300,5301],{"class":257}," Number",[240,5303,261],{"class":861},[240,5305,5306],{"class":308},"parts",[240,5308,2299],{"class":861},[240,5310,5280],{"class":308},[240,5312,5283],{"class":253},[240,5314,5315],{"class":867}," 2",[240,5317,5318],{"class":861},"])",[240,5320,312],{"class":253},[240,5322,5323,5325,5327,5329,5331,5333,5335,5337,5339,5341,5344,5346],{"class":242,"line":996},[240,5324,2895],{"class":264},[240,5326,3352],{"class":308},[240,5328,2391],{"class":253},[240,5330,5301],{"class":257},[240,5332,261],{"class":861},[240,5334,5306],{"class":308},[240,5336,2299],{"class":861},[240,5338,5280],{"class":308},[240,5340,5283],{"class":253},[240,5342,5343],{"class":867}," 3",[240,5345,5318],{"class":861},[240,5347,312],{"class":253},[240,5349,5350,5352,5354,5356,5358,5360,5362,5364,5366,5368,5371,5373],{"class":242,"line":1009},[240,5351,2895],{"class":264},[240,5353,3357],{"class":308},[240,5355,2391],{"class":253},[240,5357,5301],{"class":257},[240,5359,261],{"class":861},[240,5361,5306],{"class":308},[240,5363,2299],{"class":861},[240,5365,5280],{"class":308},[240,5367,5283],{"class":253},[240,5369,5370],{"class":867}," 4",[240,5372,5318],{"class":861},[240,5374,312],{"class":253},[240,5376,5377],{"class":242,"line":1015},[240,5378,1012],{"emptyLinePlaceholder":61},[240,5380,5381],{"class":242,"line":1027},[240,5382,5383],{"class":1899},"  \u002F\u002F Calls supabase RPC function to query the actual geo data\n",[240,5385,5386],{"class":242,"line":1039},[240,5387,5388],{"class":1899},"  \u002F\u002F Args are parsed earlier from query parameters\n",[240,5390,5391,5393,5396,5399,5401,5404,5406,5409,5412,5414,5417,5420,5422,5425,5427,5429,5432,5434,5436],{"class":242,"line":1061},[240,5392,2895],{"class":264},[240,5394,5395],{"class":253}," {",[240,5397,5398],{"class":861}," data",[240,5400,914],{"class":253},[240,5402,5403],{"class":308}," vectorTileBinary",[240,5405,272],{"class":253},[240,5407,5408],{"class":308}," error",[240,5410,5411],{"class":253}," }",[240,5413,2391],{"class":253},[240,5415,5416],{"class":2343}," await",[240,5418,5419],{"class":308}," supabase",[240,5421,223],{"class":253},[240,5423,5424],{"class":257},"rpc",[240,5426,261],{"class":861},[240,5428,4159],{"class":253},[240,5430,5431],{"class":2008},"get_dynamic_lines_mvt",[240,5433,4159],{"class":253},[240,5435,272],{"class":253},[240,5437,300],{"class":253},[240,5439,5440,5443],{"class":242,"line":1083},[240,5441,5442],{"class":308},"    dataset",[240,5444,877],{"class":253},[240,5446,5447,5450],{"class":242,"line":1106},[240,5448,5449],{"class":308},"    z",[240,5451,877],{"class":253},[240,5453,5454,5457],{"class":242,"line":1126},[240,5455,5456],{"class":308},"    x",[240,5458,877],{"class":253},[240,5460,5461],{"class":242,"line":3696},[240,5462,5463],{"class":308},"    y\n",[240,5465,5466,5469,5471],{"class":242,"line":3719},[240,5467,5468],{"class":253},"  }",[240,5470,172],{"class":861},[240,5472,312],{"class":253},[240,5474,5475],{"class":242,"line":3742},[240,5476,3309],{"class":861},[240,5478,5479],{"class":242,"line":4984},[240,5480,5481],{"class":1899},"  \u002F\u002F Returns mvt data in raw bytes\n",[240,5483,5484,5486,5488,5491,5493,5496,5498],{"class":242,"line":4990},[240,5485,2505],{"class":2343},[240,5487,5157],{"class":253},[240,5489,5490],{"class":257}," Response",[240,5492,261],{"class":861},[240,5494,5495],{"class":308},"vectorTileBinary",[240,5497,272],{"class":253},[240,5499,300],{"class":253},[240,5501,5502,5505,5507,5510],{"class":242,"line":4996},[240,5503,5504],{"class":861},"    status",[240,5506,914],{"class":253},[240,5508,5509],{"class":867}," 200",[240,5511,877],{"class":253},[240,5513,5514,5517,5519],{"class":242,"line":5002},[240,5515,5516],{"class":861},"    headers",[240,5518,914],{"class":253},[240,5520,300],{"class":253},[240,5522,5523,5526,5529],{"class":242,"line":5008},[240,5524,5525],{"class":253},"      ...",[240,5527,5528],{"class":308},"corsHeaders",[240,5530,877],{"class":253},[240,5532,5533,5536,5539,5541,5543,5545,5548],{"class":242,"line":5014},[240,5534,5535],{"class":253},"      '",[240,5537,5538],{"class":861},"Content-Type",[240,5540,4159],{"class":253},[240,5542,914],{"class":253},[240,5544,4154],{"class":253},[240,5546,5547],{"class":2008},"application\u002Fvnd.mapbox-vector-tile",[240,5549,4694],{"class":253},[240,5551,5552],{"class":242,"line":5020},[240,5553,5040],{"class":253},[240,5555,5556,5558,5560],{"class":242,"line":5026},[240,5557,5468],{"class":253},[240,5559,172],{"class":861},[240,5561,312],{"class":253},[240,5563,5564,5567,5569],{"class":242,"line":5031},[240,5565,5566],{"class":253},"}",[240,5568,172],{"class":308},[240,5570,312],{"class":253},[136,5572,5573,5574,5576],{},"Now for the interesting part. The PostgreSQL ",[169,5575,5431],{}," RPC function was made to have very rudamentary API, as shown below:",[232,5578,5583],{"className":5579,"code":5580,"filename":5581,"language":5582,"meta":99,"style":99},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","CREATE OR REPLACE FUNCTION public.get_dynamic_lines_mvt(\n  dataset text,\n  z integer,\n  x integer,\n  y integer\n) RETURNS bytea\n","PostgreSQL","sql",[169,5584,5585,5599,5608,5618,5627,5635],{"__ignoreMap":99},[240,5586,5587,5590,5593,5596],{"class":242,"line":243},[240,5588,5589],{"class":867},"CREATE OR REPLACE",[240,5591,5592],{"class":867}," FUNCTION",[240,5594,5595],{"class":257}," public",[240,5597,5598],{"class":308},".get_dynamic_lines_mvt(\n",[240,5600,5601,5604,5606],{"class":242,"line":100},[240,5602,5603],{"class":308},"  dataset ",[240,5605,4485],{"class":264},[240,5607,877],{"class":308},[240,5609,5610,5613,5616],{"class":242,"line":107},[240,5611,5612],{"class":308},"  z ",[240,5614,5615],{"class":264},"integer",[240,5617,877],{"class":308},[240,5619,5620,5623,5625],{"class":242,"line":325},[240,5621,5622],{"class":308},"  x ",[240,5624,5615],{"class":264},[240,5626,877],{"class":308},[240,5628,5629,5632],{"class":242,"line":336},[240,5630,5631],{"class":308},"  y ",[240,5633,5634],{"class":264},"integer\n",[240,5636,5637,5640,5643],{"class":242,"line":420},[240,5638,5639],{"class":308},") ",[240,5641,5642],{"class":867},"RETURNS",[240,5644,5645],{"class":264}," bytea\n",[136,5647,5648],{},"What actually happens inside of the function body can best be described in a handful of procedural steps.",[136,5650,5651],{},"Firstly, the sent coordinates values (x, y, z) are translated into rectangular polygon coordinates representing the user's bounding box.",[232,5653,5655],{"className":5579,"code":5654,"filename":5581,"language":5582,"meta":99,"style":99},"bounds3857 := ST_TileEnvelope(z, x, y);\n",[169,5656,5657],{"__ignoreMap":99},[240,5658,5659,5662,5664],{"class":242,"line":243},[240,5660,5661],{"class":308},"bounds3857 :",[240,5663,1308],{"class":253},[240,5665,5666],{"class":308}," ST_TileEnvelope(z, x, y);\n",[1272,5668,5669],{},[136,5670,5671,5674,5675],{},[169,5672,5673],{},"bounds385"," is generated and later injected at runtime using alias ",[169,5676,5677],{},"$1",[136,5679,5680],{},"Next, geometries that land outside of the requested tile are filtered out before moving on to expensive intersection computations.",[232,5682,5684],{"className":5579,"code":5683,"filename":5581,"language":5582,"meta":99,"style":99},"WITH filtered AS (\n  SELECT ST_Transform(wkb_geometry, 3857) AS geom3857 FROM %I.%I\n  WHERE ST_Transform(wkb_geometry, 3857) && $1::geometry\n  AND ST_Intersects(ST_Transform(wkb_geometry, 3857), $1::geometry)\n),\n",[169,5685,5686,5699,5723,5742,5764],{"__ignoreMap":99},[240,5687,5688,5691,5694,5697],{"class":242,"line":243},[240,5689,5690],{"class":867},"WITH",[240,5692,5693],{"class":308}," filtered ",[240,5695,5696],{"class":867},"AS",[240,5698,862],{"class":308},[240,5700,5701,5704,5707,5710,5712,5714,5717,5720],{"class":242,"line":100},[240,5702,5703],{"class":867},"  SELECT",[240,5705,5706],{"class":308}," ST_Transform(wkb_geometry, ",[240,5708,5709],{"class":867},"3857",[240,5711,5639],{"class":308},[240,5713,5696],{"class":867},[240,5715,5716],{"class":308}," geom3857 ",[240,5718,5719],{"class":867},"FROM",[240,5721,5722],{"class":308}," %I.%I\n",[240,5724,5725,5728,5730,5732,5735,5737,5739],{"class":242,"line":107},[240,5726,5727],{"class":867},"  WHERE",[240,5729,5706],{"class":308},[240,5731,5709],{"class":867},[240,5733,5734],{"class":308},") && $",[240,5736,1520],{"class":867},[240,5738,254],{"class":308},[240,5740,5741],{"class":867},"geometry\n",[240,5743,5744,5747,5750,5752,5755,5757,5759,5762],{"class":242,"line":325},[240,5745,5746],{"class":867},"  AND",[240,5748,5749],{"class":308}," ST_Intersects(ST_Transform(wkb_geometry, ",[240,5751,5709],{"class":867},[240,5753,5754],{"class":308},"), $",[240,5756,1520],{"class":867},[240,5758,254],{"class":308},[240,5760,5761],{"class":867},"geometry",[240,5763,2926],{"class":308},[240,5765,5766],{"class":242,"line":336},[240,5767,5768],{"class":308},"),\n",[136,5770,5771],{},"To further help performance, before any of the expensive geometry checks are done, the caller's z-value gets evaluated and classified. This is done in oreder to not overfetch very detailed geometry data for a large zoomed-out bounding box when geometry resolution is least important.",[232,5773,5775],{"className":5579,"code":5774,"filename":5581,"language":5582,"meta":99,"style":99},"simplification := CASE\n  WHEN z \u003C= 5  THEN 80\n  WHEN z \u003C= 10 THEN 15\n  ELSE 0\nEND;\n",[169,5776,5777,5787,5806,5823,5831],{"__ignoreMap":99},[240,5778,5779,5782,5784],{"class":242,"line":243},[240,5780,5781],{"class":308},"simplification :",[240,5783,1308],{"class":253},[240,5785,5786],{"class":867}," CASE\n",[240,5788,5789,5792,5795,5797,5800,5803],{"class":242,"line":100},[240,5790,5791],{"class":867},"  WHEN",[240,5793,5794],{"class":308}," z ",[240,5796,1417],{"class":253},[240,5798,5799],{"class":867}," 5",[240,5801,5802],{"class":867},"  THEN",[240,5804,5805],{"class":867}," 80\n",[240,5807,5808,5810,5812,5814,5817,5820],{"class":242,"line":107},[240,5809,5791],{"class":867},[240,5811,5794],{"class":308},[240,5813,1417],{"class":253},[240,5815,5816],{"class":867}," 10",[240,5818,5819],{"class":867}," THEN",[240,5821,5822],{"class":867}," 15\n",[240,5824,5825,5828],{"class":242,"line":325},[240,5826,5827],{"class":867},"  ELSE",[240,5829,5830],{"class":867}," 0\n",[240,5832,5833,5836],{"class":242,"line":336},[240,5834,5835],{"class":867},"END",[240,5837,312],{"class":308},[1272,5839,5840],{},[136,5841,5842,5843,2751,5845,5848,5849],{},"Just as ",[169,5844,5673],{},[169,5846,5847],{},"simplifications"," is also generated and injected at runtime using alias ",[169,5850,5851],{},"$2",[136,5853,5854,5855,5858,5859,223],{},"This value is subsequently used as an argument to clip and simplify the data to varying degrees. At the same time, ",[169,5856,5857],{},"ST_AsMVTGeom"," turns the simplified geometries from \"Well-Known Binary\" representation found in the actual table into the coordinate space of a MVT tile stored in ",[169,5860,5861],{},"mvtgeom",[232,5863,5865],{"className":5579,"code":5864,"filename":5581,"language":5582,"meta":99,"style":99},"mvtgeom AS (\n  SELECT\n    ST_AsMVTGeom(\n      CASE\n        WHEN $2 > 0\n          THEN ST_SimplifyPreserveTopology(geom3857, $2)\n        ELSE geom3857\n      END,\n      $1, 4096, 0, true\n    ) AS geom\n  FROM filtered\n)\n",[169,5866,5867,5876,5881,5886,5891,5905,5917,5925,5932,5951,5961,5969],{"__ignoreMap":99},[240,5868,5869,5872,5874],{"class":242,"line":243},[240,5870,5871],{"class":308},"mvtgeom ",[240,5873,5696],{"class":867},[240,5875,862],{"class":308},[240,5877,5878],{"class":242,"line":100},[240,5879,5880],{"class":867},"  SELECT\n",[240,5882,5883],{"class":242,"line":107},[240,5884,5885],{"class":308},"    ST_AsMVTGeom(\n",[240,5887,5888],{"class":242,"line":325},[240,5889,5890],{"class":867},"      CASE\n",[240,5892,5893,5896,5899,5901,5903],{"class":242,"line":336},[240,5894,5895],{"class":867},"        WHEN",[240,5897,5898],{"class":308}," $",[240,5900,1319],{"class":867},[240,5902,5229],{"class":253},[240,5904,5830],{"class":867},[240,5906,5907,5910,5913,5915],{"class":242,"line":420},[240,5908,5909],{"class":867},"          THEN",[240,5911,5912],{"class":308}," ST_SimplifyPreserveTopology(geom3857, $",[240,5914,1319],{"class":867},[240,5916,2926],{"class":308},[240,5918,5919,5922],{"class":242,"line":949},[240,5920,5921],{"class":867},"        ELSE",[240,5923,5924],{"class":308}," geom3857\n",[240,5926,5927,5930],{"class":242,"line":972},[240,5928,5929],{"class":867},"      END",[240,5931,877],{"class":308},[240,5933,5934,5937,5939,5941,5944,5946,5948],{"class":242,"line":996},[240,5935,5936],{"class":308},"      $",[240,5938,1520],{"class":867},[240,5940,2751],{"class":308},[240,5942,5943],{"class":867},"4096",[240,5945,2751],{"class":308},[240,5947,917],{"class":867},[240,5949,5950],{"class":308},", true\n",[240,5952,5953,5956,5958],{"class":242,"line":1009},[240,5954,5955],{"class":308},"    ) ",[240,5957,5696],{"class":867},[240,5959,5960],{"class":308}," geom\n",[240,5962,5963,5966],{"class":242,"line":1015},[240,5964,5965],{"class":867},"  FROM",[240,5967,5968],{"class":308}," filtered\n",[240,5970,5971],{"class":242,"line":1027},[240,5972,2926],{"class":308},[136,5974,5975],{},"Finally, filtered and simplified data is serialized into raw mvt bytes to be returned to the caller and later the user.",[232,5977,5979],{"className":5579,"code":5978,"filename":5581,"language":5582,"meta":99,"style":99},"SELECT ST_AsMVT(\n  q,\n  'lines',\n  4096,\n  'geom'\n)\nFROM (\n  SELECT geom\n  FROM mvtgeom\n) q;\n",[169,5980,5981,5989,5994,6006,6013,6022,6026,6032,6038,6045],{"__ignoreMap":99},[240,5982,5983,5986],{"class":242,"line":243},[240,5984,5985],{"class":867},"SELECT",[240,5987,5988],{"class":308}," ST_AsMVT(\n",[240,5990,5991],{"class":242,"line":100},[240,5992,5993],{"class":308},"  q,\n",[240,5995,5996,5999,6002,6004],{"class":242,"line":107},[240,5997,5998],{"class":253},"  '",[240,6000,6001],{"class":2008},"lines",[240,6003,4159],{"class":253},[240,6005,877],{"class":308},[240,6007,6008,6011],{"class":242,"line":325},[240,6009,6010],{"class":867},"  4096",[240,6012,877],{"class":308},[240,6014,6015,6017,6020],{"class":242,"line":336},[240,6016,5998],{"class":253},[240,6018,6019],{"class":2008},"geom",[240,6021,4694],{"class":253},[240,6023,6024],{"class":242,"line":420},[240,6025,2926],{"class":308},[240,6027,6028,6030],{"class":242,"line":949},[240,6029,5719],{"class":867},[240,6031,862],{"class":308},[240,6033,6034,6036],{"class":242,"line":972},[240,6035,5703],{"class":867},[240,6037,5960],{"class":308},[240,6039,6040,6042],{"class":242,"line":996},[240,6041,5965],{"class":867},[240,6043,6044],{"class":308}," mvtgeom\n",[240,6046,6047],{"class":242,"line":1009},[240,6048,6049],{"class":308},") q;\n",[136,6051,6052,6053,6058],{},"As you may already have noticed, in order to achieve much of the geometry translation and serialization functionallity I made use of PostGIS' exhaustive list of Special Functions. Perhaps this post is not the ideal place to go into detail on them so I highly recommend checking out the official ",[375,6054,6057],{"href":6055,"rel":6056},"https:\u002F\u002Fpostgis.net\u002Fdocs\u002FPostGIS_Special_Functions_Index.html",[379],"PostGIS Special Function Index"," in case you'd like to know more. The main Special Functions I ended up using were the following:",[186,6060,6061],{},[232,6062,6066],{"className":6063,"code":6064,"language":6065,"meta":99,"style":99},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","ST_TileEnvelope()\nST_Transform()\nST_SimplifyPreserveTopology()\nST_Intersects()\nST_AsMVTGeom()\nST_AsMVT()\n","ts",[169,6067,6068,6076,6083,6090,6097,6103],{"__ignoreMap":99},[240,6069,6070,6073],{"class":242,"line":243},[240,6071,6072],{"class":257},"ST_TileEnvelope",[240,6074,6075],{"class":308},"()\n",[240,6077,6078,6081],{"class":242,"line":100},[240,6079,6080],{"class":257},"ST_Transform",[240,6082,6075],{"class":308},[240,6084,6085,6088],{"class":242,"line":107},[240,6086,6087],{"class":257},"ST_SimplifyPreserveTopology",[240,6089,6075],{"class":308},[240,6091,6092,6095],{"class":242,"line":325},[240,6093,6094],{"class":257},"ST_Intersects",[240,6096,6075],{"class":308},[240,6098,6099,6101],{"class":242,"line":336},[240,6100,5857],{"class":257},[240,6102,6075],{"class":308},[240,6104,6105,6108],{"class":242,"line":420},[240,6106,6107],{"class":257},"ST_AsMVT",[240,6109,6075],{"class":308},[399,6111,6112],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":99,"searchDepth":100,"depth":100,"links":6114},[6115,6116,6117,6118,6119],{"id":133,"depth":107,"text":134},{"id":4789,"depth":100,"text":4790},{"id":733,"depth":100,"text":734},{"id":4806,"depth":100,"text":4807},{"id":4827,"depth":100,"text":4828,"children":6120},[6121,6122,6123],{"id":4831,"depth":107,"text":4832},{"id":4865,"depth":107,"text":4866},{"id":5079,"depth":107,"text":5080},"#137E6E","2025-10-01","Client project where I was tasked with building a full-stack energy grid visualization platform by ingesting open municipality data into a PostGIS databse.","content\u002Ffocal-grid-banner.png",{},"Full-Stack GIS App",{"title":32,"description":6126},[6132,6133,6134,6135],"TypeScript","Nuxt.js","Mapbox SDK","PostGIS","B52rFuBHshMTdmD2sWIMFnRQ0ni8sSG8Xl18ace4qgo",1789678541640]