Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Signal and dust

Verified against Minecraft 26.3 · Part V · A lever on the floor is flipped on and then off again, and two redstone dust to the east of it go to 15 and 14 — and then count their way back down.

You flip a lever, and the dust beside it turns bright. Flip it back and the line goes dark — in one tick, but not in one pass. Each wire recomputes its own strength from scratch, writes it, and then hand-issues seven Level.updateNeighborsAt calls: its own position and all six neighbours. Each of those seven visits six neighbours of its own, so one wire whose power changed costs forty-two neighbour updates (DefaultRedstoneWireEvaluator.updatePowerStrength). And because the recursion stops on value rather than on distance, the wire at the far end is reached once for every intermediate value the near end passes through on the way down. That staircase is the whole cost of redstone dust, and nobody has ever seen it: the cascade finishes inside one packet handler, and the packet a client is sent is built later in the same tick from whatever the position holds by then. The game ships a second implementation of exactly this computation, behind a feature flag, which walks the whole connected network in two ordered phases and does not produce the staircase at all.

The cast

classwhat it decidesthread
SignalGetterevery question about power: what a position emits, what reaches it, and the direction order the answers are gathered ina Level interface, either side
BlockBehaviour.BlockStateBasethe three power answers this trace asks a state for — is it a source, what is its weak signal per face, what is its strong signal — beside whether it conducts and whether a wire connects to it. The analog pair is the comparator’seither side
LeverBlockthe trace’s source: 15 in every direction, and 15 strongly into one block onlyserver — the client’s copy writes nothing
RedstoneWireBlockwhich sides a wire connects to, what it emits through them, and the mutable flag that stops it counting itselfserver for power; its connections on both sides
RedstoneWireEvaluatorminus one per block: what the neighbouring wires are worth to this oneserver
DefaultRedstoneWireEvaluatorone wire at a time, recursively, with the fan-out issued by handserver
ExperimentalRedstoneWireEvaluatorevery wire the change reaches, at once, in two phases, everything off before anything on, allocated fresh per callserver
Orientationin the experimental mode only, where an update came from, so the fan-out can be ordered relative to itserver

What one neighbour update to a wire costs

Redstone dust has no scheduler and no graph. It is what happens when blocks answer two questions about their neighbours — how much signal do you give me and did something near you change — through the same BlockBehaviour.neighborChanged fan-out every neighbour update uses. This is the whole of the default implementation, from one update arriving to the next batch leaving.

flowchart TD
    IN["RedstoneWireBlock.neighborChanged"]
    CLIENT{"is this a ClientLevel"}
    NOTHING["nothing at all"]
    CALC["DefaultRedstoneWireEvaluator.calculateTargetStrength"]
    BLK["RedstoneWireBlock.getBlockSignal, with RedstoneWireBlock.shouldSignal off"]
    WIRE["RedstoneWireEvaluator.getIncomingWireSignal"]
    TARGET["the target: the block signal, or the larger of the two"]
    SAME{"is the target the POWER already stored"}
    STOP["no write, no fan-out: the cascade ends here"]
    WRITE["Level.setBlock under Block.UPDATE_CLIENTS alone"]
    FAN["seven Level.updateNeighborsAt calls: this position and its six neighbours"]
    OUT["forty-two neighbour updates, queued a layer at a time"]
    IN --> CLIENT
    CLIENT -- "yes" --> NOTHING
    CLIENT -- "no" --> CALC
    CALC --> BLK
    BLK -- "under 15" --> WIRE --> TARGET
    BLK -- "15, so the wires are never asked" --> TARGET
    TARGET --> SAME
    SAME -- "yes" --> STOP
    SAME -- "no" --> WRITE --> FAN --> OUT
    OUT -- "every wire among them, again" --> IN

One neighbour update to one wire, and the loop at the bottom is the page: forty-two updates leave, some of them land on wires, and each of those re-enters at the top. The cascade ends only where a recomputed target equals the RedstoneWireBlock.POWER already stored, which is a test on the value and not on the distance.

