[{"data":1,"prerenderedAt":7072},["ShallowReactive",2],{"navigation":3,"work-toc:\u002Fwork\u002Fvalhalla":43,"work-page":824,"work":832,"\u002Fwork\u002Fvalhalla":6050,"\u002Fwork\u002Fvalhalla-surround":6560,"work:\u002Fwork\u002Fvalhalla":6562},[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":40,"body":45,"color":808,"date":809,"description":810,"draft":21,"extension":811,"image":812,"meta":813,"navigation":182,"oneliner":814,"path":41,"seo":815,"stem":42,"tags":816,"url":822,"__hash__":823},"work\u002Fwork\u002Fvalhalla.md",{"type":46,"value":47,"toc":798},"minimark",[48,53,57,62,66,83,794],[49,50,52],"h2",{"id":51},"tldr","TL;DR",[54,55,56],"p",{},"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.",[58,59],"table-of-contents",{"exclude":60,":max-depth":61},"Table of Contents, TL;DR","2",[49,63,65],{"id":64},"background","Background",[54,67,68,69,77,78,82],{},"During GSoC 2026 I applied for a ",[70,71,76],"a",{"href":72,"rel":73,"target":75},"https:\u002F\u002Fwiki.openstreetmap.org\u002Fwiki\u002FGoogle_Summer_of_Code\u002F2026\u002FProject_ideas#Routing",[74],"nofollow","_blank","project suggestion"," by OSM member Simon Poole. The gist was to integrate an existing platform (",[79,80,81],"code",{},"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.",[84,85,86,90,98,124,131,256,263,283,287,290,293,300,303,320,324,331,334,341,344,388,391,414,418,421,424,462,465,468,473,480,486,493,771],"steps",{"level":61},[49,87,89],{"id":88},"docker-version-logging","Docker Version Logging",[54,91,92,93,97],{},"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 ",[94,95,96],"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.",[99,100,105],"pre",{"className":101,"code":102,"language":103,"meta":104,"style":104},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","valhalla_service --version # prints \"x.x.x-commit_hash\"\n","bash","",[79,106,107],{"__ignoreMap":104},[108,109,112,116,120],"span",{"class":110,"line":111},"line",1,[108,113,115],{"class":114},"sBMFI","valhalla_service",[108,117,119],{"class":118},"sfazB"," --version",[108,121,123],{"class":122},"sHwdD"," # prints \"x.x.x-commit_hash\"\n",[54,125,126,127,130],{},"My patch added the following lines to the start of ",[79,128,129],{},"docker\u002Fscripts\u002Fdocker-entrypoint.sh",":",[99,132,134],{"className":101,"code":133,"language":103,"meta":104,"style":104},"# 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",[79,135,136,141,177,184,194,208,220,231,249],{"__ignoreMap":104},[108,137,138],{"class":110,"line":111},[108,139,140],{"class":122},"# logging valhalla version\n",[108,142,144,148,152,154,156,159,162,165,168,171,174],{"class":110,"line":143},2,[108,145,147],{"class":146},"sTEyZ","VALHALLA_VERSION",[108,149,151],{"class":150},"sMK4o","=$(",[108,153,115],{"class":114},[108,155,119],{"class":118},[108,157,158],{"class":150}," |",[108,160,161],{"class":114}," tr",[108,163,164],{"class":118}," -d",[108,166,167],{"class":150}," '",[108,169,170],{"class":118},"\\n",[108,172,173],{"class":150},"'",[108,175,176],{"class":150},")\n",[108,178,180],{"class":110,"line":179},3,[108,181,183],{"emptyLinePlaceholder":182},true,"\n",[108,185,187,191],{"class":110,"line":186},4,[108,188,190],{"class":189},"s2Zo4","echo",[108,192,193],{"class":150}," \"\"\n",[108,195,197,199,202,205],{"class":110,"line":196},5,[108,198,190],{"class":189},[108,200,201],{"class":150}," \"",[108,203,204],{"class":118},"====================",[108,206,207],{"class":150},"\"\n",[108,209,211,213,215,218],{"class":110,"line":210},6,[108,212,190],{"class":189},[108,214,201],{"class":150},[108,216,217],{"class":118},"= Valhalla version =",[108,219,207],{"class":150},[108,221,223,225,227,229],{"class":110,"line":222},7,[108,224,190],{"class":189},[108,226,201],{"class":150},[108,228,204],{"class":118},[108,230,207],{"class":150},[108,232,234,236,238,241,244,246],{"class":110,"line":233},8,[108,235,190],{"class":189},[108,237,201],{"class":150},[108,239,240],{"class":118},"Valhalla version: ",[108,242,243],{"class":150},"${",[108,245,147],{"class":146},[108,247,248],{"class":150},"}\"\n",[108,250,252,254],{"class":110,"line":251},9,[108,253,190],{"class":189},[108,255,193],{"class":150},[54,257,258,259,262],{},"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 ",[79,260,261],{},"3.6.3",".",[264,265,266,275],"card-group",{},[267,268,272],"card",{"icon":269,"title":270,"to":271,"target":75},"octicon:issue-opened-16","Issue #5721","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fissues\u002F5721",[54,273,274],{},"Include Valhalla version in Docker container startup logs",[267,276,280],{"icon":277,"title":278,"to":279,"target":75},"boxicons:git-pull-request","Pull Request #5884","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F5884",[54,281,282],{},"docker-entrypoint: display Valhalla version on container start",[49,284,286],{"id":285},"gsoc-proposal","GSoC Proposal",[54,288,289],{},"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.",[54,291,292],{},"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.",[54,294,295,296,299],{},"From a technical perspective, the pipeline contained in the proposed ",[79,297,298],{},"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.",[54,301,302],{},"My blog post linked below contains a link to the full GSoC proposal.",[264,304,305,312],{},[267,306,309],{"icon":307,"title":308,"to":15},"tabler:article","Blog Post",[54,310,311],{},"GSoC 2026: A Hopeful Rejection",[267,313,317],{"icon":314,"title":315,"to":316,"target":75},"octicon:comment-discussion-16","Discussion #5944","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fdiscussions\u002F5944",[54,318,319],{},"Native Dynamic Road Closure Support",[49,321,323],{"id":322},"python-openlr-module","Python OpenLR Module",[54,325,326,327,330],{},"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 (",[79,328,329],{},"baldr\u002Fopenlr.h",").",[54,332,333],{},"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.",[54,335,336,337,340],{},"The bindings would live in ",[79,338,339],{},"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.",[54,342,343],{},"Some notable bindings include:",[345,346,347,365,374],"ul",{},[348,349,350,353,354,357,358,353,361,364],"li",{},[79,351,352],{},"to_base64"," and ",[79,355,356],{},"to_binary"," encoding functions (returning ",[79,359,360],{},"str",[79,362,363],{},"bytes"," respectively)",[348,366,367,353,370,373],{},[79,368,369],{},"LocationReferencePoint",[79,371,372],{},"PointLL"," data structures",[348,375,376,379,380,383,384,387],{},[79,377,378],{},"SideOfTheRoad"," ",[79,381,382],{},"Orientation",", and ",[79,385,386],{},"FormOfWay"," enums",[54,389,390],{},"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.",[264,392,393,400,407],{},[267,394,397],{"icon":314,"title":395,"to":396,"target":75},"Discussion #6035","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fdiscussions\u002F6035",[54,398,399],{},"Python bindings for OpenLR",[267,401,404],{"icon":269,"title":402,"to":403,"target":75},"Issue #6037","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fissues\u002F6037",[54,405,406],{},"OpenLR Python bindings",[267,408,411],{"icon":277,"title":409,"to":410,"target":75},"Pull Request #6087","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F6087",[54,412,413],{},"feat: openlr python bindings",[49,415,417],{"id":416},"python-binding-restructuring","Python Binding Restructuring",[54,419,420],{},"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.",[54,422,423],{},"My proposed solution was to unify the bindings extension approach by grouping them by their corresponding C++ namespaces.",[99,425,430],{"className":426,"code":427,"filename":428,"language":429,"meta":104,"style":104},"language-plaintext shiki shiki-themes material-theme-lighter material-theme material-theme-palenight",".  \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","plaintext",[79,431,432,437,442,447,452,457],{"__ignoreMap":104},[108,433,434],{"class":110,"line":111},[108,435,436],{},".  \n",[108,438,439],{"class":110,"line":143},[108,440,441],{},"├── midgard\n",[108,443,444],{"class":110,"line":179},[108,445,446],{},"│ ├── PointLL  \n",[108,448,449],{"class":110,"line":186},[108,450,451],{},"│ ├── Heading  \n",[108,453,454],{"class":110,"line":196},[108,455,456],{},"│ ├── Curvature  \n",[108,458,459],{"class":110,"line":210},[108,460,461],{},"│ └── ...\n",[54,463,464],{},"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.",[54,466,467],{},"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.",[469,470,472],"h3",{"id":471},"segfault-bug-identification","Segfault Bug Identification",[54,474,475,476,479],{},"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 ",[79,477,478],{},"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.",[54,481,482,483,262],{},"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 ",[79,484,485],{},"tee",[54,487,488,489,492],{},"I also found a separate Python related ",[79,490,491],{},"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.",[494,495,496,505],"collapsible",{},[54,497,498],{},[499,500,501],"em",{},[502,503,504],"strong",{},"VictorYanson commented on Jun 19",[506,507,508,519,547,555,768],"blockquote",{},[54,509,510,511,514,515,518],{},"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 ",[79,512,513],{},"Run Tests"," job in >the ",[79,516,517],{},"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.",[99,520,525],{"className":521,"code":522,"language":523,"meta":524,"style":104},"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",[79,526,527,532,537,542],{"__ignoreMap":104},[108,528,529],{"class":110,"line":111},[108,530,531],{},"2026-06-14T19:32:46.5585100Z test_get_tile_ids_from_bbox (test_graph_utils.TestBindings.test_get_tile_ids_from_bbox) ... ok\n",[108,533,534],{"class":110,"line":143},[108,535,536],{},"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",[108,538,539],{"class":110,"line":179},[108,540,541],{},"2026-06-14T19:32:46.5586790Z Open ring (last != first) should be auto-closed. ... [ 34%] Built target gurka_closure_penalty\n",[108,543,544],{"class":110,"line":186},[108,545,546],{},"2026-06-14T19:32:46.5587250Z [ 34%] Built target run-python_valhalla\n",[54,548,549,550,554],{},"I guess because the test runner pipes the output through ",[70,551,485],{"href":552,"rel":553},"https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fblob\u002F8315d324f8d179b6477c3152e994e134a54df50f\u002Ftest\u002FCMakeLists.txt#L480",[74]," the CI only >checks if tee succeeded while completely missing the fact that python crashed.",[556,557,558,562,565,739,748],"details",{},[559,560,561],"summary",{},"I also spotted a separate python key error locally during the config load which is reflected in the CI logs as well.",[54,563,564],{},"Local:",[99,566,568],{"className":101,"code":567,"language":103,"meta":104,"style":104},"======================================================================\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",[79,569,570,575,586,591,611,639,661,684,726],{"__ignoreMap":104},[108,571,572],{"class":110,"line":111},[108,573,574],{"class":118},"======================================================================\n",[108,576,577,580,583],{"class":110,"line":143},[108,578,579],{"class":114},"ERROR:",[108,581,582],{"class":118}," test_change_config",[108,584,585],{"class":146}," (test_actor.TestBindings.test_change_config)\n",[108,587,588],{"class":110,"line":179},[108,589,590],{"class":114},"----------------------------------------------------------------------\n",[108,592,593,596,599,602,605,608],{"class":110,"line":186},[108,594,595],{"class":114},"Traceback",[108,597,598],{"class":146}," (most ",[108,600,601],{"class":118},"recent",[108,603,604],{"class":118}," call",[108,606,607],{"class":118}," last",[108,609,610],{"class":146},"):\n",[108,612,613,616,618,621,624,627,630,633,636],{"class":110,"line":196},[108,614,615],{"class":114}," File",[108,617,201],{"class":150},[108,619,620],{"class":118},"...\u002Ftest_actor.py",[108,622,623],{"class":150},"\"",[108,625,626],{"class":118},",",[108,628,629],{"class":118}," line",[108,631,632],{"class":118}," 186,",[108,634,635],{"class":118}," in",[108,637,638],{"class":118}," test_change_config\n",[108,640,641,644,647,650,653,656,659],{"class":110,"line":210},[108,642,643],{"class":114},"   config",[108,645,646],{"class":118}," =",[108,648,649],{"class":118}," get_config",[108,651,652],{"class":150},"(",[108,654,655],{"class":114},"self.extract_path,",[108,657,658],{"class":118}," self.tiles_path",[108,660,176],{"class":150},[108,662,663,665,667,670,672,674,676,679,681],{"class":110,"line":222},[108,664,615],{"class":114},[108,666,201],{"class":150},[108,668,669],{"class":118},"...\u002Fvalhalla\u002Fconfig.py",[108,671,623],{"class":150},[108,673,626],{"class":118},[108,675,629],{"class":118},[108,677,678],{"class":118}," 57,",[108,680,635],{"class":118},[108,682,683],{"class":118}," get_config\n",[108,685,686,689,692,695,698,700,703,706,708,710,713,715,718,721,724],{"class":110,"line":233},[108,687,688],{"class":114},"   config[",[108,690,691],{"class":114},"\"mjolnir\"",[108,693,694],{"class":114},"][",[108,696,697],{"class":114},"\"logging\"",[108,699,694],{"class":114},[108,701,702],{"class":114},"\"type\"",[108,704,705],{"class":114},"]",[108,707,646],{"class":118},[108,709,201],{"class":150},[108,711,712],{"class":118},"std_out",[108,714,623],{"class":150},[108,716,717],{"class":118}," if",[108,719,720],{"class":118}," verbose",[108,722,723],{"class":118}," else",[108,725,193],{"class":150},[108,727,728,731,733,736],{"class":110,"line":251},[108,729,730],{"class":114},"KeyError:",[108,732,167],{"class":150},[108,734,735],{"class":118},"logging",[108,737,738],{"class":150},"'\n",[54,740,741,742,744,745,747],{},"CI (lines 1149-1151 in ",[79,743,517],{}," > ",[79,746,513],{},"):",[99,749,751],{"className":521,"code":750,"language":523,"meta":524,"style":104},"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",[79,752,753,758,763],{"__ignoreMap":104},[108,754,755],{"class":110,"line":111},[108,756,757],{},"2026-06-14T19:32:46.0710400Z test_change_config (test_actor.TestBindings.test_change_config) ... ERROR\n",[108,759,760],{"class":110,"line":143},[108,761,762],{},"2026-06-14T19:32:46.0810630Z test_config (test_actor.TestBindings.test_config) ... ERROR\n",[108,764,765],{"class":110,"line":179},[108,766,767],{},"2026-06-14T19:32:46.0912660Z test_config_actor (test_actor.TestBindings.test_config_actor) ... ERROR\n",[54,769,770],{},"Anyways, the code changes in last commit look totally fine to me. I’ll mark it for approval!",[264,772,773,780,787],{},[267,774,777],{"icon":314,"title":775,"to":776,"target":75},"Discussion #6069","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fdiscussions\u002F6096",[54,778,779],{},"re-design python binding structure",[267,781,784],{"icon":277,"title":782,"to":783,"target":75},"Pull Request #6133","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F6133",[54,785,786],{},"BREAKING(python): change layout of bindings, no signature change",[267,788,791],{"icon":277,"title":789,"to":790,"target":75},"Pull Request #6179","https:\u002F\u002Fgithub.com\u002Fvalhalla\u002Fvalhalla\u002Fpull\u002F6179",[54,792,793],{},"fix(release): 3.8.0 didn't land on pypi",[795,796,797],"style",{},"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":104,"searchDepth":143,"depth":143,"links":799},[800,801,802,803,804,805],{"id":51,"depth":143,"text":52},{"id":64,"depth":143,"text":65},{"id":88,"depth":143,"text":89},{"id":285,"depth":143,"text":286},{"id":322,"depth":143,"text":323},{"id":416,"depth":143,"text":417,"children":806},[807],{"id":471,"depth":179,"text":472},"#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.","md","\u002Fcontent\u002Fvalhalla-banner.png",{},"OSM Routing Engine",{"title":40,"description":810},[817,818,819,820,821],"OSM","GIS","Routing Engine","C++","Python",null,"K8w0XE83IFjxe9YpAdeihbSYomrrGh8QYfnrlVSkZos",{"id":825,"title":826,"body":822,"description":827,"extension":828,"links":822,"meta":829,"navigation":182,"path":24,"seo":830,"stem":25,"__hash__":831},"pages\u002Fwork.yml","Notable Projects","My work varies from client and open source projects to more educational-oriented personal endeavors. I think these four projects in particular depict my evolution as an engineer the best.","yml",{},{"title":826,"description":827},"5rea1WYGIa_7FKUEWP5ekVG473i6gIOG6Bcf9GNZhJo",[833,2932,4178,4688],{"id":834,"title":28,"body":835,"color":2919,"date":2920,"description":2921,"draft":21,"extension":811,"image":2922,"meta":2923,"navigation":182,"oneliner":2924,"path":29,"seo":2925,"stem":30,"tags":2926,"url":822,"__hash__":2931},"work\u002Fwork\u002Fdmap-cache.md",{"type":46,"value":836,"toc":2900},[837,839,842,845,853,855,875,879,894,901,904,910,913,923,927,930,934,937,941,944,948,951,957,961,964,1246,1272,1275,1279,1282,1287,1292,1295,1315,1385,1388,1394,1398,1401,1407,1410,1413,1456,1459,1463,1482,1488,1502,1507,1510,1542,1558,1562,1565,1568,1737,1876,1880,1887,1890,1904,1910,1914,1917,1951,1954,1960,1979,1982,1986,1993,2035,2038,2041,2102,2105,2307,2385,2389,2392,2395,2628,2631,2651,2654,2857,2874,2894,2897],[469,838,52],{"id":51},[54,840,841],{},"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.",[58,843],{"exclude":60,":max-depth":844},"3",[267,846,850],{"icon":847,"target":75,"title":848,"to":849},"i-simple-icons-github","Code","https:\u002F\u002Fgithub.com\u002FVictorYanson\u002FCFU-Proving-Ground\u002Fblob\u002F48486392679fc3ebdbe8db0b572b6c263bb74cee\u002Fmain.v#L190-L290",[54,851,852],{},"Checkout the code for yourself on GitHub",[49,854,65],{"id":64},[54,856,857,858,353,863,868,869,874],{},"Having gotten extensive practice solving ",[70,859,862],{"href":860,"rel":861},"https:\u002F\u002Fhdlbits.01xz.net\u002Fwiki\u002FMain_Page",[74],"HDLBits",[70,864,867],{"href":865,"rel":866},"https:\u002F\u002Fasmbits.01xz.net\u002Fwiki\u002FMain_Page",[74],"ASMBits"," problems and reading through Patterson and Hennesy's ",[70,870,873],{"href":871,"rel":872},"https:\u002F\u002Fbooks.google.pl\u002Fbooks\u002Fabout\u002FComputer_Organization_and_Design_RISC_V.html?id=H7wxDQAAQBAJ&source=kp_book_description&redir_esc=y",[74],"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.",[469,876,878],{"id":877},"cfu-proving-ground-in-a-nutshell","CFU Proving Ground in a Nutshell",[54,880,881,882,887,888,893],{},"In essence, ",[70,883,886],{"href":884,"rel":885},"https:\u002F\u002Fgithub.com\u002Farchlab-sciencetokyo\u002FCFU-Proving-Ground",[74],"CFU Proving Ground"," is an RISC-V acceleration framework for FPGA development which iterates on Google's ",[70,889,892],{"href":890,"rel":891},"https:\u002F\u002Fgithub.com\u002Fgoogle\u002FCFU-Playground",[74],"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.",[54,895,896],{},[897,898],"img",{"alt":899,"src":900},"CPU Diagram Showing Five Stage Pipeline","\u002Fcontent\u002Fd-map-cache-screenshot-2.png",[54,902,903],{},"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 ;).",[54,905,906],{},[897,907],{"alt":908,"src":909},"SoC Diagram","\u002Fcontent\u002Fd-map-cache-photo-2.jpeg",[54,911,912],{},"In this SoC diagram the video I\u002FO is truncated as it is not relevant to this project.",[914,915,916],"tip",{},[54,917,918,919,922],{},"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 ",[79,920,921],{},"main.c"," file get compiled by GCC and run on a 1,000 line processor feels quite insane!",[49,924,926],{"id":925},"the-plan","The Plan",[54,928,929],{},"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.",[469,931,933],{"id":932},"why-specifically-a-direct-mapped-cache","Why specifically a direct-mapped cache?",[54,935,936],{},"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.",[469,938,940],{"id":939},"whats-the-point-of-adding-such-a-simple-cache","What's the point of adding such a simple cache?",[54,942,943],{},"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.",[49,945,947],{"id":946},"the-implementation","The Implementation",[54,949,950],{},"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.",[54,952,953],{},[897,954],{"alt":955,"src":956},"SoC Diagram with Cache","\u002Fcontent\u002Fd-map-cache-photo-4.jpeg",[469,958,960],{"id":959},"the-interface","The Interface",[54,962,963],{},"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.",[99,965,969],{"className":966,"code":967,"language":968,"meta":104,"style":104},"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",[79,970,971,984,999,1010,1021,1046,1067,1089,1112,1124,1129,1141,1153,1175,1197,1220,1240],{"__ignoreMap":104},[108,972,973,977,980],{"class":110,"line":111},[108,974,976],{"class":975},"spNyl","module",[108,978,979],{"class":114}," dmap_cache",[108,981,983],{"class":982},"swJcz"," (\n",[108,985,986,990,993,996],{"class":110,"line":143},[108,987,989],{"class":988},"sbssI","  input",[108,991,992],{"class":988},"  wire",[108,994,995],{"class":146},"        clk_i",[108,997,998],{"class":982},",\n",[108,1000,1001,1003,1005,1008],{"class":110,"line":179},[108,1002,989],{"class":988},[108,1004,992],{"class":988},[108,1006,1007],{"class":146},"        re_i",[108,1009,998],{"class":982},[108,1011,1012,1014,1016,1019],{"class":110,"line":186},[108,1013,989],{"class":988},[108,1015,992],{"class":988},[108,1017,1018],{"class":146},"        we_i",[108,1020,998],{"class":982},[108,1022,1023,1025,1027,1030,1033,1035,1038,1041,1044],{"class":110,"line":196},[108,1024,989],{"class":988},[108,1026,992],{"class":988},[108,1028,1029],{"class":982}," [",[108,1031,1032],{"class":988},"31",[108,1034,130],{"class":150},[108,1036,1037],{"class":988},"0",[108,1039,1040],{"class":982},"] ",[108,1042,1043],{"class":146},"addr_i",[108,1045,998],{"class":982},[108,1047,1048,1050,1052,1054,1056,1058,1060,1062,1065],{"class":110,"line":210},[108,1049,989],{"class":988},[108,1051,992],{"class":988},[108,1053,1029],{"class":982},[108,1055,1032],{"class":988},[108,1057,130],{"class":150},[108,1059,1037],{"class":988},[108,1061,1040],{"class":982},[108,1063,1064],{"class":146},"wdata_i",[108,1066,998],{"class":982},[108,1068,1069,1071,1073,1076,1078,1080,1082,1084,1087],{"class":110,"line":222},[108,1070,989],{"class":988},[108,1072,992],{"class":988},[108,1074,1075],{"class":982},"  [",[108,1077,844],{"class":988},[108,1079,130],{"class":150},[108,1081,1037],{"class":988},[108,1083,1040],{"class":982},[108,1085,1086],{"class":146},"wstrb_i",[108,1088,998],{"class":982},[108,1090,1091,1094,1097,1099,1101,1103,1105,1107,1110],{"class":110,"line":233},[108,1092,1093],{"class":988},"  output",[108,1095,1096],{"class":988}," reg",[108,1098,1075],{"class":982},[108,1100,1032],{"class":988},[108,1102,130],{"class":150},[108,1104,1037],{"class":988},[108,1106,1040],{"class":982},[108,1108,1109],{"class":146},"rdata_o",[108,1111,998],{"class":982},[108,1113,1114,1116,1119,1122],{"class":110,"line":251},[108,1115,1093],{"class":988},[108,1117,1118],{"class":988}," wire",[108,1120,1121],{"class":146},"        stall_o",[108,1123,998],{"class":982},[108,1125,1127],{"class":110,"line":1126},10,[108,1128,183],{"emptyLinePlaceholder":182},[108,1130,1132,1134,1136,1139],{"class":110,"line":1131},11,[108,1133,1093],{"class":988},[108,1135,1118],{"class":988},[108,1137,1138],{"class":146},"        mem_re_o",[108,1140,998],{"class":982},[108,1142,1144,1146,1148,1151],{"class":110,"line":1143},12,[108,1145,1093],{"class":988},[108,1147,1118],{"class":988},[108,1149,1150],{"class":146},"        mem_we_o",[108,1152,998],{"class":982},[108,1154,1156,1158,1160,1162,1164,1166,1168,1170,1173],{"class":110,"line":1155},13,[108,1157,1093],{"class":988},[108,1159,1118],{"class":988},[108,1161,1029],{"class":982},[108,1163,1032],{"class":988},[108,1165,130],{"class":150},[108,1167,1037],{"class":988},[108,1169,1040],{"class":982},[108,1171,1172],{"class":146},"mem_addr_o",[108,1174,998],{"class":982},[108,1176,1178,1180,1182,1184,1186,1188,1190,1192,1195],{"class":110,"line":1177},14,[108,1179,1093],{"class":988},[108,1181,1118],{"class":988},[108,1183,1029],{"class":982},[108,1185,1032],{"class":988},[108,1187,130],{"class":150},[108,1189,1037],{"class":988},[108,1191,1040],{"class":982},[108,1193,1194],{"class":146},"mem_wdata_o",[108,1196,998],{"class":982},[108,1198,1200,1202,1204,1206,1208,1210,1212,1215,1218],{"class":110,"line":1199},15,[108,1201,1093],{"class":988},[108,1203,1118],{"class":988},[108,1205,1029],{"class":982},[108,1207,844],{"class":988},[108,1209,130],{"class":150},[108,1211,1037],{"class":988},[108,1213,1214],{"class":982},"]  ",[108,1216,1217],{"class":146},"mem_wstrb_o",[108,1219,998],{"class":982},[108,1221,1223,1225,1227,1229,1231,1233,1235,1237],{"class":110,"line":1222},16,[108,1224,989],{"class":988},[108,1226,992],{"class":988},[108,1228,1029],{"class":982},[108,1230,1032],{"class":988},[108,1232,130],{"class":150},[108,1234,1037],{"class":988},[108,1236,1040],{"class":982},[108,1238,1239],{"class":146},"mem_rdata_i\n",[108,1241,1243],{"class":110,"line":1242},17,[108,1244,1245],{"class":982},");\n",[54,1247,1248,1249,353,1252,1255,1256,1258,1259,1261,1262,1264,1265,1267,1268,1271],{},"From the CPU side a handful of wires are exposed closely resembling the original memory interface including: ",[79,1250,1251],{},"re_i",[79,1253,1254],{},"we_i"," for read\u002Fwrite enable signals, ",[79,1257,1043],{}," for the target address, ",[79,1260,1064],{}," for the write data, and ",[79,1263,1109],{}," for the data retruned to the CPU. Also ",[79,1266,1086],{}," 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 ",[79,1269,1270],{},"stall_o",". This wire is simple used to idle the CPU during the cycles when the data is being fetched after a cache miss.",[54,1273,1274],{},"From the memory side things are mostly the same only the direction of the data being reversed.",[469,1276,1278],{"id":1277},"the-internals","The Internals",[54,1280,1281],{},"While reading the explanation of the internals keep the following mental model in mind:",[54,1283,1284],{},[499,1285,1286],{},"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.",[1288,1289,1291],"h4",{"id":1290},"cache-storage","Cache Storage",[54,1293,1294],{},"Data is stored within the module in three unpacked arrays of registers:",[345,1296,1297,1303,1309],{},[348,1298,1299,1302],{},[79,1300,1301],{},"cache_data"," represents the 16 cachelines containing a 32 bit word each,",[348,1304,1305,1308],{},[79,1306,1307],{},"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),",[348,1310,1311,1314],{},[79,1312,1313],{},"cache_valid"," is initialized at startup to 0 to force compulsory misses when commencing the software boot.",[99,1316,1318],{"className":966,"code":1317,"language":968,"meta":104,"style":104},"reg [31:0] cache_data  [0:15];\nreg [25:0] cache_tag   [0:15];\nreg        cache_valid [0:15];\n",[79,1319,1320,1346,1370],{"__ignoreMap":104},[108,1321,1322,1325,1327,1329,1331,1333,1336,1338,1340,1343],{"class":110,"line":111},[108,1323,1324],{"class":988},"reg",[108,1326,1029],{"class":146},[108,1328,1032],{"class":988},[108,1330,130],{"class":150},[108,1332,1037],{"class":988},[108,1334,1335],{"class":146},"] cache_data  [",[108,1337,1037],{"class":988},[108,1339,130],{"class":150},[108,1341,1342],{"class":988},"15",[108,1344,1345],{"class":146},"];\n",[108,1347,1348,1350,1352,1355,1357,1359,1362,1364,1366,1368],{"class":110,"line":143},[108,1349,1324],{"class":988},[108,1351,1029],{"class":146},[108,1353,1354],{"class":988},"25",[108,1356,130],{"class":150},[108,1358,1037],{"class":988},[108,1360,1361],{"class":146},"] cache_tag   [",[108,1363,1037],{"class":988},[108,1365,130],{"class":150},[108,1367,1342],{"class":988},[108,1369,1345],{"class":146},[108,1371,1372,1374,1377,1379,1381,1383],{"class":110,"line":179},[108,1373,1324],{"class":988},[108,1375,1376],{"class":146},"        cache_valid [",[108,1378,1037],{"class":988},[108,1380,130],{"class":150},[108,1382,1342],{"class":988},[108,1384,1345],{"class":146},[54,1386,1387],{},"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!",[1389,1390,1391],"note",{},[54,1392,1393],{},"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.",[1288,1395,1397],{"id":1396},"address-slicing","Address Slicing",[54,1399,1400],{},"As alluded to earlier, incoming memory addresses requested by the CPU are sliced to determine each memory addresses association within the cache.",[54,1402,1403],{},[897,1404],{"alt":1405,"src":1406},"Address Slicing Diagram","\u002Fcontent\u002Fd-map-cache-photo-3.jpeg",[54,1408,1409],{},"The top 26 bits determine its tag, while the following 4 bits decide which of 16 lines that specific address belongs to.",[54,1411,1412],{},"Written in Verilog it looks like:",[99,1414,1416],{"className":966,"code":1415,"language":968,"meta":104,"style":104},"index = addr_i[5:2];\ntag   = addr_i[31:6];\n",[79,1417,1418,1438],{"__ignoreMap":104},[108,1419,1420,1423,1426,1429,1432,1434,1436],{"class":110,"line":111},[108,1421,1422],{"class":146},"index ",[108,1424,1425],{"class":150},"=",[108,1427,1428],{"class":146}," addr_i[",[108,1430,1431],{"class":988},"5",[108,1433,130],{"class":150},[108,1435,61],{"class":988},[108,1437,1345],{"class":146},[108,1439,1440,1443,1445,1447,1449,1451,1454],{"class":110,"line":143},[108,1441,1442],{"class":146},"tag   ",[108,1444,1425],{"class":150},[108,1446,1428],{"class":146},[108,1448,1032],{"class":988},[108,1450,130],{"class":150},[108,1452,1453],{"class":988},"6",[108,1455,1345],{"class":146},[54,1457,1458],{},"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!",[1288,1460,1462],{"id":1461},"hit-miss-mechanics","Hit & Miss Mechanics",[54,1464,1465,1466,1469,1470,1473,1474,1477,1478,1481],{},"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",[79,1467,1468],{},"always"," block. In essence, the two states represent an ",[79,1471,1472],{},"IDLE"," state, where data can immedeatily be written or retrieved, and a ",[79,1475,1476],{},"WAIT"," state where the CPU is stalled via its handy ",[79,1479,1480],{},"stall_i"," input.",[54,1483,1484],{},[897,1485],{"alt":1486,"src":1487},"2-state Finite State Machine Diagram","\u002Fcontent\u002Fd-map-cache-photo-1.jpeg",[54,1489,1490,1491,1494,1495,1498,1499,262],{},"Inside the cache there's a continuously assigned wire called ",[79,1492,1493],{},"hit"," which, as you might expect, detects whether the current request matches a hit condition or not. The two simple questions it asks is ",[499,1496,1497],{},"\"Is the incoming index valid?\""," ('no' here would mean a compulsory miss) and ",[499,1500,1501],{},"\"Does the incoming tag match the current tag in its associated cacheline?\"",[1503,1504,1506],"h5",{"id":1505},"read","Read",[54,1508,1509],{},"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!",[99,1511,1513],{"className":966,"code":1512,"language":968,"meta":104,"style":104},"if (hit) begin\n  rdata_o \u003C= cache_data[index];\nend\n",[79,1514,1515,1526,1537],{"__ignoreMap":104},[108,1516,1517,1520,1523],{"class":110,"line":111},[108,1518,1519],{"class":988},"if",[108,1521,1522],{"class":146}," (hit) ",[108,1524,1525],{"class":988},"begin\n",[108,1527,1528,1531,1534],{"class":110,"line":143},[108,1529,1530],{"class":146},"  rdata_o ",[108,1532,1533],{"class":150},"\u003C=",[108,1535,1536],{"class":146}," cache_data[index];\n",[108,1538,1539],{"class":110,"line":179},[108,1540,1541],{"class":988},"end\n",[54,1543,1544,1545,353,1548,1551,1552,1554,1555,1557],{},"A miss on the other hand temporarily stores the requested index and tag in ",[79,1546,1547],{},"miss_index",[79,1549,1550],{},"miss_tag"," vectors. At the same time, the cache's state is switched to ",[79,1553,1476],{}," 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 ",[79,1556,1472],{}," state again.",[1503,1559,1561],{"id":1560},"write","Write",[54,1563,1564],{},"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.",[54,1566,1567],{},"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.",[99,1569,1572],{"className":966,"code":1570,"filename":1571,"language":968,"meta":104,"style":104},"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",[79,1573,1574,1583,1592,1628,1660,1692,1723,1728,1733],{"__ignoreMap":104},[108,1575,1576,1578,1581],{"class":110,"line":111},[108,1577,1519],{"class":988},[108,1579,1580],{"class":146}," (we_i) ",[108,1582,1525],{"class":988},[108,1584,1585,1588,1590],{"class":110,"line":143},[108,1586,1587],{"class":988},"  if",[108,1589,1522],{"class":146},[108,1591,1525],{"class":988},[108,1593,1594,1597,1600,1602,1605,1608,1610,1612,1615,1617,1620,1622,1624,1626],{"class":110,"line":179},[108,1595,1596],{"class":988},"    if",[108,1598,1599],{"class":146}," (wstrb_i[",[108,1601,1037],{"class":988},[108,1603,1604],{"class":146},"]) cache_data[index][",[108,1606,1607],{"class":988},"7",[108,1609,130],{"class":150},[108,1611,1037],{"class":988},[108,1613,1614],{"class":146},"]   ",[108,1616,1533],{"class":150},[108,1618,1619],{"class":146}," wdata_i[",[108,1621,1607],{"class":988},[108,1623,130],{"class":150},[108,1625,1037],{"class":988},[108,1627,1345],{"class":146},[108,1629,1630,1632,1634,1637,1639,1641,1643,1646,1648,1650,1652,1654,1656,1658],{"class":110,"line":186},[108,1631,1596],{"class":988},[108,1633,1599],{"class":146},[108,1635,1636],{"class":988},"1",[108,1638,1604],{"class":146},[108,1640,1342],{"class":988},[108,1642,130],{"class":150},[108,1644,1645],{"class":988},"8",[108,1647,1214],{"class":146},[108,1649,1533],{"class":150},[108,1651,1619],{"class":146},[108,1653,1342],{"class":988},[108,1655,130],{"class":150},[108,1657,1645],{"class":988},[108,1659,1345],{"class":146},[108,1661,1662,1664,1666,1668,1670,1673,1675,1678,1680,1682,1684,1686,1688,1690],{"class":110,"line":196},[108,1663,1596],{"class":988},[108,1665,1599],{"class":146},[108,1667,61],{"class":988},[108,1669,1604],{"class":146},[108,1671,1672],{"class":988},"23",[108,1674,130],{"class":150},[108,1676,1677],{"class":988},"16",[108,1679,1040],{"class":146},[108,1681,1533],{"class":150},[108,1683,1619],{"class":146},[108,1685,1672],{"class":988},[108,1687,130],{"class":150},[108,1689,1677],{"class":988},[108,1691,1345],{"class":146},[108,1693,1694,1696,1698,1700,1702,1704,1706,1709,1711,1713,1715,1717,1719,1721],{"class":110,"line":210},[108,1695,1596],{"class":988},[108,1697,1599],{"class":146},[108,1699,844],{"class":988},[108,1701,1604],{"class":146},[108,1703,1032],{"class":988},[108,1705,130],{"class":150},[108,1707,1708],{"class":988},"24",[108,1710,1040],{"class":146},[108,1712,1533],{"class":150},[108,1714,1619],{"class":146},[108,1716,1032],{"class":988},[108,1718,130],{"class":150},[108,1720,1708],{"class":988},[108,1722,1345],{"class":146},[108,1724,1725],{"class":110,"line":222},[108,1726,1727],{"class":988},"  end\n",[108,1729,1730],{"class":110,"line":233},[108,1731,1732],{"class":146},"  ...\n",[108,1734,1735],{"class":110,"line":251},[108,1736,1541],{"class":988},[99,1738,1741],{"className":966,"code":1739,"filename":1740,"language":968,"meta":104,"style":104},"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",[79,1742,1743,1751,1782,1812,1842,1872],{"__ignoreMap":104},[108,1744,1745,1747,1749],{"class":110,"line":111},[108,1746,1519],{"class":988},[108,1748,1580],{"class":146},[108,1750,1525],{"class":988},[108,1752,1753,1755,1757,1759,1762,1764,1766,1768,1770,1772,1774,1776,1778,1780],{"class":110,"line":143},[108,1754,1587],{"class":988},[108,1756,1599],{"class":146},[108,1758,1037],{"class":988},[108,1760,1761],{"class":146},"]) dmem[valid_addr][",[108,1763,1607],{"class":988},[108,1765,130],{"class":150},[108,1767,1037],{"class":988},[108,1769,1614],{"class":146},[108,1771,1533],{"class":150},[108,1773,1619],{"class":146},[108,1775,1607],{"class":988},[108,1777,130],{"class":150},[108,1779,1037],{"class":988},[108,1781,1345],{"class":146},[108,1783,1784,1786,1788,1790,1792,1794,1796,1798,1800,1802,1804,1806,1808,1810],{"class":110,"line":179},[108,1785,1587],{"class":988},[108,1787,1599],{"class":146},[108,1789,1636],{"class":988},[108,1791,1761],{"class":146},[108,1793,1342],{"class":988},[108,1795,130],{"class":150},[108,1797,1645],{"class":988},[108,1799,1214],{"class":146},[108,1801,1533],{"class":150},[108,1803,1619],{"class":146},[108,1805,1342],{"class":988},[108,1807,130],{"class":150},[108,1809,1645],{"class":988},[108,1811,1345],{"class":146},[108,1813,1814,1816,1818,1820,1822,1824,1826,1828,1830,1832,1834,1836,1838,1840],{"class":110,"line":186},[108,1815,1587],{"class":988},[108,1817,1599],{"class":146},[108,1819,61],{"class":988},[108,1821,1761],{"class":146},[108,1823,1672],{"class":988},[108,1825,130],{"class":150},[108,1827,1677],{"class":988},[108,1829,1040],{"class":146},[108,1831,1533],{"class":150},[108,1833,1619],{"class":146},[108,1835,1672],{"class":988},[108,1837,130],{"class":150},[108,1839,1677],{"class":988},[108,1841,1345],{"class":146},[108,1843,1844,1846,1848,1850,1852,1854,1856,1858,1860,1862,1864,1866,1868,1870],{"class":110,"line":196},[108,1845,1587],{"class":988},[108,1847,1599],{"class":146},[108,1849,844],{"class":988},[108,1851,1761],{"class":146},[108,1853,1032],{"class":988},[108,1855,130],{"class":150},[108,1857,1708],{"class":988},[108,1859,1040],{"class":146},[108,1861,1533],{"class":150},[108,1863,1619],{"class":146},[108,1865,1032],{"class":988},[108,1867,130],{"class":150},[108,1869,1708],{"class":988},[108,1871,1345],{"class":146},[108,1873,1874],{"class":110,"line":210},[108,1875,1541],{"class":988},[49,1877,1879],{"id":1878},"testing-debugging","Testing & Debugging",[54,1881,1882,1883,1886],{},"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 ",[79,1884,1885],{},"Makefile"," already featured some configurations to build and run the hardware in a Verilator emulation.",[54,1888,1889],{},"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.",[1891,1892,1893,1898],"callout",{},[54,1894,1895],{},[499,1896,1897],{},"Example display output on physical Digilent FPGA board taken from the original repo",[54,1899,1900],{},[897,1901],{"alt":1902,"src":1903},"FPGA Display Outpu Example Photo","\u002Fcontent\u002Fd-map-cache-photo-5.png",[54,1905,1906,1907,1909],{},"Once again, the ",[79,1908,1885],{}," configuration came to the rescue to allow for a connection with a virtual display emulator letting demo the cache on the demo program.",[469,1911,1913],{"id":1912},"timing-issue","Timing Issue",[54,1915,1916],{},"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...",[1918,1919,1920,1933],"table",{},[1921,1922,1923],"thead",{},[1924,1925,1926,1930],"tr",{},[1927,1928,1929],"th",{},"Before Cache",[1927,1931,1932],{},"After Cache",[1934,1935,1936],"tbody",{},[1924,1937,1938,1945],{},[1939,1940,1941],"td",{},[897,1942],{"alt":1943,"src":1944},"Reference Demo Gif","\u002Fcontent\u002Fd-map-cache-gif-2.gif",[1939,1946,1947],{},[897,1948],{"alt":1949,"src":1950},"Buggy Demo Gif","\u002Fcontent\u002Fd-map-cache-gif-1.gif",[54,1952,1953],{},"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.",[54,1955,1956],{},[897,1957],{"alt":1958,"src":1959},"GTKWave Signal Trace Screenshot","\u002Fcontent\u002Fd-map-cache-screenshot-1.png",[54,1961,1962,1963,1965,1966,1968,1969,1971,1972,1975,1976,1978],{},"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 ",[79,1964,1254],{}," is high yet ",[79,1967,1493],{}," stays low, indicating a write miss. As you remember, a miss switches the CPU automatically to the ",[79,1970,1476],{}," state which stalls the CPU and writes the incoming ",[79,1973,1974],{},"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 ",[79,1977,1974],{}," input from the main memory is most likely not even the value we want to be storing in the first place.",[54,1980,1981],{},"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.",[469,1983,1985],{"id":1984},"measuring-performance","Measuring Performance",[54,1987,1988,1989,1992],{},"But now we're still left with the question: ",[499,1990,1991],{},"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.",[99,1994,1996],{"className":966,"code":1995,"language":968,"meta":104,"style":104},"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",[79,1997,1998,2017],{"__ignoreMap":104},[108,1999,2000,2002,2004,2007,2009,2011,2014],{"class":110,"line":111},[108,2001,1324],{"class":988},[108,2003,1029],{"class":146},[108,2005,2006],{"class":988},"63",[108,2008,130],{"class":150},[108,2010,1037],{"class":988},[108,2012,2013],{"class":146},"] cache_access_cnt;    ",[108,2015,2016],{"class":122},"\u002F\u002F Total amount of accesses\n",[108,2018,2019,2021,2023,2025,2027,2029,2032],{"class":110,"line":143},[108,2020,1324],{"class":988},[108,2022,1029],{"class":146},[108,2024,2006],{"class":988},[108,2026,130],{"class":150},[108,2028,1037],{"class":988},[108,2030,2031],{"class":146},"] cache_hit_cnt;       ",[108,2033,2034],{"class":122},"\u002F\u002F Total amount of hits\n",[54,2036,2037],{},"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.",[54,2039,2040],{},"As any rudementary counter, they are simply incremented when the performance condition is met.",[99,2042,2044],{"className":966,"code":2043,"language":968,"meta":104,"style":104},"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",[79,2045,2046,2061,2080,2098],{"__ignoreMap":104},[108,2047,2048,2050,2053,2056,2059],{"class":110,"line":111},[108,2049,1519],{"class":988},[108,2051,2052],{"class":146}," (re_i ",[108,2054,2055],{"class":150},"||",[108,2057,2058],{"class":146}," we_i) ",[108,2060,1525],{"class":988},[108,2062,2063,2066,2068,2071,2074,2077],{"class":110,"line":143},[108,2064,2065],{"class":146},"  cache_access_cnt ",[108,2067,1533],{"class":150},[108,2069,2070],{"class":146}," cache_access_cnt ",[108,2072,2073],{"class":150},"+",[108,2075,2076],{"class":988}," 1",[108,2078,2079],{"class":146},";\n",[108,2081,2082,2084,2087,2089,2092,2094,2096],{"class":110,"line":179},[108,2083,1587],{"class":988},[108,2085,2086],{"class":146}," (hit) cache_hit_cnt ",[108,2088,1533],{"class":150},[108,2090,2091],{"class":146}," cache_hit_cnt ",[108,2093,2073],{"class":150},[108,2095,2076],{"class":988},[108,2097,2079],{"class":146},[108,2099,2100],{"class":110,"line":186},[108,2101,1541],{"class":988},[54,2103,2104],{},"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.",[99,2106,2109],{"className":966,"code":2107,"highlights":2108,"language":968,"meta":104,"style":104},"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",[222,233,251,1126,1131],[79,2110,2111,2118,2131,2145,2159,2173,2187,2203,2218,2232,2256,2278,2291,2303],{"__ignoreMap":104},[108,2112,2113,2116],{"class":110,"line":111},[108,2114,2115],{"class":146},"final ",[108,2117,1525],{"class":988},[108,2119,2120,2123,2125,2127,2129],{"class":110,"line":143},[108,2121,2122],{"class":146},"  $write(",[108,2124,623],{"class":118},[108,2126,170],{"class":146},[108,2128,623],{"class":118},[108,2130,1245],{"class":146},[108,2132,2133,2135,2138,2140,2142],{"class":110,"line":179},[108,2134,2122],{"class":146},[108,2136,2137],{"class":118},"\"===> mcycle                                 : %10d",[108,2139,170],{"class":146},[108,2141,623],{"class":118},[108,2143,2144],{"class":146},", mcycle);\n",[108,2146,2147,2149,2152,2154,2156],{"class":110,"line":186},[108,2148,2122],{"class":146},[108,2150,2151],{"class":118},"\"===> minstret                               : %10d",[108,2153,170],{"class":146},[108,2155,623],{"class":118},[108,2157,2158],{"class":146},", minstret);\n",[108,2160,2161,2163,2166,2168,2170],{"class":110,"line":196},[108,2162,2122],{"class":146},[108,2164,2165],{"class":118},"\"===> Total number of branch predictions     : %10d",[108,2167,170],{"class":146},[108,2169,623],{"class":118},[108,2171,2172],{"class":146},", br_pred_cntr);\n",[108,2174,2175,2177,2180,2182,2184],{"class":110,"line":210},[108,2176,2122],{"class":146},[108,2178,2179],{"class":118},"\"===> Total number of branch mispredictions  : %10d",[108,2181,170],{"class":146},[108,2183,623],{"class":118},[108,2185,2186],{"class":146},", br_misp_cntr);\n",[108,2188,2191,2193,2196,2198,2200],{"class":2189,"line":222},[110,2190],"highlight",[108,2192,2122],{"class":146},[108,2194,2195],{"class":118},"\"===> Total number of cache hits             : %10d",[108,2197,170],{"class":146},[108,2199,623],{"class":118},[108,2201,2202],{"class":146},", m0.cache.cache_hit_cnt);\n",[108,2204,2206,2208,2211,2213,2215],{"class":2205,"line":233},[110,2190],[108,2207,2122],{"class":146},[108,2209,2210],{"class":118},"\"===> Total number of cache accesses         : %10d",[108,2212,170],{"class":146},[108,2214,623],{"class":118},[108,2216,2217],{"class":146},", m0.cache.cache_access_cnt);\n",[108,2219,2221,2223,2226,2228,2230],{"class":2220,"line":251},[110,2190],[108,2222,2122],{"class":146},[108,2224,2225],{"class":118},"\"===> Cache hit ratio                        : %6.2f%%",[108,2227,170],{"class":146},[108,2229,623],{"class":118},[108,2231,998],{"class":146},[108,2233,2235,2238,2241,2244,2247,2249,2251,2253],{"class":2234,"line":1126},[110,2190],[108,2236,2237],{"class":146},"    (m0.cache.cache_access_cnt ",[108,2239,2240],{"class":150},"==",[108,2242,2243],{"class":988}," 0",[108,2245,2246],{"class":146},") ? ",[108,2248,1037],{"class":988},[108,2250,262],{"class":146},[108,2252,1037],{"class":988},[108,2254,2255],{"class":146}," :\n",[108,2257,2259,2262,2264,2266,2269,2272,2275],{"class":2258,"line":1131},[110,2190],[108,2260,2261],{"class":988},"    100",[108,2263,262],{"class":146},[108,2265,1037],{"class":988},[108,2267,2268],{"class":150}," *",[108,2270,2271],{"class":146}," m0.cache.cache_hit_cnt ",[108,2273,2274],{"class":150},"\u002F",[108,2276,2277],{"class":146}," m0.cache.cache_access_cnt);\n",[108,2279,2280,2282,2285,2287,2289],{"class":110,"line":1143},[108,2281,2122],{"class":146},[108,2283,2284],{"class":118},"\"===> simulation finish!!",[108,2286,170],{"class":146},[108,2288,623],{"class":118},[108,2290,1245],{"class":146},[108,2292,2293,2295,2297,2299,2301],{"class":110,"line":1155},[108,2294,2122],{"class":146},[108,2296,623],{"class":118},[108,2298,170],{"class":146},[108,2300,623],{"class":118},[108,2302,1245],{"class":146},[108,2304,2305],{"class":110,"line":1177},[108,2306,1541],{"class":988},[99,2308,2312],{"className":426,"code":2309,"filename":2310,"highlights":2311,"language":429,"meta":104,"style":104},"- 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",[222,233,251],[79,2313,2314,2319,2323,2328,2333,2338,2343,2349,2355,2361,2366,2370,2375,2380],{"__ignoreMap":104},[108,2315,2316],{"class":110,"line":111},[108,2317,2318],{},"- top.v:47: Verilog $finish\n",[108,2320,2321],{"class":110,"line":143},[108,2322,183],{"emptyLinePlaceholder":182},[108,2324,2325],{"class":110,"line":179},[108,2326,2327],{},"===> mcycle                                 :     614492\n",[108,2329,2330],{"class":110,"line":186},[108,2331,2332],{},"===> minstret                               :     581925\n",[108,2334,2335],{"class":110,"line":196},[108,2336,2337],{},"===> Total number of branch predictions     :     164858\n",[108,2339,2340],{"class":110,"line":210},[108,2341,2342],{},"===> Total number of branch mispredictions  :       7511\n",[108,2344,2346],{"class":2345,"line":222},[110,2190],[108,2347,2348],{},"===> Total number of cache hits             :      26061\n",[108,2350,2352],{"class":2351,"line":233},[110,2190],[108,2353,2354],{},"===> Total number of cache accesses         :      26506\n",[108,2356,2358],{"class":2357,"line":251},[110,2190],[108,2359,2360],{},"===> Cache hit ratio                        :     98.32%\n",[108,2362,2363],{"class":110,"line":1126},[108,2364,2365],{},"===> simulation finish!!\n",[108,2367,2368],{"class":110,"line":1131},[108,2369,183],{"emptyLinePlaceholder":182},[108,2371,2372],{"class":110,"line":1143},[108,2373,2374],{},"- S i m u l a t i o n   R e p o r t: Verilator 5.050 2026-07-01\n",[108,2376,2377],{"class":110,"line":1155},[108,2378,2379],{},"- Verilator: $finish at 6us; walltime 0.118 s; speed 51.972 us\u002Fs\n",[108,2381,2382],{"class":110,"line":1177},[108,2383,2384],{},"- Verilator: cpu 0.117 s on 1 threads; allocated 2 MB\n",[469,2386,2388],{"id":2387},"effects-of-cache-aware-vs-unaware-code","Effects of Cache Aware vs Unaware Code",[54,2390,2391],{},"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.",[54,2393,2394],{},"Let's take a look at the following example:",[99,2396,2400],{"className":2397,"code":2398,"language":2399,"meta":104,"style":104},"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",[79,2401,2402,2418,2422,2435,2449,2453,2490,2508,2512,2546,2562,2576,2590,2604,2609,2613,2623],{"__ignoreMap":104},[108,2403,2404,2407,2410,2413,2416],{"class":110,"line":111},[108,2405,2406],{"class":975},"int",[108,2408,2409],{"class":146}," array",[108,2411,2412],{"class":150},"[",[108,2414,2415],{"class":988},"64",[108,2417,1345],{"class":150},[108,2419,2420],{"class":110,"line":143},[108,2421,183],{"emptyLinePlaceholder":182},[108,2423,2424,2426,2429,2432],{"class":110,"line":179},[108,2425,2406],{"class":975},[108,2427,2428],{"class":189}," main",[108,2430,2431],{"class":150},"()",[108,2433,2434],{"class":150}," {\n",[108,2436,2437,2440,2443,2445,2447],{"class":110,"line":186},[108,2438,2439],{"class":975},"  int",[108,2441,2442],{"class":982}," sum ",[108,2444,1425],{"class":150},[108,2446,2243],{"class":988},[108,2448,2079],{"class":150},[108,2450,2451],{"class":110,"line":196},[108,2452,183],{"emptyLinePlaceholder":182},[108,2454,2455,2459,2462,2464,2467,2469,2471,2474,2476,2479,2482,2484,2487],{"class":110,"line":210},[108,2456,2458],{"class":2457},"s7zQu","  for",[108,2460,2461],{"class":150}," (",[108,2463,2406],{"class":975},[108,2465,2466],{"class":982}," i ",[108,2468,1425],{"class":150},[108,2470,2243],{"class":988},[108,2472,2473],{"class":150},";",[108,2475,2466],{"class":982},[108,2477,2478],{"class":150},"\u003C",[108,2480,2481],{"class":988}," 64",[108,2483,2473],{"class":150},[108,2485,2486],{"class":982}," i",[108,2488,2489],{"class":150},"++)\n",[108,2491,2492,2495,2497,2500,2502,2504,2506],{"class":110,"line":222},[108,2493,2494],{"class":146},"    array",[108,2496,2412],{"class":150},[108,2498,2499],{"class":982},"i",[108,2501,705],{"class":150},[108,2503,646],{"class":150},[108,2505,2486],{"class":982},[108,2507,2079],{"class":150},[108,2509,2510],{"class":110,"line":233},[108,2511,183],{"emptyLinePlaceholder":182},[108,2513,2514,2516,2518,2520,2523,2525,2527,2529,2531,2533,2536,2538,2541,2544],{"class":110,"line":251},[108,2515,2458],{"class":2457},[108,2517,2461],{"class":150},[108,2519,2406],{"class":975},[108,2521,2522],{"class":982}," k ",[108,2524,1425],{"class":150},[108,2526,2243],{"class":988},[108,2528,2473],{"class":150},[108,2530,2522],{"class":982},[108,2532,2478],{"class":150},[108,2534,2535],{"class":988}," 10000",[108,2537,2473],{"class":150},[108,2539,2540],{"class":982}," k",[108,2542,2543],{"class":150},"++)",[108,2545,2434],{"class":150},[108,2547,2548,2551,2554,2556,2558,2560],{"class":110,"line":1126},[108,2549,2550],{"class":982},"    sum ",[108,2552,2553],{"class":150},"+=",[108,2555,2409],{"class":146},[108,2557,2412],{"class":150},[108,2559,1037],{"class":988},[108,2561,1345],{"class":150},[108,2563,2564,2566,2568,2570,2572,2574],{"class":110,"line":1131},[108,2565,2550],{"class":982},[108,2567,2553],{"class":150},[108,2569,2409],{"class":146},[108,2571,2412],{"class":150},[108,2573,1636],{"class":988},[108,2575,1345],{"class":150},[108,2577,2578,2580,2582,2584,2586,2588],{"class":110,"line":1143},[108,2579,2550],{"class":982},[108,2581,2553],{"class":150},[108,2583,2409],{"class":146},[108,2585,2412],{"class":150},[108,2587,61],{"class":988},[108,2589,1345],{"class":150},[108,2591,2592,2594,2596,2598,2600,2602],{"class":110,"line":1155},[108,2593,2550],{"class":982},[108,2595,2553],{"class":150},[108,2597,2409],{"class":146},[108,2599,2412],{"class":150},[108,2601,844],{"class":988},[108,2603,1345],{"class":150},[108,2605,2606],{"class":110,"line":1177},[108,2607,2608],{"class":150},"  }\n",[108,2610,2611],{"class":110,"line":1199},[108,2612,183],{"emptyLinePlaceholder":182},[108,2614,2615,2618,2621],{"class":110,"line":1222},[108,2616,2617],{"class":2457},"  return",[108,2619,2620],{"class":982}," sum",[108,2622,2079],{"class":150},[108,2624,2625],{"class":110,"line":1242},[108,2626,2627],{"class":150},"}\n",[54,2629,2630],{},"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.",[99,2632,2634],{"className":426,"code":2633,"language":429,"meta":104,"style":104},"===> Total number of cache hits             :      39996\n===> Total number of cache accesses         :      40064\n===> Cache hit ratio                        :     99.83%\n",[79,2635,2636,2641,2646],{"__ignoreMap":104},[108,2637,2638],{"class":110,"line":111},[108,2639,2640],{},"===> Total number of cache hits             :      39996\n",[108,2642,2643],{"class":110,"line":143},[108,2644,2645],{},"===> Total number of cache accesses         :      40064\n",[108,2647,2648],{"class":110,"line":179},[108,2649,2650],{},"===> Cache hit ratio                        :     99.83%\n",[54,2652,2653],{},"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.",[99,2655,2657],{"className":2397,"code":2656,"language":2399,"meta":104,"style":104},"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",[79,2658,2659,2671,2675,2685,2697,2701,2729,2745,2749,2779,2793,2807,2822,2837,2841,2845,2853],{"__ignoreMap":104},[108,2660,2661,2663,2665,2667,2669],{"class":110,"line":111},[108,2662,2406],{"class":975},[108,2664,2409],{"class":146},[108,2666,2412],{"class":150},[108,2668,2415],{"class":988},[108,2670,1345],{"class":150},[108,2672,2673],{"class":110,"line":143},[108,2674,183],{"emptyLinePlaceholder":182},[108,2676,2677,2679,2681,2683],{"class":110,"line":179},[108,2678,2406],{"class":975},[108,2680,2428],{"class":189},[108,2682,2431],{"class":150},[108,2684,2434],{"class":150},[108,2686,2687,2689,2691,2693,2695],{"class":110,"line":186},[108,2688,2439],{"class":975},[108,2690,2442],{"class":982},[108,2692,1425],{"class":150},[108,2694,2243],{"class":988},[108,2696,2079],{"class":150},[108,2698,2699],{"class":110,"line":196},[108,2700,183],{"emptyLinePlaceholder":182},[108,2702,2703,2705,2707,2709,2711,2713,2715,2717,2719,2721,2723,2725,2727],{"class":110,"line":210},[108,2704,2458],{"class":2457},[108,2706,2461],{"class":150},[108,2708,2406],{"class":975},[108,2710,2466],{"class":982},[108,2712,1425],{"class":150},[108,2714,2243],{"class":988},[108,2716,2473],{"class":150},[108,2718,2466],{"class":982},[108,2720,2478],{"class":150},[108,2722,2481],{"class":988},[108,2724,2473],{"class":150},[108,2726,2486],{"class":982},[108,2728,2489],{"class":150},[108,2730,2731,2733,2735,2737,2739,2741,2743],{"class":110,"line":222},[108,2732,2494],{"class":146},[108,2734,2412],{"class":150},[108,2736,2499],{"class":982},[108,2738,705],{"class":150},[108,2740,646],{"class":150},[108,2742,2486],{"class":982},[108,2744,2079],{"class":150},[108,2746,2747],{"class":110,"line":233},[108,2748,183],{"emptyLinePlaceholder":182},[108,2750,2751,2753,2755,2757,2759,2761,2763,2765,2767,2769,2771,2773,2775,2777],{"class":110,"line":251},[108,2752,2458],{"class":2457},[108,2754,2461],{"class":150},[108,2756,2406],{"class":975},[108,2758,2522],{"class":982},[108,2760,1425],{"class":150},[108,2762,2243],{"class":988},[108,2764,2473],{"class":150},[108,2766,2522],{"class":982},[108,2768,2478],{"class":150},[108,2770,2535],{"class":988},[108,2772,2473],{"class":150},[108,2774,2540],{"class":982},[108,2776,2543],{"class":150},[108,2778,2434],{"class":150},[108,2780,2781,2783,2785,2787,2789,2791],{"class":110,"line":1126},[108,2782,2550],{"class":982},[108,2784,2553],{"class":150},[108,2786,2409],{"class":146},[108,2788,2412],{"class":150},[108,2790,1037],{"class":988},[108,2792,1345],{"class":150},[108,2794,2795,2797,2799,2801,2803,2805],{"class":110,"line":1131},[108,2796,2550],{"class":982},[108,2798,2553],{"class":150},[108,2800,2409],{"class":146},[108,2802,2412],{"class":150},[108,2804,1677],{"class":988},[108,2806,1345],{"class":150},[108,2808,2809,2811,2813,2815,2817,2820],{"class":110,"line":1143},[108,2810,2550],{"class":982},[108,2812,2553],{"class":150},[108,2814,2409],{"class":146},[108,2816,2412],{"class":150},[108,2818,2819],{"class":988},"32",[108,2821,1345],{"class":150},[108,2823,2824,2826,2828,2830,2832,2835],{"class":110,"line":1155},[108,2825,2550],{"class":982},[108,2827,2553],{"class":150},[108,2829,2409],{"class":146},[108,2831,2412],{"class":150},[108,2833,2834],{"class":988},"48",[108,2836,1345],{"class":150},[108,2838,2839],{"class":110,"line":1177},[108,2840,2608],{"class":150},[108,2842,2843],{"class":110,"line":1199},[108,2844,183],{"emptyLinePlaceholder":182},[108,2846,2847,2849,2851],{"class":110,"line":1222},[108,2848,2617],{"class":2457},[108,2850,2620],{"class":982},[108,2852,2079],{"class":150},[108,2854,2855],{"class":110,"line":1242},[108,2856,2627],{"class":150},[54,2858,2859,2860,2863,2864,2863,2867,353,2870,2873],{},"The second example does pretty much the opposite. By jumping between ",[79,2861,2862],{},"array[0]",", ",[79,2865,2866],{},"array[16]",[79,2868,2869],{},"array[32]",[79,2871,2872],{},"array[48]",", every access maps to the same cache index and continuously evicts the previous value.",[99,2875,2877],{"className":426,"code":2876,"language":429,"meta":104,"style":104},"===> Total number of branch mispredictions  :          6\n===> Total number of cache hits             :          0\n===> Total number of cache accesses         :         68\n",[79,2878,2879,2884,2889],{"__ignoreMap":104},[108,2880,2881],{"class":110,"line":111},[108,2882,2883],{},"===> Total number of branch mispredictions  :          6\n",[108,2885,2886],{"class":110,"line":143},[108,2887,2888],{},"===> Total number of cache hits             :          0\n",[108,2890,2891],{"class":110,"line":179},[108,2892,2893],{},"===> Total number of cache accesses         :         68\n",[54,2895,2896],{},"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.",[795,2898,2899],{},"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":104,"searchDepth":143,"depth":143,"links":2901},[2902,2903,2906,2910,2914],{"id":51,"depth":179,"text":52},{"id":64,"depth":143,"text":65,"children":2904},[2905],{"id":877,"depth":179,"text":878},{"id":925,"depth":143,"text":926,"children":2907},[2908,2909],{"id":932,"depth":179,"text":933},{"id":939,"depth":179,"text":940},{"id":946,"depth":143,"text":947,"children":2911},[2912,2913],{"id":959,"depth":179,"text":960},{"id":1277,"depth":179,"text":1278},{"id":1878,"depth":143,"text":1879,"children":2915},[2916,2917,2918],{"id":1912,"depth":179,"text":1913},{"id":1984,"depth":179,"text":1985},{"id":2387,"depth":179,"text":2388},"#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":2921},[2927,2928,2929,2930],"RISC-V","Verilog","FPGA","Architecture","egBwE_OqfcRQullVBh5mvHKl7x5njJo1M-GQsWaJG3M",{"id":2933,"title":36,"body":2934,"color":4167,"date":4168,"description":4169,"draft":21,"extension":811,"image":4170,"meta":4171,"navigation":182,"oneliner":4172,"path":37,"seo":4173,"stem":38,"tags":4174,"url":822,"__hash__":4177},"work\u002Fwork\u002Fpico-router.md",{"type":46,"value":2935,"toc":4148},[2936,2938,2941,2947,2952,2954,2956,2967,2975,2978,2982,2985,2988,3044,3050,3055,3059,3062,3075,3078,3114,3118,3127,3142,3146,3149,3162,3223,3227,3234,3239,3242,3255,3262,3274,3277,3281,3288,3310,3333,3342,3362,3865,3869,3880,3897,3908,3918,3938,3942,3945,3950,3953,3980,3987,3998,4001,4005,4008,4015,4026,4029,4058,4065,4071,4075,4082,4085,4089,4096,4101,4105,4108,4145],[469,2937,52],{"id":51},[54,2939,2940],{},"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.",[2942,2943,2944],"warning",{},[54,2945,2946],{},"This article is based on Pico Router v0.2.0",[267,2948,2950],{"icon":847,"target":75,"title":848,"to":2949},"https:\u002F\u002Fgithub.com\u002FPico-Router\u002FPico-Router",[54,2951,852],{},[58,2953],{"exclude":60,":max-depth":844},[49,2955,65],{"id":64},[54,2957,2958,2959,2962,2963,2966],{},"Pico Router grew out of my preparations for ",[70,2960,2961],{"href":15},"GSoC 2026",". During this period I spent several months exploring routing engines and the OpenStreetMap ecosystem. After being rejected I thought ",[499,2964,2965],{},"\"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.",[54,2968,2969,2970,2974],{},"Originally setting out to build an exhaustive routing firmware ecosystem, I later scaled back my ambitions slightly by first perfecting the routing core (read ",[499,2971,2972],{},[70,2973,18],{"href":19}," for more details). This effectively turned Pico Router into an embedded C++ routing library.",[54,2976,2977],{},"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.",[49,2979,2981],{"id":2980},"pico-router-in-a-nutshell","Pico Router in a Nutshell",[54,2983,2984],{},"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.",[54,2986,2987],{},"The bulk of the library is abstracted behind a relatively simple interface.",[99,2989,2993],{"className":2990,"code":2991,"language":2992,"meta":104,"style":104},"language-cpp shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","Path calculatePath(\n  const Graph& graph,\n  uint32_t start_node,\n  uint32_t goal_node\n)\n","cpp",[79,2994,2995,3006,3023,3033,3040],{"__ignoreMap":104},[108,2996,2997,3000,3003],{"class":110,"line":111},[108,2998,2999],{"class":114},"Path",[108,3001,3002],{"class":189}," calculatePath",[108,3004,3005],{"class":150},"(\n",[108,3007,3008,3011,3014,3017,3021],{"class":110,"line":143},[108,3009,3010],{"class":975},"  const",[108,3012,3013],{"class":114}," Graph",[108,3015,3016],{"class":975},"&",[108,3018,3020],{"class":3019},"sHdIc"," graph",[108,3022,998],{"class":150},[108,3024,3025,3028,3031],{"class":110,"line":179},[108,3026,3027],{"class":975},"  uint32_t",[108,3029,3030],{"class":3019}," start_node",[108,3032,998],{"class":150},[108,3034,3035,3037],{"class":110,"line":186},[108,3036,3027],{"class":975},[108,3038,3039],{"class":3019}," goal_node\n",[108,3041,3042],{"class":110,"line":196},[108,3043,176],{"class":150},[54,3045,3046,3047,3049],{},"Here the returned ",[79,3048,2999],{}," struct is simply an array nodes found to be the optimal path.",[3051,3052],"video-gif",{"alt":3053,"src":3054},"high-level a* explainer gif","\u002Fcontent\u002Fpico-router-video-1.webm",[469,3056,3058],{"id":3057},"the-algorithm","The Algorithm",[54,3060,3061],{},"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.",[54,3063,3064,3065,3070,3071,330],{},"The positional nature of road network nodes gave me an easy starting point for the heuristic function, namely the ",[70,3066,3069],{"href":3067,"rel":3068,"target":75},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FEuclidean_distance",[74],"Euclidean distance"," function. Having this computation encapsulated gave me a great surface to experiment on using A\u002FB methods (see ",[70,3072,3074],{"href":3073},"#benchmarking-and-profiling","Benchmarking and Profiling",[54,3076,3077],{},"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.",[1891,3079,3080,3087],{},[54,3081,3082,3083,3086],{},"Some ",[79,3084,3085],{},"Graph"," member functions",[99,3088,3090],{"className":2990,"code":3089,"language":2992,"meta":104,"style":104},"getNeighbors();\ngetCoordinates();\ngetNodeCount();\n",[79,3091,3092,3100,3107],{"__ignoreMap":104},[108,3093,3094,3097],{"class":110,"line":111},[108,3095,3096],{"class":189},"getNeighbors",[108,3098,3099],{"class":150},"();\n",[108,3101,3102,3105],{"class":110,"line":143},[108,3103,3104],{"class":189},"getCoordinates",[108,3106,3099],{"class":150},[108,3108,3109,3112],{"class":110,"line":179},[108,3110,3111],{"class":189},"getNodeCount",[108,3113,3099],{"class":150},[469,3115,3117],{"id":3116},"why-c","Why C++?",[54,3119,3120,3121,3126],{},"Firstly, my choices were quickly limited to C and C++ due to their native compatibility with the official ",[70,3122,3125],{"href":3123,"rel":3124},"https:\u002F\u002Fgithub.com\u002Fraspberrypi\u002Fpico-sdk",[74],"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.",[54,3128,3129,3130,3133,3134,3137,3138,3141],{},"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 ",[79,3131,3132],{},"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 ",[79,3135,3136],{},"std::span"," (introduced in C++20) allowed me to roll my own contiguous pointer ",[79,3139,3140],{},"EdgeRange"," to return a node's connected neighbors.",[49,3143,3145],{"id":3144},"designing-a-router-for-the-pico","Designing a Router for the Pico",[54,3147,3148],{},"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.",[54,3150,3151,3152,3157,3158,3161],{},"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 ",[70,3153,3156],{"href":3154,"rel":3155,"target":75},"https:\u002F\u002Fgithub.com\u002Fmatgla\u002FRenode_RP2040",[74],"platform description"," is for the Pi Pico (RP2040). Point being, my mindset was \"",[499,3159,3160],{},"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.",[1918,3163,3164,3177],{},[1921,3165,3166],{},[1924,3167,3168,3171,3174],{},[1927,3169,3170],{},"Specs",[1927,3172,3173],{},"RP2040",[1927,3175,3176],{},"RP2350",[1934,3178,3179,3190,3201,3212],{},[1924,3180,3181,3184,3187],{},[1939,3182,3183],{},"CPU",[1939,3185,3186],{},"Dual-core Cortex-M0+",[1939,3188,3189],{},"Dual-core Cortex-M33 or Hazard3 RISC-V",[1924,3191,3192,3195,3198],{},[1939,3193,3194],{},"Clock",[1939,3196,3197],{},"133 MHz",[1939,3199,3200],{},"150 MHz",[1924,3202,3203,3206,3209],{},[1939,3204,3205],{},"SRAM",[1939,3207,3208],{},"264 KB",[1939,3210,3211],{},"520 KB",[1924,3213,3214,3217,3220],{},[1939,3215,3216],{},"External flash",[1939,3218,3219],{},"Up to 16 MB",[1939,3221,3222],{},"Up to 32 MB",[469,3224,3226],{"id":3225},"the-problem-and-constraints","The Problem and Constraints",[54,3228,3229,3230,3233],{},"From the very start I saw memory usage as a kind of promise to the user. When using Pico Router you will ",[502,3231,3232],{},"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.",[1389,3235,3236],{},[54,3237,3238],{},"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.",[54,3240,3241],{},"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",[1891,3243,3244,3247],{},[54,3245,3246],{},"Estimated RP2040 SRAM Budget Breakdown (264 KB Total)",[3248,3249],"chart",{":datasets":3250,":labels":3251,":type":3252,":donut":3253,":show-grid":3254,":show-tooltip":3254},"[{\"data\":[15,35,35,15],\"backgroundColor\":[\"#f9a04a\",\"#ff8100\",\"#d96f00\",\"#b85a00\"]}]","[\"System & Pico SDK\",\"User Application\",\"Pico Router Peak Overhead\",\"Safety Margin\"]","pie","true","false",[54,3256,3257,3258,3261],{},"I would like to make it clear that this memory constraint poses a ",[502,3259,3260],{},"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.",[1891,3263,3264,3267],{},[54,3265,3266],{},"Routing engine runtime memory usage comparison",[3248,3268],{":datasets":3269,":labels":3270,":type":3271,"index-axis":3272,"x-title":3273},"[{\"label\":\"Memory Usage\",\"data\":[6,7,20,24.17,25,26.91]}]","[\"Pico Router\",\"OsmAnd\",\"GraphHopper\",\"Valhalla\",\"OSRM\"]","bar","y","log₂(memory \u002F KiB)",[54,3275,3276],{},"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.",[469,3278,3280],{"id":3279},"the-graph-representation","The Graph Representation",[54,3282,3283,3284,3287],{},"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 ",[79,3285,3286],{},"v0.2.0"," I've taken three-ish attempts at creating the graph data structure with each iteration improving on the previous.",[54,3289,3290,3291,2863,3293,2863,3296,3299,3300,3302,3303,3306,3307,3309],{},"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 (",[79,3292,3085],{},[79,3294,3295],{},"Edge",[79,3297,3298],{},"Node",") containing all the graph's state. The ",[79,3301,3298],{}," struct was notably large containing F-score, G-score, XY coordinates, and heuristic fields. On top of that, it had both an ",[79,3304,3305],{},"edge_count"," property and a hardcoded 4-index ",[79,3308,3295],{}," array as member data (I can not remember why it had this but I assume it was meant to be temporary xD).",[54,3311,3312,3313,3315,3316,3319,3320,3322,3323,3325,3326,3329,3330,3332],{},"The second try (version 1) maintained the same rough outline by keeping the three structs while changing their relations. Many of the ",[79,3314,3298],{}," fields were moved to the ",[79,3317,3318],{},"pathfind::Astar"," class and ",[79,3321,3085],{}," 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. ",[79,3324,3298],{}," contains ",[79,3327,3328],{},"first_edge_index"," which points at the its first edge. Then each consecutive ",[79,3331,3295],{}," will point to the next edge in line until reaching a 'no next edge' sentinel value.",[1891,3334,3335,3338],{},[54,3336,3337],{},"This animation gives a pretty good idea of how my linked list edge exploration works conceptually",[3051,3339],{"alt":3340,"src":3341},"linked edge graph explainer gif","\u002Fcontent\u002Fpico-router-video-2.mp4",[54,3343,3344,3345,3350,3351,3353,3354,3357,3358,3361],{},"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 ",[70,3346,3349],{"href":3347,"rel":3348,"target":75},"https:\u002F\u002Fwww.boost.org\u002Fdoc\u002Flibs\u002F1_61_0\u002Flibs\u002Fgraph\u002Fdoc\u002Fcompressed_sparse_row.html",[74],"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 ",[79,3352,3298],{}," coordinates are now stored in a separate ",[79,3355,3356],{},"Coords"," struct. To see the performance boost I mention earlier check out ",[70,3359,3074],{"href":3360},"#bench-comparison"," below.",[3363,3364,3365,3527,3692],"code-group",{},[99,3366,3369],{"className":2990,"code":3367,"filename":3368,"language":2992,"meta":104,"style":104},"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",[79,3370,3371,3380,3389,3394,3398,3407,3416,3429,3438,3442,3446,3455,3465,3479,3498,3513,3523],{"__ignoreMap":104},[108,3372,3373,3376,3378],{"class":110,"line":111},[108,3374,3375],{"class":975},"struct",[108,3377,3013],{"class":114},[108,3379,2434],{"class":150},[108,3381,3382,3384,3387],{"class":110,"line":143},[108,3383,3027],{"class":975},[108,3385,3386],{"class":146}," graph_id",[108,3388,2079],{"class":150},[108,3390,3391],{"class":110,"line":179},[108,3392,3393],{"class":150},"};\n",[108,3395,3396],{"class":110,"line":186},[108,3397,183],{"emptyLinePlaceholder":182},[108,3399,3400,3402,3405],{"class":110,"line":196},[108,3401,3375],{"class":975},[108,3403,3404],{"class":114}," Edge",[108,3406,2434],{"class":150},[108,3408,3409,3411,3414],{"class":110,"line":210},[108,3410,3027],{"class":975},[108,3412,3413],{"class":146}," edge_id",[108,3415,2079],{"class":150},[108,3417,3418,3421,3424,3426],{"class":110,"line":222},[108,3419,3420],{"class":975},"  uint16_t",[108,3422,3423],{"class":146}," to",[108,3425,2473],{"class":150},[108,3427,3428],{"class":982}," \n",[108,3430,3431,3433,3436],{"class":110,"line":233},[108,3432,3420],{"class":975},[108,3434,3435],{"class":146}," cost",[108,3437,2079],{"class":150},[108,3439,3440],{"class":110,"line":251},[108,3441,3393],{"class":150},[108,3443,3444],{"class":110,"line":1126},[108,3445,183],{"emptyLinePlaceholder":182},[108,3447,3448,3450,3453],{"class":110,"line":1131},[108,3449,3375],{"class":975},[108,3451,3452],{"class":114}," Node",[108,3454,2434],{"class":150},[108,3456,3457,3460,3463],{"class":110,"line":1143},[108,3458,3459],{"class":975},"  int32_t",[108,3461,3462],{"class":146}," node_id",[108,3464,2079],{"class":150},[108,3466,3467,3469,3472,3474,3477],{"class":110,"line":1155},[108,3468,3459],{"class":975},[108,3470,3471],{"class":146}," x",[108,3473,626],{"class":150},[108,3475,3476],{"class":146}," y",[108,3478,2079],{"class":150},[108,3480,3481,3483,3486,3488,3491,3493,3496],{"class":110,"line":1177},[108,3482,3459],{"class":975},[108,3484,3485],{"class":146}," g",[108,3487,626],{"class":150},[108,3489,3490],{"class":146}," h",[108,3492,626],{"class":150},[108,3494,3495],{"class":146}," f",[108,3497,2079],{"class":150},[108,3499,3500,3503,3506,3508,3511],{"class":110,"line":1199},[108,3501,3502],{"class":114},"  Edge",[108,3504,3505],{"class":146}," edges",[108,3507,2412],{"class":150},[108,3509,3510],{"class":988},"4",[108,3512,1345],{"class":150},[108,3514,3515,3518,3521],{"class":110,"line":1222},[108,3516,3517],{"class":975},"  uint8_t",[108,3519,3520],{"class":146}," edge_count",[108,3522,2079],{"class":150},[108,3524,3525],{"class":110,"line":1242},[108,3526,3393],{"class":150},[99,3528,3531],{"className":2990,"code":3529,"filename":3530,"language":2992,"meta":104,"style":104},"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",[79,3532,3533,3541,3550,3558,3567,3571,3575,3583,3595,3607,3620,3624,3628,3636,3665,3688],{"__ignoreMap":104},[108,3534,3535,3537,3539],{"class":110,"line":111},[108,3536,3375],{"class":975},[108,3538,3404],{"class":114},[108,3540,2434],{"class":150},[108,3542,3543,3545,3548],{"class":110,"line":143},[108,3544,3420],{"class":975},[108,3546,3547],{"class":146}," target",[108,3549,2079],{"class":150},[108,3551,3552,3554,3556],{"class":110,"line":179},[108,3553,3420],{"class":975},[108,3555,3435],{"class":146},[108,3557,2079],{"class":150},[108,3559,3560,3562,3565],{"class":110,"line":186},[108,3561,3027],{"class":975},[108,3563,3564],{"class":146}," next_edge_index",[108,3566,2079],{"class":150},[108,3568,3569],{"class":110,"line":196},[108,3570,3393],{"class":150},[108,3572,3573],{"class":110,"line":210},[108,3574,183],{"emptyLinePlaceholder":182},[108,3576,3577,3579,3581],{"class":110,"line":222},[108,3578,3375],{"class":975},[108,3580,3452],{"class":114},[108,3582,2434],{"class":150},[108,3584,3585,3587,3589,3591,3593],{"class":110,"line":233},[108,3586,3459],{"class":975},[108,3588,3471],{"class":146},[108,3590,646],{"class":150},[108,3592,2243],{"class":988},[108,3594,2079],{"class":150},[108,3596,3597,3599,3601,3603,3605],{"class":110,"line":251},[108,3598,3459],{"class":975},[108,3600,3476],{"class":146},[108,3602,646],{"class":150},[108,3604,2243],{"class":988},[108,3606,2079],{"class":150},[108,3608,3609,3611,3614,3616,3618],{"class":110,"line":1126},[108,3610,3027],{"class":975},[108,3612,3613],{"class":146}," first_edge_index",[108,3615,646],{"class":150},[108,3617,2243],{"class":988},[108,3619,2079],{"class":150},[108,3621,3622],{"class":110,"line":1131},[108,3623,3393],{"class":150},[108,3625,3626],{"class":110,"line":1143},[108,3627,183],{"emptyLinePlaceholder":182},[108,3629,3630,3632,3634],{"class":110,"line":1155},[108,3631,3375],{"class":975},[108,3633,3013],{"class":114},[108,3635,2434],{"class":150},[108,3637,3638,3641,3644,3647,3649,3651,3653,3656,3659,3662],{"class":110,"line":1177},[108,3639,3640],{"class":114},"  std",[108,3642,3643],{"class":150},"::",[108,3645,3646],{"class":114},"array",[108,3648,2478],{"class":150},[108,3650,3298],{"class":114},[108,3652,626],{"class":150},[108,3654,3655],{"class":114}," MAX_NODES",[108,3657,3658],{"class":150},">",[108,3660,3661],{"class":146}," nodes",[108,3663,3664],{"class":150},"{};\n",[108,3666,3667,3669,3671,3673,3675,3677,3679,3682,3684,3686],{"class":110,"line":1199},[108,3668,3640],{"class":114},[108,3670,3643],{"class":150},[108,3672,3646],{"class":114},[108,3674,2478],{"class":150},[108,3676,3295],{"class":114},[108,3678,626],{"class":150},[108,3680,3681],{"class":114}," MAX_EDGES",[108,3683,3658],{"class":150},[108,3685,3505],{"class":146},[108,3687,3664],{"class":150},[108,3689,3690],{"class":110,"line":1222},[108,3691,3393],{"class":150},[99,3693,3696],{"className":2990,"code":3694,"filename":3695,"language":2992,"meta":104,"style":104},"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",[79,3697,3698,3706,3714,3722,3726,3730,3739,3747,3755,3759,3763,3771,3781,3790,3798,3802,3806,3814,3837,3860],{"__ignoreMap":104},[108,3699,3700,3702,3704],{"class":110,"line":111},[108,3701,3375],{"class":975},[108,3703,3404],{"class":114},[108,3705,2434],{"class":150},[108,3707,3708,3710,3712],{"class":110,"line":143},[108,3709,3027],{"class":975},[108,3711,3547],{"class":146},[108,3713,2079],{"class":150},[108,3715,3716,3718,3720],{"class":110,"line":179},[108,3717,3027],{"class":975},[108,3719,3435],{"class":146},[108,3721,2079],{"class":150},[108,3723,3724],{"class":110,"line":186},[108,3725,3393],{"class":150},[108,3727,3728],{"class":110,"line":196},[108,3729,183],{"emptyLinePlaceholder":182},[108,3731,3732,3734,3737],{"class":110,"line":210},[108,3733,3375],{"class":975},[108,3735,3736],{"class":114}," Coords",[108,3738,2434],{"class":150},[108,3740,3741,3743,3745],{"class":110,"line":222},[108,3742,3459],{"class":975},[108,3744,3471],{"class":146},[108,3746,2079],{"class":150},[108,3748,3749,3751,3753],{"class":110,"line":233},[108,3750,3459],{"class":975},[108,3752,3476],{"class":146},[108,3754,2079],{"class":150},[108,3756,3757],{"class":110,"line":251},[108,3758,3393],{"class":150},[108,3760,3761],{"class":110,"line":1126},[108,3762,183],{"emptyLinePlaceholder":182},[108,3764,3765,3767,3769],{"class":110,"line":1131},[108,3766,3375],{"class":975},[108,3768,3452],{"class":114},[108,3770,2434],{"class":150},[108,3772,3773,3776,3779],{"class":110,"line":1143},[108,3774,3775],{"class":114},"  Coords",[108,3777,3778],{"class":146}," coordinates",[108,3780,2079],{"class":150},[108,3782,3783,3785,3788],{"class":110,"line":1155},[108,3784,3027],{"class":975},[108,3786,3787],{"class":146}," edge_offset",[108,3789,2079],{"class":150},[108,3791,3792,3794,3796],{"class":110,"line":1177},[108,3793,3027],{"class":975},[108,3795,3520],{"class":146},[108,3797,2079],{"class":150},[108,3799,3800],{"class":110,"line":1199},[108,3801,3393],{"class":150},[108,3803,3804],{"class":110,"line":1222},[108,3805,183],{"emptyLinePlaceholder":182},[108,3807,3808,3810,3812],{"class":110,"line":1242},[108,3809,3375],{"class":975},[108,3811,3013],{"class":114},[108,3813,2434],{"class":150},[108,3815,3817,3819,3821,3823,3825,3827,3829,3831,3833,3835],{"class":110,"line":3816},18,[108,3818,3640],{"class":114},[108,3820,3643],{"class":150},[108,3822,3646],{"class":114},[108,3824,2478],{"class":150},[108,3826,3298],{"class":114},[108,3828,626],{"class":150},[108,3830,3655],{"class":114},[108,3832,3658],{"class":150},[108,3834,3661],{"class":146},[108,3836,3664],{"class":150},[108,3838,3840,3842,3844,3846,3848,3850,3852,3854,3856,3858],{"class":110,"line":3839},19,[108,3841,3640],{"class":114},[108,3843,3643],{"class":150},[108,3845,3646],{"class":114},[108,3847,2478],{"class":150},[108,3849,3295],{"class":114},[108,3851,626],{"class":150},[108,3853,3681],{"class":114},[108,3855,3658],{"class":150},[108,3857,3505],{"class":146},[108,3859,3664],{"class":150},[108,3861,3863],{"class":110,"line":3862},20,[108,3864,3393],{"class":150},[469,3866,3868],{"id":3867},"keeping-memory-predictable","Keeping Memory Predictable",[54,3870,3871,3872,3875,3876,3879],{},"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. \"",[499,3873,3874],{},"Arrays of what","?\" I hear you ask. The answer is that they all contain node ID's, which are just type alias of a ",[79,3877,3878],{},"uint32_t",". 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.",[914,3881,3882],{},[54,3883,3884,3885,3888,3889,3892,3893,3896],{},"In the near future I'd like to look into evaluating the maximum amount of nodes at compile time to lower ",[79,3886,3887],{},"node_id"," to ",[79,3890,3891],{},"uint16_t"," or maybe even ",[79,3894,3895],{},"uint8_t"," for tiny graphs, saving the user a boatload of space.",[54,3898,3899,3900,3903,3904,3907],{},"The next question you might ask is \"",[499,3901,3902],{},"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 ",[79,3905,3906],{},"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.",[54,3909,3910,3911,3913,3914,3917],{},"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 ",[79,3912,3132],{}," allows me to track the static memory usage by running ",[79,3915,3916],{},"arm-none-eabi-size"," on the compiled ELF path. This then also gets printed alongside the other objects memory numbers.",[1891,3919,3920,3932],{},[54,3921,3922,3923,2863,3926,383,3929],{},"Memory report result when ",[79,3924,3925],{},"MAX_NODES = 1000",[79,3927,3928],{},"MAX_EDGES = 4000",[79,3930,3931],{},"MAX_PATH_LENGTH = 100",[54,3933,3934],{},[897,3935],{"alt":3936,"src":3937},"memory report output screenshot","\u002Fcontent\u002Fpico-router-screenshot-3.jpeg",[49,3939,3941],{"id":3940},"making-the-first-version-work","Making the First Version Work",[54,3943,3944],{},"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.",[1389,3946,3947],{},[54,3948,3949],{},"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.",[54,3951,3952],{},"I subsequently worked in this environment until fulfilling handful of checkboxes:",[3954,3955,3956,3959,3966,3973],"ol",{},[348,3957,3958],{},"Make a graph, any graph that is, as long as its compatible with the algorithm;",[348,3960,3961,3962,3965],{},"Get a functional version of an ",[79,3963,3964],{},"Astar"," class;",[348,3967,3968,3969,3972],{},"Make the ",[79,3970,3971],{},"main"," loop execute a path traversal demo on static graph fixture;",[348,3974,3975,3976,3979],{},"Print a pretty banner and some basic ",[79,3977,3978],{},"std::chrono"," benchmarks from the demo run.",[54,3981,3982,3983,3986],{},"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 ",[79,3984,3985],{},"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.",[54,3988,3989,3990,3993,3994,3997],{},"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 ",[79,3991,3992],{},"MAX_NODES"," and  ",[79,3995,3996],{},"MAX_EDGES"," constants way too high causing the program to allocate out-of-bounds memory and instantly killing the Pico.",[54,3999,4000],{},"With that fixed, I was able succesfully run the demo you see here below:",[3051,4002],{"alt":4003,"src":4004},"first on-device demo gif","\u002Fcontent\u002Fpico-router-video-4.webm",[49,4006,3074],{"id":4007},"benchmarking-and-profiling",[54,4009,4010,4011,4014],{},"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 ",[79,4012,4013],{},"std::chrono::steady_clock"," to loop over the main demo traversal several thousand times and return the average wall clock time.",[54,4016,4017,4018,4021,4022,4025],{},"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 ",[79,4019,4020],{},"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 (",[79,4023,4024],{},"std::mt19937",") allowed me to make generations random yet deterministic using a seed int.",[54,4027,4028],{},"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.",[1891,4030,4031,4037,4043],{},[54,4032,4033,4034],{},"Benchmark results on Apple M4\n",[70,4035],{"id":4036},"bench-comparison",[54,4038,4039],{},[897,4040],{"alt":4041,"src":4042},"benchmarks output screenshot","\u002Fcontent\u002Fpico-router-screenshot-1.jpeg",[1389,4044,4045],{},[54,4046,4047,4048,2863,4051,4054,4055,4057],{},"In ",[79,4049,4050],{},"BM_Astar_Grid\u002F[x]\u002F[y]",[79,4052,4053],{},"x"," denotes the grid dimension, i.e. the number of nodes along each dimension. ",[79,4056,3272],{}," 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.",[54,4059,4060,4061,4064],{},"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 ",[79,4062,4063],{},"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%.",[54,4066,4067],{},[897,4068],{"alt":4069,"src":4070},"historical benchmarks chart","\u002Fcontent\u002Fpico-router-screenshot-2.jpeg",[49,4072,4074],{"id":4073},"whats-left","What's Left?",[54,4076,4077,4078,4081],{},"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 ",[79,4079,4080],{},"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.",[54,4083,4084],{},"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.",[469,4086,4088],{"id":4087},"using-arbitrary-osm-data","Using arbitrary OSM data",[54,4090,4091,4092,4095],{},"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 ",[79,4093,4094],{},"osm-convert"," 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.",[1389,4097,4098],{},[54,4099,4100],{},"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.",[469,4102,4104],{"id":4103},"flash-storage-and-tile-caching","Flash Storage and Tile Caching",[54,4106,4107],{},"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).",[4109,4110,4111],"accordion",{},[4112,4113,4115,4122,4125,4142],"accordion-item",{"label":4114},"If you're interested, here's a deep-dive into some architectural ideas for multi-tiered graph storage.",[54,4116,4117,4118,4121],{},"Perhaps easiest of all is the allocation of medium-sized graphs on the heap. I'm thinking returning a simple ",[79,4119,4120],{},"std::unique_ptr"," will only fragment the graph into a few chunks and it will nicely clean up after itself of course.",[54,4123,4124],{},"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.",[1891,4126,4127,4136],{},[54,4128,4129,4130,4135],{},"Visualization of Morton's Z-order space-filling curve (",[70,4131,4134],{"href":4132,"rel":4133,"target":75},"https:\u002F\u002Ftex.stackexchange.com\u002Fquestions\u002F347601\u002Ftikz-lebesgue-curve-z-curve",[74],"source",")",[54,4137,4138],{},[897,4139],{"alt":4140,"src":4141},"z-order curve explainer gif","\u002Fcontent\u002Fpico-router-video-3.gif",[54,4143,4144],{},"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.",[795,4146,4147],{},"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":104,"searchDepth":143,"depth":143,"links":4149},[4150,4151,4152,4156,4161,4162,4163],{"id":51,"depth":179,"text":52},{"id":64,"depth":143,"text":65},{"id":2980,"depth":143,"text":2981,"children":4153},[4154,4155],{"id":3057,"depth":179,"text":3058},{"id":3116,"depth":179,"text":3117},{"id":3144,"depth":143,"text":3145,"children":4157},[4158,4159,4160],{"id":3225,"depth":179,"text":3226},{"id":3279,"depth":179,"text":3280},{"id":3867,"depth":179,"text":3868},{"id":3940,"depth":143,"text":3941},{"id":4007,"depth":143,"text":3074},{"id":4073,"depth":143,"text":4074,"children":4164},[4165,4166],{"id":4087,"depth":179,"text":4088},{"id":4103,"depth":179,"text":4104},"#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":4169},[4175,820,4176,821],"Embedded Systems","OpenStreetMap","_0VmfGZ8Lx92pgjE95cJqtqvJxjxze4n1Krimd_DAAU",{"id":44,"title":40,"body":4179,"color":808,"date":809,"description":810,"draft":21,"extension":811,"image":812,"meta":4685,"navigation":182,"oneliner":814,"path":41,"seo":4686,"stem":42,"tags":4687,"url":822,"__hash__":823},{"type":46,"value":4180,"toc":4675},[4181,4183,4185,4187,4189,4196,4673],[49,4182,52],{"id":51},[54,4184,56],{},[58,4186],{"exclude":60,":max-depth":61},[49,4188,65],{"id":64},[54,4190,68,4191,77,4194,82],{},[70,4192,76],{"href":72,"rel":4193,"target":75},[74],[79,4195,81],{},[84,4197,4198,4200,4204,4216,4220,4312,4316,4326,4328,4330,4332,4336,4338,4348,4350,4354,4356,4360,4362,4388,4390,4404,4406,4408,4410,4438,4440,4442,4444,4448,4452,4456,4659],{"level":61},[49,4199,89],{"id":88},[54,4201,92,4202,97],{},[94,4203,96],{},[99,4205,4206],{"className":101,"code":102,"language":103,"meta":104,"style":104},[79,4207,4208],{"__ignoreMap":104},[108,4209,4210,4212,4214],{"class":110,"line":111},[108,4211,115],{"class":114},[108,4213,119],{"class":118},[108,4215,123],{"class":122},[54,4217,126,4218,130],{},[79,4219,129],{},[99,4221,4222],{"className":101,"code":133,"language":103,"meta":104,"style":104},[79,4223,4224,4228,4252,4256,4262,4272,4282,4292,4306],{"__ignoreMap":104},[108,4225,4226],{"class":110,"line":111},[108,4227,140],{"class":122},[108,4229,4230,4232,4234,4236,4238,4240,4242,4244,4246,4248,4250],{"class":110,"line":143},[108,4231,147],{"class":146},[108,4233,151],{"class":150},[108,4235,115],{"class":114},[108,4237,119],{"class":118},[108,4239,158],{"class":150},[108,4241,161],{"class":114},[108,4243,164],{"class":118},[108,4245,167],{"class":150},[108,4247,170],{"class":118},[108,4249,173],{"class":150},[108,4251,176],{"class":150},[108,4253,4254],{"class":110,"line":179},[108,4255,183],{"emptyLinePlaceholder":182},[108,4257,4258,4260],{"class":110,"line":186},[108,4259,190],{"class":189},[108,4261,193],{"class":150},[108,4263,4264,4266,4268,4270],{"class":110,"line":196},[108,4265,190],{"class":189},[108,4267,201],{"class":150},[108,4269,204],{"class":118},[108,4271,207],{"class":150},[108,4273,4274,4276,4278,4280],{"class":110,"line":210},[108,4275,190],{"class":189},[108,4277,201],{"class":150},[108,4279,217],{"class":118},[108,4281,207],{"class":150},[108,4283,4284,4286,4288,4290],{"class":110,"line":222},[108,4285,190],{"class":189},[108,4287,201],{"class":150},[108,4289,204],{"class":118},[108,4291,207],{"class":150},[108,4293,4294,4296,4298,4300,4302,4304],{"class":110,"line":233},[108,4295,190],{"class":189},[108,4297,201],{"class":150},[108,4299,240],{"class":118},[108,4301,243],{"class":150},[108,4303,147],{"class":146},[108,4305,248],{"class":150},[108,4307,4308,4310],{"class":110,"line":251},[108,4309,190],{"class":189},[108,4311,193],{"class":150},[54,4313,258,4314,262],{},[79,4315,261],{},[264,4317,4318,4322],{},[267,4319,4320],{"icon":269,"title":270,"to":271,"target":75},[54,4321,274],{},[267,4323,4324],{"icon":277,"title":278,"to":279,"target":75},[54,4325,282],{},[49,4327,286],{"id":285},[54,4329,289],{},[54,4331,292],{},[54,4333,295,4334,299],{},[79,4335,298],{},[54,4337,302],{},[264,4339,4340,4344],{},[267,4341,4342],{"icon":307,"title":308,"to":15},[54,4343,311],{},[267,4345,4346],{"icon":314,"title":315,"to":316,"target":75},[54,4347,319],{},[49,4349,323],{"id":322},[54,4351,326,4352,330],{},[79,4353,329],{},[54,4355,333],{},[54,4357,336,4358,340],{},[79,4359,339],{},[54,4361,343],{},[345,4363,4364,4374,4380],{},[348,4365,4366,353,4368,357,4370,353,4372,364],{},[79,4367,352],{},[79,4369,356],{},[79,4371,360],{},[79,4373,363],{},[348,4375,4376,353,4378,373],{},[79,4377,369],{},[79,4379,372],{},[348,4381,4382,379,4384,383,4386,387],{},[79,4383,378],{},[79,4385,382],{},[79,4387,386],{},[54,4389,390],{},[264,4391,4392,4396,4400],{},[267,4393,4394],{"icon":314,"title":395,"to":396,"target":75},[54,4395,399],{},[267,4397,4398],{"icon":269,"title":402,"to":403,"target":75},[54,4399,406],{},[267,4401,4402],{"icon":277,"title":409,"to":410,"target":75},[54,4403,413],{},[49,4405,417],{"id":416},[54,4407,420],{},[54,4409,423],{},[99,4411,4412],{"className":426,"code":427,"filename":428,"language":429,"meta":104,"style":104},[79,4413,4414,4418,4422,4426,4430,4434],{"__ignoreMap":104},[108,4415,4416],{"class":110,"line":111},[108,4417,436],{},[108,4419,4420],{"class":110,"line":143},[108,4421,441],{},[108,4423,4424],{"class":110,"line":179},[108,4425,446],{},[108,4427,4428],{"class":110,"line":186},[108,4429,451],{},[108,4431,4432],{"class":110,"line":196},[108,4433,456],{},[108,4435,4436],{"class":110,"line":210},[108,4437,461],{},[54,4439,464],{},[54,4441,467],{},[469,4443,472],{"id":471},[54,4445,475,4446,479],{},[79,4447,478],{},[54,4449,482,4450,262],{},[79,4451,485],{},[54,4453,488,4454,492],{},[79,4455,491],{},[494,4457,4458,4464],{},[54,4459,4460],{},[499,4461,4462],{},[502,4463,504],{},[506,4465,4466,4472,4492,4497,4657],{},[54,4467,510,4468,514,4470,518],{},[79,4469,513],{},[79,4471,517],{},[99,4473,4474],{"className":521,"code":522,"language":523,"meta":524,"style":104},[79,4475,4476,4480,4484,4488],{"__ignoreMap":104},[108,4477,4478],{"class":110,"line":111},[108,4479,531],{},[108,4481,4482],{"class":110,"line":143},[108,4483,536],{},[108,4485,4486],{"class":110,"line":179},[108,4487,541],{},[108,4489,4490],{"class":110,"line":186},[108,4491,546],{},[54,4493,549,4494,554],{},[70,4495,485],{"href":552,"rel":4496},[74],[556,4498,4499,4501,4503,4635,4641],{},[559,4500,561],{},[54,4502,564],{},[99,4504,4505],{"className":101,"code":567,"language":103,"meta":104,"style":104},[79,4506,4507,4511,4519,4523,4537,4557,4573,4593,4625],{"__ignoreMap":104},[108,4508,4509],{"class":110,"line":111},[108,4510,574],{"class":118},[108,4512,4513,4515,4517],{"class":110,"line":143},[108,4514,579],{"class":114},[108,4516,582],{"class":118},[108,4518,585],{"class":146},[108,4520,4521],{"class":110,"line":179},[108,4522,590],{"class":114},[108,4524,4525,4527,4529,4531,4533,4535],{"class":110,"line":186},[108,4526,595],{"class":114},[108,4528,598],{"class":146},[108,4530,601],{"class":118},[108,4532,604],{"class":118},[108,4534,607],{"class":118},[108,4536,610],{"class":146},[108,4538,4539,4541,4543,4545,4547,4549,4551,4553,4555],{"class":110,"line":196},[108,4540,615],{"class":114},[108,4542,201],{"class":150},[108,4544,620],{"class":118},[108,4546,623],{"class":150},[108,4548,626],{"class":118},[108,4550,629],{"class":118},[108,4552,632],{"class":118},[108,4554,635],{"class":118},[108,4556,638],{"class":118},[108,4558,4559,4561,4563,4565,4567,4569,4571],{"class":110,"line":210},[108,4560,643],{"class":114},[108,4562,646],{"class":118},[108,4564,649],{"class":118},[108,4566,652],{"class":150},[108,4568,655],{"class":114},[108,4570,658],{"class":118},[108,4572,176],{"class":150},[108,4574,4575,4577,4579,4581,4583,4585,4587,4589,4591],{"class":110,"line":222},[108,4576,615],{"class":114},[108,4578,201],{"class":150},[108,4580,669],{"class":118},[108,4582,623],{"class":150},[108,4584,626],{"class":118},[108,4586,629],{"class":118},[108,4588,678],{"class":118},[108,4590,635],{"class":118},[108,4592,683],{"class":118},[108,4594,4595,4597,4599,4601,4603,4605,4607,4609,4611,4613,4615,4617,4619,4621,4623],{"class":110,"line":233},[108,4596,688],{"class":114},[108,4598,691],{"class":114},[108,4600,694],{"class":114},[108,4602,697],{"class":114},[108,4604,694],{"class":114},[108,4606,702],{"class":114},[108,4608,705],{"class":114},[108,4610,646],{"class":118},[108,4612,201],{"class":150},[108,4614,712],{"class":118},[108,4616,623],{"class":150},[108,4618,717],{"class":118},[108,4620,720],{"class":118},[108,4622,723],{"class":118},[108,4624,193],{"class":150},[108,4626,4627,4629,4631,4633],{"class":110,"line":251},[108,4628,730],{"class":114},[108,4630,167],{"class":150},[108,4632,735],{"class":118},[108,4634,738],{"class":150},[54,4636,741,4637,744,4639,747],{},[79,4638,517],{},[79,4640,513],{},[99,4642,4643],{"className":521,"code":750,"language":523,"meta":524,"style":104},[79,4644,4645,4649,4653],{"__ignoreMap":104},[108,4646,4647],{"class":110,"line":111},[108,4648,757],{},[108,4650,4651],{"class":110,"line":143},[108,4652,762],{},[108,4654,4655],{"class":110,"line":179},[108,4656,767],{},[54,4658,770],{},[264,4660,4661,4665,4669],{},[267,4662,4663],{"icon":314,"title":775,"to":776,"target":75},[54,4664,779],{},[267,4666,4667],{"icon":277,"title":782,"to":783,"target":75},[54,4668,786],{},[267,4670,4671],{"icon":277,"title":789,"to":790,"target":75},[54,4672,793],{},[795,4674,797],{},{"title":104,"searchDepth":143,"depth":143,"links":4676},[4677,4678,4679,4680,4681,4682],{"id":51,"depth":143,"text":52},{"id":64,"depth":143,"text":65},{"id":88,"depth":143,"text":89},{"id":285,"depth":143,"text":286},{"id":322,"depth":143,"text":323},{"id":416,"depth":143,"text":417,"children":4683},[4684],{"id":471,"depth":179,"text":472},{},{"title":40,"description":810},[817,818,819,820,821],{"id":4689,"title":32,"body":4690,"color":6037,"date":6038,"description":6039,"draft":21,"extension":811,"image":6040,"meta":6041,"navigation":182,"oneliner":6042,"path":33,"seo":6043,"stem":34,"tags":6044,"url":822,"__hash__":6049},"work\u002Fwork\u002Ffocal-grid.md",{"type":46,"value":4691,"toc":6026},[4692,4694,4697,4700,4704,4712,4714,4717,4721,4729,4732,4735,4738,4742,4746,4753,4767,4770,4773,4776,4780,4783,4786,4987,4990,4994,5013,5016,5484,5490,5559,5562,5565,5580,5591,5594,5682,5685,5751,5765,5775,5886,5889,5963,5972,6023],[469,4693,52],{"id":51},[54,4695,4696],{},"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.",[58,4698],{"exclude":4699,":max-depth":3510},"Table of Contents, TL;DR, Mini Demo",[49,4701,4703],{"id":4702},"mini-demo","Mini Demo",[1891,4705,4706],{},[54,4707,4708],{},[897,4709],{"alt":4710,"src":4711},"Focal Grid Demo GIF","content\u002Ffocal-grid-demo.gif",[49,4713,65],{"id":64},[54,4715,4716],{},"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.",[49,4718,4720],{"id":4719},"tech-stack","Tech Stack",[1891,4722,4723],{},[54,4724,4725],{},[897,4726],{"alt":4727,"src":4728},"Focal Grid Architecture Overview","content\u002Ffocal-grid-architecture.jpg",[54,4730,4731],{},"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.",[54,4733,4734],{},"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.",[54,4736,4737],{},"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.",[49,4739,4741],{"id":4740},"key-features","Key Features",[469,4743,4745],{"id":4744},"custom-ui-map-interactions","Custom UI & Map Interactions",[54,4747,4748,4749,4752],{},"Perhaps this might be the easiest part to grasp intuitively seeing the ",[70,4750,4703],{"href":4751},"#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:",[345,4754,4755,4758,4761,4764],{},[348,4756,4757],{},"Toggling different energy grid layers on and off which were seperated by their voltage;",[348,4759,4760],{},"Toggling energy substations connected to each kind of voltage level;",[348,4762,4763],{},"On-map polygon drawing and deletion for grouping data points geographically;",[348,4765,4766],{},"Drag-and-drop icons for projecting energy asset placement;",[54,4768,4769],{},"Furthermore, the top bar additionally includes simple sattelite view and pitched angle toggle buttons.",[54,4771,4772],{},"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.",[54,4774,4775],{},"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.",[469,4777,4779],{"id":4778},"geojson-to-postgis-pipeline","GeoJSON to PostGIS Pipeline",[54,4781,4782],{},"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.",[54,4784,4785],{},"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.",[4787,4788,4789],"code-collapse",{},[99,4790,4795],{"className":4791,"code":4792,"filename":4793,"language":4794,"meta":104,"style":104},"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",[79,4796,4797,4802,4807,4812,4817,4822,4827,4832,4837,4842,4847,4852,4857,4862,4867,4871,4876,4880,4885,4890,4895,4901,4907,4913,4919,4925,4931,4937,4942,4948,4954,4959,4965,4971,4977,4982],{"__ignoreMap":104},[108,4798,4799],{"class":110,"line":111},[108,4800,4801],{},"async function importGeoJSON(\n",[108,4803,4804],{"class":110,"line":143},[108,4805,4806],{},"  filePath, \n",[108,4808,4809],{"class":110,"line":179},[108,4810,4811],{},"  tableName, \n",[108,4813,4814],{"class":110,"line":186},[108,4815,4816],{},"  supabaseClient\n",[108,4818,4819],{"class":110,"line":196},[108,4820,4821],{},") {\n",[108,4823,4824],{"class":110,"line":210},[108,4825,4826],{},"  try {\n",[108,4828,4829],{"class":110,"line":222},[108,4830,4831],{},"    const geojson = JSON.parse(fs.readFileSync(filePath, 'utf8'))\n",[108,4833,4834],{"class":110,"line":233},[108,4835,4836],{},"    \n",[108,4838,4839],{"class":110,"line":251},[108,4840,4841],{},"    \u002F\u002F Process in batches to avoid timeouts\n",[108,4843,4844],{"class":110,"line":1126},[108,4845,4846],{},"    const batchSize = 1000\n",[108,4848,4849],{"class":110,"line":1131},[108,4850,4851],{},"    const features = geojson.features.map(feature => ({\n",[108,4853,4854],{"class":110,"line":1143},[108,4855,4856],{},"      geom: feature.geometry,\n",[108,4858,4859],{"class":110,"line":1155},[108,4860,4861],{},"      properties: feature.properties || {}\n",[108,4863,4864],{"class":110,"line":1177},[108,4865,4866],{},"    }))\n",[108,4868,4869],{"class":110,"line":1199},[108,4870,183],{"emptyLinePlaceholder":182},[108,4872,4873],{"class":110,"line":1222},[108,4874,4875],{},"    console.log(`Importing ${features.length} features...`)\n",[108,4877,4878],{"class":110,"line":1242},[108,4879,4836],{},[108,4881,4882],{"class":110,"line":3816},[108,4883,4884],{},"    for (let i = 0; i \u003C features.length; i += batchSize) {\n",[108,4886,4887],{"class":110,"line":3839},[108,4888,4889],{},"      const batch = features.slice(i, i + batchSize)\n",[108,4891,4892],{"class":110,"line":3862},[108,4893,4894],{},"      const { data, error } = await supabaseClient\n",[108,4896,4898],{"class":110,"line":4897},21,[108,4899,4900],{},"        .from(tableName)\n",[108,4902,4904],{"class":110,"line":4903},22,[108,4905,4906],{},"        .insert(batch)\n",[108,4908,4910],{"class":110,"line":4909},23,[108,4911,4912],{},"      \n",[108,4914,4916],{"class":110,"line":4915},24,[108,4917,4918],{},"      if (error) {\n",[108,4920,4922],{"class":110,"line":4921},25,[108,4923,4924],{},"        console.error('Error inserting batch:', error)\n",[108,4926,4928],{"class":110,"line":4927},26,[108,4929,4930],{},"        break\n",[108,4932,4934],{"class":110,"line":4933},27,[108,4935,4936],{},"      }\n",[108,4938,4940],{"class":110,"line":4939},28,[108,4941,4912],{},[108,4943,4945],{"class":110,"line":4944},29,[108,4946,4947],{},"      console.log(`Inserted batch ${Math.floor(i\u002FbatchSize) + 1}\u002F${Math.ceil(features.length\u002FbatchSize)}`)\n",[108,4949,4951],{"class":110,"line":4950},30,[108,4952,4953],{},"    }\n",[108,4955,4957],{"class":110,"line":4956},31,[108,4958,4836],{},[108,4960,4962],{"class":110,"line":4961},32,[108,4963,4964],{},"    console.log('Import completed!')\n",[108,4966,4968],{"class":110,"line":4967},33,[108,4969,4970],{},"  } catch (error) {\n",[108,4972,4974],{"class":110,"line":4973},34,[108,4975,4976],{},"    console.error('Import failed:', error)\n",[108,4978,4980],{"class":110,"line":4979},35,[108,4981,2608],{},[108,4983,4985],{"class":110,"line":4984},36,[108,4986,2627],{},[54,4988,4989],{},"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",[469,4991,4993],{"id":4992},"dynamic-vector-tile-querying","Dynamic Vector Tile Querying",[54,4995,4996,4997,5000,5001,5006,5007,5012],{},"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, ",[499,4998,4999],{},"'how can I render map layers natively on the frontend while keeping the data completely external to Mapbox?'",". Lucky for me, Mapbox allows for ",[70,5002,5005],{"href":5003,"rel":5004},"https:\u002F\u002Fdocs.mapbox.com\u002Fmapbox-gl-js\u002Fapi\u002Fsources\u002F#vectortilesource",[74],"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 ",[70,5008,5011],{"href":5009,"rel":5010},"https:\u002F\u002Fmapbox.github.io\u002Fvector-tile-spec\u002F",[74],"Vector Tile specification"," (basically Google's Protocol Buffer format).",[54,5014,5015],{},"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.",[4787,5017,5018],{},[99,5019,5024],{"className":5020,"code":5021,"filename":5022,"language":5023,"meta":104,"style":104},"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",[79,5025,5026,5049,5054,5059,5087,5149,5178,5203,5234,5261,5288,5292,5297,5302,5351,5358,5365,5372,5377,5386,5390,5395,5413,5425,5434,5444,5463,5467,5475],{"__ignoreMap":104},[108,5027,5028,5031,5033,5036,5038,5041,5043,5046],{"class":110,"line":111},[108,5029,5030],{"class":189},"serve",[108,5032,652],{"class":146},[108,5034,5035],{"class":975},"async",[108,5037,2461],{"class":150},[108,5039,5040],{"class":3019},"req",[108,5042,4135],{"class":150},[108,5044,5045],{"class":975},"=>",[108,5047,5048],{"class":150},"{\n",[108,5050,5051],{"class":110,"line":143},[108,5052,5053],{"class":122},"  \u002F\u002F Parses query parameters from url \n",[108,5055,5056],{"class":110,"line":179},[108,5057,5058],{"class":122},"  \u002F\u002F example: \u002Ffunctions\u002Fmvt\u002Fdataset123\u002F{z}\u002F{x}\u002F{y}\n",[108,5060,5061,5063,5066,5068,5071,5074,5076,5078,5080,5083,5085],{"class":110,"line":186},[108,5062,3010],{"class":975},[108,5064,5065],{"class":146}," url",[108,5067,646],{"class":150},[108,5069,5070],{"class":150}," new",[108,5072,5073],{"class":189}," URL",[108,5075,652],{"class":982},[108,5077,5040],{"class":146},[108,5079,262],{"class":150},[108,5081,5082],{"class":146},"url",[108,5084,4135],{"class":982},[108,5086,2079],{"class":150},[108,5088,5089,5091,5094,5096,5098,5100,5103,5105,5108,5110,5112,5114,5116,5118,5120,5123,5125,5127,5129,5131,5133,5135,5137,5140,5143,5145,5147],{"class":110,"line":196},[108,5090,3010],{"class":975},[108,5092,5093],{"class":146}," parts",[108,5095,646],{"class":150},[108,5097,5065],{"class":146},[108,5099,262],{"class":150},[108,5101,5102],{"class":146},"pathname",[108,5104,262],{"class":150},[108,5106,5107],{"class":189},"split",[108,5109,652],{"class":982},[108,5111,173],{"class":150},[108,5113,2274],{"class":118},[108,5115,173],{"class":150},[108,5117,4135],{"class":982},[108,5119,262],{"class":150},[108,5121,5122],{"class":189},"filter",[108,5124,652],{"class":982},[108,5126,652],{"class":150},[108,5128,54],{"class":3019},[108,5130,4135],{"class":150},[108,5132,5045],{"class":975},[108,5134,54],{"class":146},[108,5136,262],{"class":150},[108,5138,5139],{"class":146},"length",[108,5141,5142],{"class":150}," >",[108,5144,2243],{"class":988},[108,5146,4135],{"class":982},[108,5148,2079],{"class":150},[108,5150,5151,5153,5156,5158,5160,5162,5165,5167,5169,5172,5174,5176],{"class":110,"line":210},[108,5152,3010],{"class":975},[108,5154,5155],{"class":146}," mvtIndex",[108,5157,646],{"class":150},[108,5159,5093],{"class":146},[108,5161,262],{"class":150},[108,5163,5164],{"class":189},"indexOf",[108,5166,652],{"class":982},[108,5168,173],{"class":150},[108,5170,5171],{"class":118},"mvt",[108,5173,173],{"class":150},[108,5175,4135],{"class":982},[108,5177,2079],{"class":150},[108,5179,5180,5182,5185,5187,5189,5191,5194,5197,5199,5201],{"class":110,"line":222},[108,5181,3010],{"class":975},[108,5183,5184],{"class":146}," dataset",[108,5186,646],{"class":150},[108,5188,5093],{"class":146},[108,5190,2412],{"class":982},[108,5192,5193],{"class":146},"mvtIndex",[108,5195,5196],{"class":150}," +",[108,5198,2076],{"class":988},[108,5200,705],{"class":982},[108,5202,2079],{"class":150},[108,5204,5205,5207,5210,5212,5215,5217,5220,5222,5224,5226,5229,5232],{"class":110,"line":233},[108,5206,3010],{"class":975},[108,5208,5209],{"class":146}," z",[108,5211,646],{"class":150},[108,5213,5214],{"class":189}," Number",[108,5216,652],{"class":982},[108,5218,5219],{"class":146},"parts",[108,5221,2412],{"class":982},[108,5223,5193],{"class":146},[108,5225,5196],{"class":150},[108,5227,5228],{"class":988}," 2",[108,5230,5231],{"class":982},"])",[108,5233,2079],{"class":150},[108,5235,5236,5238,5240,5242,5244,5246,5248,5250,5252,5254,5257,5259],{"class":110,"line":251},[108,5237,3010],{"class":975},[108,5239,3471],{"class":146},[108,5241,646],{"class":150},[108,5243,5214],{"class":189},[108,5245,652],{"class":982},[108,5247,5219],{"class":146},[108,5249,2412],{"class":982},[108,5251,5193],{"class":146},[108,5253,5196],{"class":150},[108,5255,5256],{"class":988}," 3",[108,5258,5231],{"class":982},[108,5260,2079],{"class":150},[108,5262,5263,5265,5267,5269,5271,5273,5275,5277,5279,5281,5284,5286],{"class":110,"line":1126},[108,5264,3010],{"class":975},[108,5266,3476],{"class":146},[108,5268,646],{"class":150},[108,5270,5214],{"class":189},[108,5272,652],{"class":982},[108,5274,5219],{"class":146},[108,5276,2412],{"class":982},[108,5278,5193],{"class":146},[108,5280,5196],{"class":150},[108,5282,5283],{"class":988}," 4",[108,5285,5231],{"class":982},[108,5287,2079],{"class":150},[108,5289,5290],{"class":110,"line":1131},[108,5291,183],{"emptyLinePlaceholder":182},[108,5293,5294],{"class":110,"line":1143},[108,5295,5296],{"class":122},"  \u002F\u002F Calls supabase RPC function to query the actual geo data\n",[108,5298,5299],{"class":110,"line":1155},[108,5300,5301],{"class":122},"  \u002F\u002F Args are parsed earlier from query parameters\n",[108,5303,5304,5306,5309,5312,5314,5317,5319,5322,5325,5327,5330,5333,5335,5338,5340,5342,5345,5347,5349],{"class":110,"line":1177},[108,5305,3010],{"class":975},[108,5307,5308],{"class":150}," {",[108,5310,5311],{"class":982}," data",[108,5313,130],{"class":150},[108,5315,5316],{"class":146}," vectorTileBinary",[108,5318,626],{"class":150},[108,5320,5321],{"class":146}," error",[108,5323,5324],{"class":150}," }",[108,5326,646],{"class":150},[108,5328,5329],{"class":2457}," await",[108,5331,5332],{"class":146}," supabase",[108,5334,262],{"class":150},[108,5336,5337],{"class":189},"rpc",[108,5339,652],{"class":982},[108,5341,173],{"class":150},[108,5343,5344],{"class":118},"get_dynamic_lines_mvt",[108,5346,173],{"class":150},[108,5348,626],{"class":150},[108,5350,2434],{"class":150},[108,5352,5353,5356],{"class":110,"line":1199},[108,5354,5355],{"class":146},"    dataset",[108,5357,998],{"class":150},[108,5359,5360,5363],{"class":110,"line":1222},[108,5361,5362],{"class":146},"    z",[108,5364,998],{"class":150},[108,5366,5367,5370],{"class":110,"line":1242},[108,5368,5369],{"class":146},"    x",[108,5371,998],{"class":150},[108,5373,5374],{"class":110,"line":3816},[108,5375,5376],{"class":146},"    y\n",[108,5378,5379,5382,5384],{"class":110,"line":3839},[108,5380,5381],{"class":150},"  }",[108,5383,4135],{"class":982},[108,5385,2079],{"class":150},[108,5387,5388],{"class":110,"line":3862},[108,5389,3428],{"class":982},[108,5391,5392],{"class":110,"line":4897},[108,5393,5394],{"class":122},"  \u002F\u002F Returns mvt data in raw bytes\n",[108,5396,5397,5399,5401,5404,5406,5409,5411],{"class":110,"line":4903},[108,5398,2617],{"class":2457},[108,5400,5070],{"class":150},[108,5402,5403],{"class":189}," Response",[108,5405,652],{"class":982},[108,5407,5408],{"class":146},"vectorTileBinary",[108,5410,626],{"class":150},[108,5412,2434],{"class":150},[108,5414,5415,5418,5420,5423],{"class":110,"line":4909},[108,5416,5417],{"class":982},"    status",[108,5419,130],{"class":150},[108,5421,5422],{"class":988}," 200",[108,5424,998],{"class":150},[108,5426,5427,5430,5432],{"class":110,"line":4915},[108,5428,5429],{"class":982},"    headers",[108,5431,130],{"class":150},[108,5433,2434],{"class":150},[108,5435,5436,5439,5442],{"class":110,"line":4921},[108,5437,5438],{"class":150},"      ...",[108,5440,5441],{"class":146},"corsHeaders",[108,5443,998],{"class":150},[108,5445,5446,5449,5452,5454,5456,5458,5461],{"class":110,"line":4927},[108,5447,5448],{"class":150},"      '",[108,5450,5451],{"class":982},"Content-Type",[108,5453,173],{"class":150},[108,5455,130],{"class":150},[108,5457,167],{"class":150},[108,5459,5460],{"class":118},"application\u002Fvnd.mapbox-vector-tile",[108,5462,738],{"class":150},[108,5464,5465],{"class":110,"line":4933},[108,5466,4953],{"class":150},[108,5468,5469,5471,5473],{"class":110,"line":4939},[108,5470,5381],{"class":150},[108,5472,4135],{"class":982},[108,5474,2079],{"class":150},[108,5476,5477,5480,5482],{"class":110,"line":4944},[108,5478,5479],{"class":150},"}",[108,5481,4135],{"class":146},[108,5483,2079],{"class":150},[54,5485,5486,5487,5489],{},"Now for the interesting part. The PostgreSQL ",[79,5488,5344],{}," RPC function was made to have very rudamentary API, as shown below:",[99,5491,5496],{"className":5492,"code":5493,"filename":5494,"language":5495,"meta":104,"style":104},"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",[79,5497,5498,5512,5521,5531,5540,5548],{"__ignoreMap":104},[108,5499,5500,5503,5506,5509],{"class":110,"line":111},[108,5501,5502],{"class":988},"CREATE OR REPLACE",[108,5504,5505],{"class":988}," FUNCTION",[108,5507,5508],{"class":189}," public",[108,5510,5511],{"class":146},".get_dynamic_lines_mvt(\n",[108,5513,5514,5517,5519],{"class":110,"line":143},[108,5515,5516],{"class":146},"  dataset ",[108,5518,524],{"class":975},[108,5520,998],{"class":146},[108,5522,5523,5526,5529],{"class":110,"line":179},[108,5524,5525],{"class":146},"  z ",[108,5527,5528],{"class":975},"integer",[108,5530,998],{"class":146},[108,5532,5533,5536,5538],{"class":110,"line":186},[108,5534,5535],{"class":146},"  x ",[108,5537,5528],{"class":975},[108,5539,998],{"class":146},[108,5541,5542,5545],{"class":110,"line":196},[108,5543,5544],{"class":146},"  y ",[108,5546,5547],{"class":975},"integer\n",[108,5549,5550,5553,5556],{"class":110,"line":210},[108,5551,5552],{"class":146},") ",[108,5554,5555],{"class":988},"RETURNS",[108,5557,5558],{"class":975}," bytea\n",[54,5560,5561],{},"What actually happens inside of the function body can best be described in a handful of procedural steps.",[54,5563,5564],{},"Firstly, the sent coordinates values (x, y, z) are translated into rectangular polygon coordinates representing the user's bounding box.",[99,5566,5568],{"className":5492,"code":5567,"filename":5494,"language":5495,"meta":104,"style":104},"bounds3857 := ST_TileEnvelope(z, x, y);\n",[79,5569,5570],{"__ignoreMap":104},[108,5571,5572,5575,5577],{"class":110,"line":111},[108,5573,5574],{"class":146},"bounds3857 :",[108,5576,1425],{"class":150},[108,5578,5579],{"class":146}," ST_TileEnvelope(z, x, y);\n",[1389,5581,5582],{},[54,5583,5584,5587,5588],{},[79,5585,5586],{},"bounds385"," is generated and later injected at runtime using alias ",[79,5589,5590],{},"$1",[54,5592,5593],{},"Next, geometries that land outside of the requested tile are filtered out before moving on to expensive intersection computations.",[99,5595,5597],{"className":5492,"code":5596,"filename":5494,"language":5495,"meta":104,"style":104},"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",[79,5598,5599,5612,5636,5655,5677],{"__ignoreMap":104},[108,5600,5601,5604,5607,5610],{"class":110,"line":111},[108,5602,5603],{"class":988},"WITH",[108,5605,5606],{"class":146}," filtered ",[108,5608,5609],{"class":988},"AS",[108,5611,983],{"class":146},[108,5613,5614,5617,5620,5623,5625,5627,5630,5633],{"class":110,"line":143},[108,5615,5616],{"class":988},"  SELECT",[108,5618,5619],{"class":146}," ST_Transform(wkb_geometry, ",[108,5621,5622],{"class":988},"3857",[108,5624,5552],{"class":146},[108,5626,5609],{"class":988},[108,5628,5629],{"class":146}," geom3857 ",[108,5631,5632],{"class":988},"FROM",[108,5634,5635],{"class":146}," %I.%I\n",[108,5637,5638,5641,5643,5645,5648,5650,5652],{"class":110,"line":179},[108,5639,5640],{"class":988},"  WHERE",[108,5642,5619],{"class":146},[108,5644,5622],{"class":988},[108,5646,5647],{"class":146},") && $",[108,5649,1636],{"class":988},[108,5651,3643],{"class":146},[108,5653,5654],{"class":988},"geometry\n",[108,5656,5657,5660,5663,5665,5668,5670,5672,5675],{"class":110,"line":186},[108,5658,5659],{"class":988},"  AND",[108,5661,5662],{"class":146}," ST_Intersects(ST_Transform(wkb_geometry, ",[108,5664,5622],{"class":988},[108,5666,5667],{"class":146},"), $",[108,5669,1636],{"class":988},[108,5671,3643],{"class":146},[108,5673,5674],{"class":988},"geometry",[108,5676,176],{"class":146},[108,5678,5679],{"class":110,"line":196},[108,5680,5681],{"class":146},"),\n",[54,5683,5684],{},"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.",[99,5686,5688],{"className":5492,"code":5687,"filename":5494,"language":5495,"meta":104,"style":104},"simplification := CASE\n  WHEN z \u003C= 5  THEN 80\n  WHEN z \u003C= 10 THEN 15\n  ELSE 0\nEND;\n",[79,5689,5690,5700,5719,5736,5744],{"__ignoreMap":104},[108,5691,5692,5695,5697],{"class":110,"line":111},[108,5693,5694],{"class":146},"simplification :",[108,5696,1425],{"class":150},[108,5698,5699],{"class":988}," CASE\n",[108,5701,5702,5705,5708,5710,5713,5716],{"class":110,"line":143},[108,5703,5704],{"class":988},"  WHEN",[108,5706,5707],{"class":146}," z ",[108,5709,1533],{"class":150},[108,5711,5712],{"class":988}," 5",[108,5714,5715],{"class":988},"  THEN",[108,5717,5718],{"class":988}," 80\n",[108,5720,5721,5723,5725,5727,5730,5733],{"class":110,"line":179},[108,5722,5704],{"class":988},[108,5724,5707],{"class":146},[108,5726,1533],{"class":150},[108,5728,5729],{"class":988}," 10",[108,5731,5732],{"class":988}," THEN",[108,5734,5735],{"class":988}," 15\n",[108,5737,5738,5741],{"class":110,"line":186},[108,5739,5740],{"class":988},"  ELSE",[108,5742,5743],{"class":988}," 0\n",[108,5745,5746,5749],{"class":110,"line":196},[108,5747,5748],{"class":988},"END",[108,5750,2079],{"class":146},[1389,5752,5753],{},[54,5754,5755,5756,2863,5758,5761,5762],{},"Just as ",[79,5757,5586],{},[79,5759,5760],{},"simplifications"," is also generated and injected at runtime using alias ",[79,5763,5764],{},"$2",[54,5766,5767,5768,5771,5772,262],{},"This value is subsequently used as an argument to clip and simplify the data to varying degrees. At the same time, ",[79,5769,5770],{},"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 ",[79,5773,5774],{},"mvtgeom",[99,5776,5778],{"className":5492,"code":5777,"filename":5494,"language":5495,"meta":104,"style":104},"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",[79,5779,5780,5789,5794,5799,5804,5818,5830,5838,5845,5864,5874,5882],{"__ignoreMap":104},[108,5781,5782,5785,5787],{"class":110,"line":111},[108,5783,5784],{"class":146},"mvtgeom ",[108,5786,5609],{"class":988},[108,5788,983],{"class":146},[108,5790,5791],{"class":110,"line":143},[108,5792,5793],{"class":988},"  SELECT\n",[108,5795,5796],{"class":110,"line":179},[108,5797,5798],{"class":146},"    ST_AsMVTGeom(\n",[108,5800,5801],{"class":110,"line":186},[108,5802,5803],{"class":988},"      CASE\n",[108,5805,5806,5809,5812,5814,5816],{"class":110,"line":196},[108,5807,5808],{"class":988},"        WHEN",[108,5810,5811],{"class":146}," $",[108,5813,61],{"class":988},[108,5815,5142],{"class":150},[108,5817,5743],{"class":988},[108,5819,5820,5823,5826,5828],{"class":110,"line":210},[108,5821,5822],{"class":988},"          THEN",[108,5824,5825],{"class":146}," ST_SimplifyPreserveTopology(geom3857, $",[108,5827,61],{"class":988},[108,5829,176],{"class":146},[108,5831,5832,5835],{"class":110,"line":222},[108,5833,5834],{"class":988},"        ELSE",[108,5836,5837],{"class":146}," geom3857\n",[108,5839,5840,5843],{"class":110,"line":233},[108,5841,5842],{"class":988},"      END",[108,5844,998],{"class":146},[108,5846,5847,5850,5852,5854,5857,5859,5861],{"class":110,"line":251},[108,5848,5849],{"class":146},"      $",[108,5851,1636],{"class":988},[108,5853,2863],{"class":146},[108,5855,5856],{"class":988},"4096",[108,5858,2863],{"class":146},[108,5860,1037],{"class":988},[108,5862,5863],{"class":146},", true\n",[108,5865,5866,5869,5871],{"class":110,"line":1126},[108,5867,5868],{"class":146},"    ) ",[108,5870,5609],{"class":988},[108,5872,5873],{"class":146}," geom\n",[108,5875,5876,5879],{"class":110,"line":1131},[108,5877,5878],{"class":988},"  FROM",[108,5880,5881],{"class":146}," filtered\n",[108,5883,5884],{"class":110,"line":1143},[108,5885,176],{"class":146},[54,5887,5888],{},"Finally, filtered and simplified data is serialized into raw mvt bytes to be returned to the caller and later the user.",[99,5890,5892],{"className":5492,"code":5891,"filename":5494,"language":5495,"meta":104,"style":104},"SELECT ST_AsMVT(\n  q,\n  'lines',\n  4096,\n  'geom'\n)\nFROM (\n  SELECT geom\n  FROM mvtgeom\n) q;\n",[79,5893,5894,5902,5907,5919,5926,5935,5939,5945,5951,5958],{"__ignoreMap":104},[108,5895,5896,5899],{"class":110,"line":111},[108,5897,5898],{"class":988},"SELECT",[108,5900,5901],{"class":146}," ST_AsMVT(\n",[108,5903,5904],{"class":110,"line":143},[108,5905,5906],{"class":146},"  q,\n",[108,5908,5909,5912,5915,5917],{"class":110,"line":179},[108,5910,5911],{"class":150},"  '",[108,5913,5914],{"class":118},"lines",[108,5916,173],{"class":150},[108,5918,998],{"class":146},[108,5920,5921,5924],{"class":110,"line":186},[108,5922,5923],{"class":988},"  4096",[108,5925,998],{"class":146},[108,5927,5928,5930,5933],{"class":110,"line":196},[108,5929,5911],{"class":150},[108,5931,5932],{"class":118},"geom",[108,5934,738],{"class":150},[108,5936,5937],{"class":110,"line":210},[108,5938,176],{"class":146},[108,5940,5941,5943],{"class":110,"line":222},[108,5942,5632],{"class":988},[108,5944,983],{"class":146},[108,5946,5947,5949],{"class":110,"line":233},[108,5948,5616],{"class":988},[108,5950,5873],{"class":146},[108,5952,5953,5955],{"class":110,"line":251},[108,5954,5878],{"class":988},[108,5956,5957],{"class":146}," mvtgeom\n",[108,5959,5960],{"class":110,"line":1126},[108,5961,5962],{"class":146},") q;\n",[54,5964,5965,5966,5971],{},"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 ",[70,5967,5970],{"href":5968,"rel":5969},"https:\u002F\u002Fpostgis.net\u002Fdocs\u002FPostGIS_Special_Functions_Index.html",[74],"PostGIS Special Function Index"," in case you'd like to know more. The main Special Functions I ended up using were the following:",[1891,5973,5974],{},[99,5975,5979],{"className":5976,"code":5977,"language":5978,"meta":104,"style":104},"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",[79,5980,5981,5989,5996,6003,6010,6016],{"__ignoreMap":104},[108,5982,5983,5986],{"class":110,"line":111},[108,5984,5985],{"class":189},"ST_TileEnvelope",[108,5987,5988],{"class":146},"()\n",[108,5990,5991,5994],{"class":110,"line":143},[108,5992,5993],{"class":189},"ST_Transform",[108,5995,5988],{"class":146},[108,5997,5998,6001],{"class":110,"line":179},[108,5999,6000],{"class":189},"ST_SimplifyPreserveTopology",[108,6002,5988],{"class":146},[108,6004,6005,6008],{"class":110,"line":186},[108,6006,6007],{"class":189},"ST_Intersects",[108,6009,5988],{"class":146},[108,6011,6012,6014],{"class":110,"line":196},[108,6013,5770],{"class":189},[108,6015,5988],{"class":146},[108,6017,6018,6021],{"class":110,"line":210},[108,6019,6020],{"class":189},"ST_AsMVT",[108,6022,5988],{"class":146},[795,6024,6025],{},"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":104,"searchDepth":143,"depth":143,"links":6027},[6028,6029,6030,6031,6032],{"id":51,"depth":179,"text":52},{"id":4702,"depth":143,"text":4703},{"id":64,"depth":143,"text":65},{"id":4719,"depth":143,"text":4720},{"id":4740,"depth":143,"text":4741,"children":6033},[6034,6035,6036],{"id":4744,"depth":179,"text":4745},{"id":4778,"depth":179,"text":4779},{"id":4992,"depth":179,"text":4993},"#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":6039},[6045,6046,6047,6048],"TypeScript","Nuxt.js","Mapbox SDK","PostGIS","B52rFuBHshMTdmD2sWIMFnRQ0ni8sSG8Xl18ace4qgo",{"id":44,"title":40,"body":6051,"color":808,"date":809,"description":810,"draft":21,"extension":811,"image":812,"meta":6557,"navigation":182,"oneliner":814,"path":41,"seo":6558,"stem":42,"tags":6559,"url":822,"__hash__":823},{"type":46,"value":6052,"toc":6547},[6053,6055,6057,6059,6061,6068,6545],[49,6054,52],{"id":51},[54,6056,56],{},[58,6058],{"exclude":60,":max-depth":61},[49,6060,65],{"id":64},[54,6062,68,6063,77,6066,82],{},[70,6064,76],{"href":72,"rel":6065,"target":75},[74],[79,6067,81],{},[84,6069,6070,6072,6076,6088,6092,6184,6188,6198,6200,6202,6204,6208,6210,6220,6222,6226,6228,6232,6234,6260,6262,6276,6278,6280,6282,6310,6312,6314,6316,6320,6324,6328,6531],{"level":61},[49,6071,89],{"id":88},[54,6073,92,6074,97],{},[94,6075,96],{},[99,6077,6078],{"className":101,"code":102,"language":103,"meta":104,"style":104},[79,6079,6080],{"__ignoreMap":104},[108,6081,6082,6084,6086],{"class":110,"line":111},[108,6083,115],{"class":114},[108,6085,119],{"class":118},[108,6087,123],{"class":122},[54,6089,126,6090,130],{},[79,6091,129],{},[99,6093,6094],{"className":101,"code":133,"language":103,"meta":104,"style":104},[79,6095,6096,6100,6124,6128,6134,6144,6154,6164,6178],{"__ignoreMap":104},[108,6097,6098],{"class":110,"line":111},[108,6099,140],{"class":122},[108,6101,6102,6104,6106,6108,6110,6112,6114,6116,6118,6120,6122],{"class":110,"line":143},[108,6103,147],{"class":146},[108,6105,151],{"class":150},[108,6107,115],{"class":114},[108,6109,119],{"class":118},[108,6111,158],{"class":150},[108,6113,161],{"class":114},[108,6115,164],{"class":118},[108,6117,167],{"class":150},[108,6119,170],{"class":118},[108,6121,173],{"class":150},[108,6123,176],{"class":150},[108,6125,6126],{"class":110,"line":179},[108,6127,183],{"emptyLinePlaceholder":182},[108,6129,6130,6132],{"class":110,"line":186},[108,6131,190],{"class":189},[108,6133,193],{"class":150},[108,6135,6136,6138,6140,6142],{"class":110,"line":196},[108,6137,190],{"class":189},[108,6139,201],{"class":150},[108,6141,204],{"class":118},[108,6143,207],{"class":150},[108,6145,6146,6148,6150,6152],{"class":110,"line":210},[108,6147,190],{"class":189},[108,6149,201],{"class":150},[108,6151,217],{"class":118},[108,6153,207],{"class":150},[108,6155,6156,6158,6160,6162],{"class":110,"line":222},[108,6157,190],{"class":189},[108,6159,201],{"class":150},[108,6161,204],{"class":118},[108,6163,207],{"class":150},[108,6165,6166,6168,6170,6172,6174,6176],{"class":110,"line":233},[108,6167,190],{"class":189},[108,6169,201],{"class":150},[108,6171,240],{"class":118},[108,6173,243],{"class":150},[108,6175,147],{"class":146},[108,6177,248],{"class":150},[108,6179,6180,6182],{"class":110,"line":251},[108,6181,190],{"class":189},[108,6183,193],{"class":150},[54,6185,258,6186,262],{},[79,6187,261],{},[264,6189,6190,6194],{},[267,6191,6192],{"icon":269,"title":270,"to":271,"target":75},[54,6193,274],{},[267,6195,6196],{"icon":277,"title":278,"to":279,"target":75},[54,6197,282],{},[49,6199,286],{"id":285},[54,6201,289],{},[54,6203,292],{},[54,6205,295,6206,299],{},[79,6207,298],{},[54,6209,302],{},[264,6211,6212,6216],{},[267,6213,6214],{"icon":307,"title":308,"to":15},[54,6215,311],{},[267,6217,6218],{"icon":314,"title":315,"to":316,"target":75},[54,6219,319],{},[49,6221,323],{"id":322},[54,6223,326,6224,330],{},[79,6225,329],{},[54,6227,333],{},[54,6229,336,6230,340],{},[79,6231,339],{},[54,6233,343],{},[345,6235,6236,6246,6252],{},[348,6237,6238,353,6240,357,6242,353,6244,364],{},[79,6239,352],{},[79,6241,356],{},[79,6243,360],{},[79,6245,363],{},[348,6247,6248,353,6250,373],{},[79,6249,369],{},[79,6251,372],{},[348,6253,6254,379,6256,383,6258,387],{},[79,6255,378],{},[79,6257,382],{},[79,6259,386],{},[54,6261,390],{},[264,6263,6264,6268,6272],{},[267,6265,6266],{"icon":314,"title":395,"to":396,"target":75},[54,6267,399],{},[267,6269,6270],{"icon":269,"title":402,"to":403,"target":75},[54,6271,406],{},[267,6273,6274],{"icon":277,"title":409,"to":410,"target":75},[54,6275,413],{},[49,6277,417],{"id":416},[54,6279,420],{},[54,6281,423],{},[99,6283,6284],{"className":426,"code":427,"filename":428,"language":429,"meta":104,"style":104},[79,6285,6286,6290,6294,6298,6302,6306],{"__ignoreMap":104},[108,6287,6288],{"class":110,"line":111},[108,6289,436],{},[108,6291,6292],{"class":110,"line":143},[108,6293,441],{},[108,6295,6296],{"class":110,"line":179},[108,6297,446],{},[108,6299,6300],{"class":110,"line":186},[108,6301,451],{},[108,6303,6304],{"class":110,"line":196},[108,6305,456],{},[108,6307,6308],{"class":110,"line":210},[108,6309,461],{},[54,6311,464],{},[54,6313,467],{},[469,6315,472],{"id":471},[54,6317,475,6318,479],{},[79,6319,478],{},[54,6321,482,6322,262],{},[79,6323,485],{},[54,6325,488,6326,492],{},[79,6327,491],{},[494,6329,6330,6336],{},[54,6331,6332],{},[499,6333,6334],{},[502,6335,504],{},[506,6337,6338,6344,6364,6369,6529],{},[54,6339,510,6340,514,6342,518],{},[79,6341,513],{},[79,6343,517],{},[99,6345,6346],{"className":521,"code":522,"language":523,"meta":524,"style":104},[79,6347,6348,6352,6356,6360],{"__ignoreMap":104},[108,6349,6350],{"class":110,"line":111},[108,6351,531],{},[108,6353,6354],{"class":110,"line":143},[108,6355,536],{},[108,6357,6358],{"class":110,"line":179},[108,6359,541],{},[108,6361,6362],{"class":110,"line":186},[108,6363,546],{},[54,6365,549,6366,554],{},[70,6367,485],{"href":552,"rel":6368},[74],[556,6370,6371,6373,6375,6507,6513],{},[559,6372,561],{},[54,6374,564],{},[99,6376,6377],{"className":101,"code":567,"language":103,"meta":104,"style":104},[79,6378,6379,6383,6391,6395,6409,6429,6445,6465,6497],{"__ignoreMap":104},[108,6380,6381],{"class":110,"line":111},[108,6382,574],{"class":118},[108,6384,6385,6387,6389],{"class":110,"line":143},[108,6386,579],{"class":114},[108,6388,582],{"class":118},[108,6390,585],{"class":146},[108,6392,6393],{"class":110,"line":179},[108,6394,590],{"class":114},[108,6396,6397,6399,6401,6403,6405,6407],{"class":110,"line":186},[108,6398,595],{"class":114},[108,6400,598],{"class":146},[108,6402,601],{"class":118},[108,6404,604],{"class":118},[108,6406,607],{"class":118},[108,6408,610],{"class":146},[108,6410,6411,6413,6415,6417,6419,6421,6423,6425,6427],{"class":110,"line":196},[108,6412,615],{"class":114},[108,6414,201],{"class":150},[108,6416,620],{"class":118},[108,6418,623],{"class":150},[108,6420,626],{"class":118},[108,6422,629],{"class":118},[108,6424,632],{"class":118},[108,6426,635],{"class":118},[108,6428,638],{"class":118},[108,6430,6431,6433,6435,6437,6439,6441,6443],{"class":110,"line":210},[108,6432,643],{"class":114},[108,6434,646],{"class":118},[108,6436,649],{"class":118},[108,6438,652],{"class":150},[108,6440,655],{"class":114},[108,6442,658],{"class":118},[108,6444,176],{"class":150},[108,6446,6447,6449,6451,6453,6455,6457,6459,6461,6463],{"class":110,"line":222},[108,6448,615],{"class":114},[108,6450,201],{"class":150},[108,6452,669],{"class":118},[108,6454,623],{"class":150},[108,6456,626],{"class":118},[108,6458,629],{"class":118},[108,6460,678],{"class":118},[108,6462,635],{"class":118},[108,6464,683],{"class":118},[108,6466,6467,6469,6471,6473,6475,6477,6479,6481,6483,6485,6487,6489,6491,6493,6495],{"class":110,"line":233},[108,6468,688],{"class":114},[108,6470,691],{"class":114},[108,6472,694],{"class":114},[108,6474,697],{"class":114},[108,6476,694],{"class":114},[108,6478,702],{"class":114},[108,6480,705],{"class":114},[108,6482,646],{"class":118},[108,6484,201],{"class":150},[108,6486,712],{"class":118},[108,6488,623],{"class":150},[108,6490,717],{"class":118},[108,6492,720],{"class":118},[108,6494,723],{"class":118},[108,6496,193],{"class":150},[108,6498,6499,6501,6503,6505],{"class":110,"line":251},[108,6500,730],{"class":114},[108,6502,167],{"class":150},[108,6504,735],{"class":118},[108,6506,738],{"class":150},[54,6508,741,6509,744,6511,747],{},[79,6510,517],{},[79,6512,513],{},[99,6514,6515],{"className":521,"code":750,"language":523,"meta":524,"style":104},[79,6516,6517,6521,6525],{"__ignoreMap":104},[108,6518,6519],{"class":110,"line":111},[108,6520,757],{},[108,6522,6523],{"class":110,"line":143},[108,6524,762],{},[108,6526,6527],{"class":110,"line":179},[108,6528,767],{},[54,6530,770],{},[264,6532,6533,6537,6541],{},[267,6534,6535],{"icon":314,"title":775,"to":776,"target":75},[54,6536,779],{},[267,6538,6539],{"icon":277,"title":782,"to":783,"target":75},[54,6540,786],{},[267,6542,6543],{"icon":277,"title":789,"to":790,"target":75},[54,6544,793],{},[795,6546,797],{},{"title":104,"searchDepth":143,"depth":143,"links":6548},[6549,6550,6551,6552,6553,6554],{"id":51,"depth":143,"text":52},{"id":64,"depth":143,"text":65},{"id":88,"depth":143,"text":89},{"id":285,"depth":143,"text":286},{"id":322,"depth":143,"text":323},{"id":416,"depth":143,"text":417,"children":6555},[6556],{"id":471,"depth":179,"text":472},{},{"title":40,"description":810},[817,818,819,820,821],[6561,822],{"title":36,"path":37,"stem":38,"description":4169,"children":-1},{"id":44,"title":40,"body":6563,"color":808,"date":809,"description":810,"draft":21,"extension":811,"image":812,"meta":7069,"navigation":182,"oneliner":814,"path":41,"seo":7070,"stem":42,"tags":7071,"url":822,"__hash__":823},{"type":46,"value":6564,"toc":7059},[6565,6567,6569,6571,6573,6580,7057],[49,6566,52],{"id":51},[54,6568,56],{},[58,6570],{"exclude":60,":max-depth":61},[49,6572,65],{"id":64},[54,6574,68,6575,77,6578,82],{},[70,6576,76],{"href":72,"rel":6577,"target":75},[74],[79,6579,81],{},[84,6581,6582,6584,6588,6600,6604,6696,6700,6710,6712,6714,6716,6720,6722,6732,6734,6738,6740,6744,6746,6772,6774,6788,6790,6792,6794,6822,6824,6826,6828,6832,6836,6840,7043],{"level":61},[49,6583,89],{"id":88},[54,6585,92,6586,97],{},[94,6587,96],{},[99,6589,6590],{"className":101,"code":102,"language":103,"meta":104,"style":104},[79,6591,6592],{"__ignoreMap":104},[108,6593,6594,6596,6598],{"class":110,"line":111},[108,6595,115],{"class":114},[108,6597,119],{"class":118},[108,6599,123],{"class":122},[54,6601,126,6602,130],{},[79,6603,129],{},[99,6605,6606],{"className":101,"code":133,"language":103,"meta":104,"style":104},[79,6607,6608,6612,6636,6640,6646,6656,6666,6676,6690],{"__ignoreMap":104},[108,6609,6610],{"class":110,"line":111},[108,6611,140],{"class":122},[108,6613,6614,6616,6618,6620,6622,6624,6626,6628,6630,6632,6634],{"class":110,"line":143},[108,6615,147],{"class":146},[108,6617,151],{"class":150},[108,6619,115],{"class":114},[108,6621,119],{"class":118},[108,6623,158],{"class":150},[108,6625,161],{"class":114},[108,6627,164],{"class":118},[108,6629,167],{"class":150},[108,6631,170],{"class":118},[108,6633,173],{"class":150},[108,6635,176],{"class":150},[108,6637,6638],{"class":110,"line":179},[108,6639,183],{"emptyLinePlaceholder":182},[108,6641,6642,6644],{"class":110,"line":186},[108,6643,190],{"class":189},[108,6645,193],{"class":150},[108,6647,6648,6650,6652,6654],{"class":110,"line":196},[108,6649,190],{"class":189},[108,6651,201],{"class":150},[108,6653,204],{"class":118},[108,6655,207],{"class":150},[108,6657,6658,6660,6662,6664],{"class":110,"line":210},[108,6659,190],{"class":189},[108,6661,201],{"class":150},[108,6663,217],{"class":118},[108,6665,207],{"class":150},[108,6667,6668,6670,6672,6674],{"class":110,"line":222},[108,6669,190],{"class":189},[108,6671,201],{"class":150},[108,6673,204],{"class":118},[108,6675,207],{"class":150},[108,6677,6678,6680,6682,6684,6686,6688],{"class":110,"line":233},[108,6679,190],{"class":189},[108,6681,201],{"class":150},[108,6683,240],{"class":118},[108,6685,243],{"class":150},[108,6687,147],{"class":146},[108,6689,248],{"class":150},[108,6691,6692,6694],{"class":110,"line":251},[108,6693,190],{"class":189},[108,6695,193],{"class":150},[54,6697,258,6698,262],{},[79,6699,261],{},[264,6701,6702,6706],{},[267,6703,6704],{"icon":269,"title":270,"to":271,"target":75},[54,6705,274],{},[267,6707,6708],{"icon":277,"title":278,"to":279,"target":75},[54,6709,282],{},[49,6711,286],{"id":285},[54,6713,289],{},[54,6715,292],{},[54,6717,295,6718,299],{},[79,6719,298],{},[54,6721,302],{},[264,6723,6724,6728],{},[267,6725,6726],{"icon":307,"title":308,"to":15},[54,6727,311],{},[267,6729,6730],{"icon":314,"title":315,"to":316,"target":75},[54,6731,319],{},[49,6733,323],{"id":322},[54,6735,326,6736,330],{},[79,6737,329],{},[54,6739,333],{},[54,6741,336,6742,340],{},[79,6743,339],{},[54,6745,343],{},[345,6747,6748,6758,6764],{},[348,6749,6750,353,6752,357,6754,353,6756,364],{},[79,6751,352],{},[79,6753,356],{},[79,6755,360],{},[79,6757,363],{},[348,6759,6760,353,6762,373],{},[79,6761,369],{},[79,6763,372],{},[348,6765,6766,379,6768,383,6770,387],{},[79,6767,378],{},[79,6769,382],{},[79,6771,386],{},[54,6773,390],{},[264,6775,6776,6780,6784],{},[267,6777,6778],{"icon":314,"title":395,"to":396,"target":75},[54,6779,399],{},[267,6781,6782],{"icon":269,"title":402,"to":403,"target":75},[54,6783,406],{},[267,6785,6786],{"icon":277,"title":409,"to":410,"target":75},[54,6787,413],{},[49,6789,417],{"id":416},[54,6791,420],{},[54,6793,423],{},[99,6795,6796],{"className":426,"code":427,"filename":428,"language":429,"meta":104,"style":104},[79,6797,6798,6802,6806,6810,6814,6818],{"__ignoreMap":104},[108,6799,6800],{"class":110,"line":111},[108,6801,436],{},[108,6803,6804],{"class":110,"line":143},[108,6805,441],{},[108,6807,6808],{"class":110,"line":179},[108,6809,446],{},[108,6811,6812],{"class":110,"line":186},[108,6813,451],{},[108,6815,6816],{"class":110,"line":196},[108,6817,456],{},[108,6819,6820],{"class":110,"line":210},[108,6821,461],{},[54,6823,464],{},[54,6825,467],{},[469,6827,472],{"id":471},[54,6829,475,6830,479],{},[79,6831,478],{},[54,6833,482,6834,262],{},[79,6835,485],{},[54,6837,488,6838,492],{},[79,6839,491],{},[494,6841,6842,6848],{},[54,6843,6844],{},[499,6845,6846],{},[502,6847,504],{},[506,6849,6850,6856,6876,6881,7041],{},[54,6851,510,6852,514,6854,518],{},[79,6853,513],{},[79,6855,517],{},[99,6857,6858],{"className":521,"code":522,"language":523,"meta":524,"style":104},[79,6859,6860,6864,6868,6872],{"__ignoreMap":104},[108,6861,6862],{"class":110,"line":111},[108,6863,531],{},[108,6865,6866],{"class":110,"line":143},[108,6867,536],{},[108,6869,6870],{"class":110,"line":179},[108,6871,541],{},[108,6873,6874],{"class":110,"line":186},[108,6875,546],{},[54,6877,549,6878,554],{},[70,6879,485],{"href":552,"rel":6880},[74],[556,6882,6883,6885,6887,7019,7025],{},[559,6884,561],{},[54,6886,564],{},[99,6888,6889],{"className":101,"code":567,"language":103,"meta":104,"style":104},[79,6890,6891,6895,6903,6907,6921,6941,6957,6977,7009],{"__ignoreMap":104},[108,6892,6893],{"class":110,"line":111},[108,6894,574],{"class":118},[108,6896,6897,6899,6901],{"class":110,"line":143},[108,6898,579],{"class":114},[108,6900,582],{"class":118},[108,6902,585],{"class":146},[108,6904,6905],{"class":110,"line":179},[108,6906,590],{"class":114},[108,6908,6909,6911,6913,6915,6917,6919],{"class":110,"line":186},[108,6910,595],{"class":114},[108,6912,598],{"class":146},[108,6914,601],{"class":118},[108,6916,604],{"class":118},[108,6918,607],{"class":118},[108,6920,610],{"class":146},[108,6922,6923,6925,6927,6929,6931,6933,6935,6937,6939],{"class":110,"line":196},[108,6924,615],{"class":114},[108,6926,201],{"class":150},[108,6928,620],{"class":118},[108,6930,623],{"class":150},[108,6932,626],{"class":118},[108,6934,629],{"class":118},[108,6936,632],{"class":118},[108,6938,635],{"class":118},[108,6940,638],{"class":118},[108,6942,6943,6945,6947,6949,6951,6953,6955],{"class":110,"line":210},[108,6944,643],{"class":114},[108,6946,646],{"class":118},[108,6948,649],{"class":118},[108,6950,652],{"class":150},[108,6952,655],{"class":114},[108,6954,658],{"class":118},[108,6956,176],{"class":150},[108,6958,6959,6961,6963,6965,6967,6969,6971,6973,6975],{"class":110,"line":222},[108,6960,615],{"class":114},[108,6962,201],{"class":150},[108,6964,669],{"class":118},[108,6966,623],{"class":150},[108,6968,626],{"class":118},[108,6970,629],{"class":118},[108,6972,678],{"class":118},[108,6974,635],{"class":118},[108,6976,683],{"class":118},[108,6978,6979,6981,6983,6985,6987,6989,6991,6993,6995,6997,6999,7001,7003,7005,7007],{"class":110,"line":233},[108,6980,688],{"class":114},[108,6982,691],{"class":114},[108,6984,694],{"class":114},[108,6986,697],{"class":114},[108,6988,694],{"class":114},[108,6990,702],{"class":114},[108,6992,705],{"class":114},[108,6994,646],{"class":118},[108,6996,201],{"class":150},[108,6998,712],{"class":118},[108,7000,623],{"class":150},[108,7002,717],{"class":118},[108,7004,720],{"class":118},[108,7006,723],{"class":118},[108,7008,193],{"class":150},[108,7010,7011,7013,7015,7017],{"class":110,"line":251},[108,7012,730],{"class":114},[108,7014,167],{"class":150},[108,7016,735],{"class":118},[108,7018,738],{"class":150},[54,7020,741,7021,744,7023,747],{},[79,7022,517],{},[79,7024,513],{},[99,7026,7027],{"className":521,"code":750,"language":523,"meta":524,"style":104},[79,7028,7029,7033,7037],{"__ignoreMap":104},[108,7030,7031],{"class":110,"line":111},[108,7032,757],{},[108,7034,7035],{"class":110,"line":143},[108,7036,762],{},[108,7038,7039],{"class":110,"line":179},[108,7040,767],{},[54,7042,770],{},[264,7044,7045,7049,7053],{},[267,7046,7047],{"icon":314,"title":775,"to":776,"target":75},[54,7048,779],{},[267,7050,7051],{"icon":277,"title":782,"to":783,"target":75},[54,7052,786],{},[267,7054,7055],{"icon":277,"title":789,"to":790,"target":75},[54,7056,793],{},[795,7058,797],{},{"title":104,"searchDepth":143,"depth":143,"links":7060},[7061,7062,7063,7064,7065,7066],{"id":51,"depth":143,"text":52},{"id":64,"depth":143,"text":65},{"id":88,"depth":143,"text":89},{"id":285,"depth":143,"text":286},{"id":322,"depth":143,"text":323},{"id":416,"depth":143,"text":417,"children":7067},[7068],{"id":471,"depth":179,"text":472},{},{"title":40,"description":810},[817,818,819,820,821],1789678542078]