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
| class | what it decides | thread |
|---|---|---|
SignalGetter | every question about power: what a position emits, what reaches it, and the direction order the answers are gathered in | a Level interface, either side |
BlockBehaviour.BlockStateBase | the 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’s | either side |
LeverBlock | the trace’s source: 15 in every direction, and 15 strongly into one block only | server — the client’s copy writes nothing |
RedstoneWireBlock | which sides a wire connects to, what it emits through them, and the mutable flag that stops it counting itself | server for power; its connections on both sides |
RedstoneWireEvaluator | minus one per block: what the neighbouring wires are worth to this one | server |
DefaultRedstoneWireEvaluator | one wire at a time, recursively, with the fan-out issued by hand | server |
ExperimentalRedstoneWireEvaluator | every wire the change reaches, at once, in two phases, everything off before anything on, allocated fresh per call | server |
Orientation | in the experimental mode only, where an update came from, so the fan-out can be ordered relative to it | server |
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.
| array | order | what it governs |
|---|---|---|
SignalGetter.DIRECTIONS | down, up, north, south, west, east | what 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_ORDER | west, east, down, up, north, south | which neighbour is told first about a change, on the neighbour channel |
BlockBehaviour.UPDATE_SHAPE_ORDER | west, east, north, south, down, up | which 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.neighborChangednow takes a nullableOrientationrather than a sourceBlockPos— which is what lets the experimental evaluator order a fan-out relative to where the update came from — andBlockBehaviour.affectNeighborsAfterRemoval, which replaced the old removal hook, does not take one at all. RedStoneWireBlock is nowRedstoneWireBlock, and the repeater and observer tests thatRedstoneWireBlock.shouldConnectToheld are gone from it: each block answersBlockBehaviour.shouldRedstoneWireConnectTofor 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.