The first branch is belt and braces. RedstoneWireBlock.neighborChanged opens with a not-client test, and it would never be reached on a ClientLevel anyway: Level.updateNeighborsAt and Level.neighborChanged are empty methods there, so nothing on the client ever dispatches a neighbour update to a block at all (blocks and states).

The two facts that make the staircase are both in that figure. The write uses Block.UPDATE_CLIENTS alone — flag 2, the broadcast bit and nothing else (block update flags) — so the fan-out is not the one Level.setBlock would have done: it is issued afterwards, by hand, over seven positions rather than one. And the recursion terminates on value, not on distance: a wire whose recomputed strength equals what it already holds writes nothing and tells nobody. A line going dark therefore re-enters every wire again at every step of the descent, and each visit is the last one only when the value has stopped moving. None of those intermediate writes is ever sent: Level.sendBlockUpdated records the position and not the state, and the once-a-tick flush reads the level back to build the packet (what the client is told).

What a block answers when it is asked for power

Three questions, all on BlockBehaviour.BlockStateBase, all answered by the block. BlockBehaviour.BlockStateBase.isSignalSource is whether the block emits at all. BlockBehaviour.BlockStateBase.getSignal is the weak signal it offers to a given face. BlockBehaviour.BlockStateBase.getDirectSignal is the strong one, and the difference between them is entirely a matter of who is allowed to pass it on. Conduction is BlockBehaviour.BlockStateBase.isRedstoneConductor, from BlockBehaviour.Properties.isRedstoneConductor, which defaults to is this state’s collision shape a full block.

SignalGetter is where the two meet, and the join is easy to get backwards. SignalGetter.getSignal reads the block’s own weak signal and then, only if that block is a redstone conductor, takes the larger of it and SignalGetter.getDirectSignalTo — the strongest signal being pushed into that position from any of its six neighbours. It is a maximum, not a choice between two modes: a powered conductor offers the greater of what it emits itself and what is being forced into it. That single line is what “strongly powered” means, and it is why a block with a lever on it powers the dust beside it. A conducting block a wire merely points into is strongly powered too — that is how a line lights a lamp through the block at its end — but no other dust can see it, for a reason that has nothing to do with this line and everything to do with RedstoneWireBlock.shouldSignal, below.

The lever shows both halves at once. LeverBlock.ownSignal is 15 in every direction when powered — that is the weak signal, and it is what the dust next to the lever reads. LeverBlock.getDirectSignal is 15 only into the one block the lever is attached to. So a conducting block behind a lever is strongly powered, and everything touching that block sees 15 too.

The other sources answer the same three questions and differ mostly in what they answer and when they change their mind: ButtonBlock books a scheduled tick to turn itself off, BasePressurePlateBlock and its two subclasses re-read what is standing on them, DetectorRailBlock watches for entities and TripWireHookBlock for its tripwire, DaylightDetectorBlock reads the sky, and RedstoneTorchBlock and RedstoneWallTorchBlock invert whatever they are attached to. None of these needs a section of its own, because the contract above is nearly the whole of what a circuit sees; the repeater and the observer, which also answer for themselves whether a wire connects, belong to the part’s last lecture.

One of them keeps state the contract cannot see, and it is the reason a fast clock burns out. RedstoneTorchBlock.RECENT_TOGGLES is a weak map from level to a list of toggles; RedstoneTorchBlock.tick prunes anything older than 60 ticks off the front of it, and RedstoneTorchBlock.isToggledTooFrequently burns the torch out on the eighth surviving entry for that position, to try again 160 ticks later. Those are the numbers RedstoneTorchBlock.MAX_RECENT_TOGGLES, RedstoneTorchBlock.RECENT_TOGGLE_TIMER and RedstoneTorchBlock.RESTART_DELAY name: 8, 60 and 160.

Three direction orders, and only one of them is about reading

Three fixed direction orders run through this page and they are not interchangeable. Two decide who gets told something; the third decides what a block reads, and it is the one this page uses.

