#FactoryGame

Ok. So. Conveyors hard. Well. Performant conveyors hard.

---

There's the relative distance approach. Items track the distance to the next item on thr belt. This means you only need to update the distance of the first item on the belt, and the other items are updated as they're effectively relative to it. To my knowledge, this is the approach factorio uses (these days,).

Pro: Cheap to update, cheap to change belt length, arbitrary belt length

Cons: Viewing, inserting items or extracting items requires iteration of items from the head to the relevant distance along the belt.

Con mitigations: Caching (last time we were waiting for item #18 and it was 5 units away, don't check until we think it should have arrived or an item is inserted between 17 & 18 or of 18 is removed before it gets to us ie.)

Con mitigation con: ow. no thanks. exploiting temporal coherence is an exercise left to the reader.

---

Then there's the timestamp approach. It's an evolution of the relative distance approach. Instead of tracking relative distance, you mark the timestamp it would reach the end of the belt at, if unobstructed. To resolve the position of an item you pick the higher value of: a) the time between it's end time and the current time, and b) the # item in the queue it is. B ensures items don't overlap, and instead accumulate at the head of the belt.

Pro: Less iteration required, belts have 0 update cost

Con: Factorio maps have already run long enough that 32bit timestamps aren't enough, lengthening/shortening belts from the head is expensive, arbitrary belt length

Con minimisation: when the 32 bit timestamp is hit pause the game and adjust timestamps to exist closer to zero, when lengthening or shortening belts track the "time offset" (apply to all belt timestamps when save/load or offset limit hit)

Con minimisation con: gross, but the second one is probably fine

---

The approach I came up with is what I call the occupany map approach. We represent each belt as a 32/64 bit interger. We do some shifting magic involving
blsr, which causes the bits to shift right but to never shift 0s out, causing them to accumulate. 0s represent items on the belt and their position, and the number of 0 bits before an item determines its index in an array.

Pros: updates are cheap (like 4x 1 clock cost instructions), memory footprint amortizes well (64bit/63 items*, ~1 bit per item vs 32bits/item), position of items is explict and absolute.

Cons: belt lengths are limited, machines without the BMI1 instruction set will have less terse asm

Con mitigation: we'll get into this with my next post because this is what I've been scraping my face against

* 31/63 items is due to needing to reserve the top bit to cause 1s to shift in naturally, instead of requiring a branch



- posted by Natalie
The issue with limited belt length is hard to solve in a performance friendly way, and the other two methods are also victim to this as they may wish to place an upper limit on iteration by splitting belts.

This is because it introduces ordering requirements. Say belt B joins to belt A. If belt A is has an item at the end and we update belt B, then we won't transfer an item to belt A. Additionally, if there isn't an item blocking B from transferring the item and we update it first, the transferred item will get moved twice.

Naively, we could track a list of conveyor heads and link-list style refer to the next one and update following them in-order. That's a lot of random access though, and not performant.

Improving on that we could then either move belt segments into an ordered array on the heap, or bucket belts by their update order. Both of these options kinda suck for splitting/joining long belts, and updating attached things.

There's the option too of requiring a "joiner" "machine". That basically buffers the input and helps remove the ordering requirement for some extra processing. Sort of like mergers and splitters. Definitely a solution, but, one that's intrusive to the player. Also, I'm not sure how the perf stacks up.

We can improve on that though. How about we make the head of a joining belt into this? Automatically stick a machine on, invisible and silent. Better. Less intrusive.

I've also thought about merging that "machine" into the belt itself, reserving the head and tail for belt transfers, having them basically overlap to hide the delayed move or dpuble move. Could definitely work too, and I plan to explore both these methods.

The invisible machie approach probably has more promise, but items in a superposition sounds rad and a maintenance nightmare. Choices.... Hmmm.....

Oh and this isn't even considering factorio style belts where belts flow onto belts that meet in the middle of. Am considering disallowing that, not for perf reasons but for gameplay, but perf/complexity reasons might push me over the edge on that. The "joiner machine" could work well here if I want to allow this.

---

Finally there's also the question of will 63-length belt segments become a bottleneck in terms of memory throughput? I don't think so, as iirc factorio is using shorter belt segments internally around 8-16 irc. And yeah, they're hitting memory throughput limits, but thr game runs well enough

- posted by Natalie

@nyatalily If I can give my 2 cents: I don't think it's worthwhile worrying too much about the cost updating attached machines since it is by far the more rare case since presumably that's only done by the player.

I would expect you can get around a ton of random-access woes by arranging the belts in memory with a topological sort. I haven't fully thought this through, but you ought to be able to do blind writes through the linked-list pointer but never any reads since those are via flat iteration.

I don't know if that made any sense because what I wrote only sorta makes sense to me

@nyatalily btw if you find that you're in a situation where it would be beneficial to partition a graph by minimizing both cut and euclidean radius of clusters I have an unpublished paper that is currently a solution in search of a problem

Factory game seems like it's definitely a case where that could be nice

@gfaster ooooo, that sounds pretty interesting, unfortunately I'm not well read enough to know how applicable it'd be to my situation 🤔 Regardless, I'd love to read it sometime!

- posted by Natalie

@nyatalily It would be for optimizing locality for both simulation and viewing. A mincut partition of the "factory" graph would mean you know there will be a higher degree of spacial locality for accesses if you put everything in the same partition close in memory.

I think that was almost entirely unintelligible so an example: imagine you have a mining outpost that is a mostly isolated system except the one connection to the base. No matter what system you decide on for belts, it'll probably be beneficial for every machine/belt in the outpost to be near each other in memory, but the connection to the rest of the base matters less since it's just a single line - it's fine if there's a cache miss there. A mincut partition would put the mining outpost in its own partition, and minimize the number of likely cache misses from traversing the graph.

The added euclidean constraint is frankly less useful, but it gives a spacial locality bound on any rendering view.

@gfaster ahhhh, interesting, I see what you mean.

I've considered something like this for power networks, but kind of the opposite I suppose? Usually it's expensive to calculate transmission loss due to the interconnectedness and size of these networks in this style of game. But i was figuring calculating the cost in a tight cluster isn't actually that high impact, but that one line running from your main base to the outpost? Probably more significant in terms of impact.

I didn't come up with a solution that didn't require something intrusive to the player

- posted by Natalie

@nyatalily unrelated to the implementation details I remember reading recently (though I absolutely cannot remember where) that in the real world, transmission loss on the scale of factorio is a rounding error. The thing that would be a huge issue is current maximums.

Of course this leads dangerously close to https://xkcd.com/356/ and gets into some math that is a tad over my head. I was lead down this rabbit hole when I was trying to make a Factorio base layout optimizer using electric current flow as a traffic anologue for the multi-commodity flow model. I found the (shockingly difficult to find) Wikipedia page (https://en.wikipedia.org/wiki/Resistance_distance ) on the subject and looking back I might be able to take another shot at it

Nerd Sniping

xkcd
@gfaster lol, I'm definitely a nerd sniping target 😅

Yeah, real world transmission loss definitely isn't worth considering on this scale, it would've been artificially high, the goal was to incentivise the creation of outposts and local power generation.

Everyday I'm getting closer to taking a course in graph theory though, stg. I need it.

- posted by Natalie
@gfaster also i gotta say, that base layout optimizer sounds pretty interesting!

- posted by Natalie
@nyatalily it's sat idle for a couple years now I really ought to just polish it up to write a blog post or something