arrayorderwhat it governs
SignalGetter.DIRECTIONSdown, up, north, south, west, eastwhat a block reads. Only SignalGetter.getBestNeighborSignal walks the array; SignalGetter.getDirectSignalTo and SignalGetter.hasNeighborSignal are written out by hand in the same order
NeighborUpdater.UPDATE_ORDERwest, east, down, up, north, southwhich neighbour is told first about a change, on the neighbour channel
BlockBehaviour.UPDATE_SHAPE_ORDERwest, east, north, south, down, upwhich neighbour is asked first to re-fit, on the shape channel

Only the first row is about reading, and the three reading methods in it stop early, not on the same thing: the two that return a number — SignalGetter.getBestNeighborSignal and SignalGetter.getDirectSignalTo — stop at a 15, while SignalGetter.hasNeighborSignal, which returns a boolean, stops at the first answer above zero. That is not a micro-optimisation with no consequences: a position saturated from one side never reads the sides after it. The other two rows are orders in which something is told, and a fan-out never stops early. SignalGetter.DIRECTIONS is plain Direction order, which is the only reason its first entry is down.

Dust, and how far it reaches

RedstoneWireBlock.POWER is the number, 0 to 15. Four RedstoneSide properties — one per horizontal, each NONE, SIDE or UP — record how the wire is drawn and, more importantly, which sides it talks through. What a wire is worth to its neighbours is RedstoneWireEvaluator.getIncomingWireSignal, and the minus one per block everyone knows lives in its last line: it takes the best of the wires it can see and subtracts one, floored at zero. That subtraction is the whole of the reach: in a line whose first wire holds 15, the fifteenth holds 1 and the sixteenth 0, which is a dark block rather than a shorter line. The wires it can see are the four beside it, plus the wire on top of a conducting neighbour when nothing conducts above this position, plus the wire below a non-conducting neighbour — which is the power half of “dust climbs a block and falls down one”. The drawing half is RedstoneWireBlock.getConnectingSide below, which asks BlockBehaviour.BlockStateBase.isFaceSturdy for the climb where this one asks about conduction.

Two asymmetries follow from RedstoneWireBlock.getSignal and are worth stating plainly, because they are the two questions every redstone build eventually asks. A wire returns zero when the direction asked about is Direction.DOWN — so dust never powers the block above it. It returns its full power without any connection test when the direction is Direction.UP — so dust always powers the block below it. In every other direction it answers only if its connection on the opposite side is made.

Connection itself is two rules and a completion pass. RedstoneWireBlock.shouldConnectTo is the real one, and it asks the neighbour’s own BlockBehaviour.shouldRedstoneWireConnectTo: another wire always, a RepeaterBlock along its own axis, an ObserverBlock only on its output side, opposite the one it watches, and by default any block that says it is a signal source — that last clause only when a direction is supplied, which the vertical rules do not do, so up and down connect to wire and to nothing else. RedstoneWireBlock.getConnectingSide adds the vertical cases — up over a face-sturdy neighbour, down past a non-conducting one. And then RedstoneWireBlock.getConnectionState runs a completion pass that produces most of the confusion: if a wire has no north or south connection, west and east are set anyway, and the same the other way round. That is why a lone dust is drawn as a cross, and why a wire fed from the west appears to point firmly into whatever is on its east — a piston, say — which satisfies neither real rule. The piston is not a source and, by Blocks.pistonProperties, not a conductor. It gets powered anyway, because the wire’s east side is SIDE by completion and RedstoneWireBlock.getSignal asks about the side, not about the neighbour.

RedstoneWireBlock.shouldSignal is the oddest thing on the page: a mutable boolean on the block singleton, flipped false for the duration of RedstoneWireBlock.getBlockSignal so that a wire does not count itself or its neighbouring wires as sources while it works out its block power. It works because the Server thread is the only writer and never re-enters the method. It is also the answer to the question left open above: a conducting block a wire merely points into is strongly powered, and no other dust can see that, because while any wire is asking what its block power is, every wire in the world temporarily answers that it is not a source at all.

The lever, two dust, and a powered piston

sequenceDiagram
    participant LevB as LeverBlock
    participant SL as ServerLevel
    participant CNU as Collecting<br/>NeighborUpdater
    participant RWB as RedstoneWireBlock
    participant DRWE as DefaultRedstone<br/>WireEvaluator
    participant PBB as PistonBaseBlock
    Note over LevB,PBB: one call stack, inside one packet handler, before the level ticks
    LevB->>SL: setBlockAndUpdate, POWERED with flags 3
    SL->>CNU: updateNeighbors<br/>AtExceptFromFacing<br/>drained at once
    CNU->>RWB: neighborChanged at the first dust
    RWB->>DRWE: updatePowerStrength, and the lever answers 15
    DRWE->>SL: setBlock POWER 15 with flag 2, then seven updateNeighborsAt
    CNU->>RWB: neighborChanged at the second dust, ahead of the lever's other directions
    RWB->>DRWE: updatePowerStrength, block signal 0 and a wire at 15
    DRWE->>SL: setBlock POWER 14 with flag 2, then seven more
    CNU->>PBB: neighborChanged, and the wire answers from its east side
    Note over CNU,PBB: the remaining dozens of updates change nothing
    LevB->>SL: updateNeighborsAt, two more fan-outs by hand

The same cascade in time, and the thing to watch is the order: the second dust’s whole turn — its recompute, its write and its seven fan-outs — finishes before the lever has issued its own remaining directions. That is the queue being drained depth-first, inside one call stack, with no tick anywhere in it.

The lever is the place in this trace where the client does not even try. LeverBlock.useWithoutItem writes no state on a ClientLevel — it spawns a particle, and only when the lever is going on — so unlike a door, a lever is not predicted, and the client’s dust changes colour only when the block updates arrive. The dust and the piston do nothing on the client either, but for the ordinary reason: nothing ever calls their BlockBehaviour.neighborChanged there. The sound follows the same split for a different reason: LeverBlock.pull is handed a null player, so nobody is excluded and the clicker hears the server’s ClientboundSoundPacket like everyone else. Compare block interaction, where the door passes the clicker as except and they hear their own prediction instead.

Two details in the diagram are worth naming. The lever writes through LevelWriter.setBlockAndUpdate, which is flags 3, Block.UPDATE_ALL, so Block.UPDATE_NEIGHBORS fans out once before LeverBlock.updateNeighbours fans out twice more, at the lever’s own position and at the block it stands on — and because nothing was running when the first one was queued, it drains the entire dust cascade before the other two are even issued. And the ordering of the seven positions a wire updates is fixed by no array: they come out of a hash set. The depth-first drain of CollectingNeighborUpdater is the subject of block interaction, and it is what puts the second dust’s whole cascade ahead of the lever’s remaining directions.

Placing and breaking a wire take a wider path again: RedstoneWireBlock.onPlace and RedstoneWireBlock.affectNeighborsAfterRemoval both call RedstoneWireBlock.updateNeighborsOfNeighboringWires, which walks the four horizontals and then the diagonals — reaching over a conducting neighbour and under a non-conducting one — and calls RedstoneWireBlock.checkCornerChangeAt on each. That is another seven Level.updateNeighborsAt per wire found. Which write does what is blocks and states.

The second implementation

FeatureFlags.REDSTONE_EXPERIMENTS is a feature flag (identifiers and registries), turned on by a built-in data pack whose entire content is the line that enables it (the resource system), and RedstoneWireBlock.useExperimentalEvaluator asks the level for it on every call — RedstoneWireBlock.evaluator is always the default one, and an ExperimentalRedstoneWireEvaluator is a fresh object per update, because it carries working state. What changes is the semantics, and the cost with it.

Run the same lever back the other way — off, so the line has to go dark, which is where the default evaluator does its counting down — and follow the two dust through ExperimentalRedstoneWireEvaluator.calculateCurrentChanges instead. Nothing is written until the whole connected network has been computed. Phase one drains ExperimentalRedstoneWireEvaluator.wiresToTurnOff, and this is the phase that kills the staircase: a wire whose recomputed power is lower than what it holds goes to zero in a working map rather than to its new value, so the two dust do not count down past each other, 13 and 12, 11 and 10, on the way to nothing — each goes straight to nothing, and each is re-queued for the second phase only if it has block power of its own or a stronger wire beside it. Phase two drains ExperimentalRedstoneWireEvaluator.wiresToTurnOn and raises each survivor to its true value, which reaches the world in one write. Both phases spread through ExperimentalRedstoneWireEvaluator.propagateChangeToNeighbors and ExperimentalRedstoneWireEvaluator.enqueueNeighborWire, recording every wire they reach in ExperimentalRedstoneWireEvaluator.updatedWires, an insertion-ordered map of position to a packed orientation and power — which is the working state that makes the evaluator a fresh object per call. The write pass at the end drops any entry whose stored power already matches, so what reaches the world is exactly the wires that really changed — each with Block.UPDATE_CLIENTS and, for every wire the pass touches, Block.UPDATE_SKIP_SHAPE_UPDATE_ON_WIRE, the bit that makes NeighborUpdater.executeShapeUpdate skip any shape update whose target is dust. (The one wire exempted from that bit is the one an evaluation started from, and then only when the caller was RedstoneWireBlock.onPlace, which wants the shape updates around a newly placed wire.)

The fan-out is the second difference, and it is what the seven positions become. ExperimentalRedstoneWireEvaluator.causeNeighborUpdates issues one Level.neighborChanged per connected side per changed wire — the four horizontals the wire’s own state records, plus Direction.DOWN unconditionally and Direction.UP never — in Orientation.getDirections order, which is derived from where the update came from rather than from a fixed array, and then five more at any side that is a redstone conductor. So the piston east of our two dust is still told, and told once, without the seven-position scattergun. The recursion closes because RedstoneWireBlock.neighborChanged ignores wire-sourced updates entirely in this mode; the default evaluator does not, which is what opens it.

For a 1.21-era reader. BlockBehaviour.neighborChanged now takes a nullable Orientation rather than a source BlockPos — which is what lets the experimental evaluator order a fan-out relative to where the update came from — and BlockBehaviour.affectNeighborsAfterRemoval, which replaced the old removal hook, does not take one at all. RedStoneWireBlock is now RedstoneWireBlock, and the repeater and observer tests that RedstoneWireBlock.shouldConnectTo held are gone from it: each block answers BlockBehaviour.shouldRedstoneWireConnectTo for itself.

Where to look

The three questions a block answers are BlockBehaviour.BlockStateBase.isSignalSource, BlockBehaviour.BlockStateBase.getSignal and BlockBehaviour.BlockStateBase.getDirectSignal; SignalGetter.getSignal combines the last two with BlockBehaviour.BlockStateBase.isRedstoneConductor, the strong half through SignalGetter.getDirectSignalTo, and SignalGetter.getBestNeighborSignal over SignalGetter.DIRECTIONS is how a position is read. The trace starts at LeverBlock.pull with LeverBlock.updateNeighbours, enters the wire at RedstoneWireBlock.neighborChanged, and the number is decided by RedstoneWireBlock.getBlockSignal against RedstoneWireEvaluator.getIncomingWireSignal, with DefaultRedstoneWireEvaluator.updatePowerStrength doing the write and the fan-out. For the drawing rules read RedstoneWireBlock.getConnectionState and BlockBehaviour.shouldRedstoneWireConnectTo; for what leaves a wire, RedstoneWireBlock.getSignal. The second implementation is ExperimentalRedstoneWireEvaluator.calculateCurrentChanges and ExperimentalRedstoneWireEvaluator.causeNeighborUpdates. SignalGetter.getControlInputSignal is not named above and is the door into what a diode reads from its sides.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.