Zero press

From DoomWiki.org

The zero press is a technique that can be used to activate switches through walls and at farther distances than is normally possible. It is commonly used in speedrunning to reach normally inaccessible switches in order to skip sections of maps.

Performing it requires precise positioning parallel to the button linedef that is typically achieved by making very slight adjustments while pressing the use key repeatedly. This is helped along by shorttics, which cuts the player's range of possible turning angles down, making it significantly easier to aim parallel to buttons placed at right angles or even some diagonal angles.

The trick is named after Zero-Master, who discovered and shared it in 2017. This led to many speedrunning record improvements, such as on MAP07: Dead Simple which became beatable in three seconds.

Technical explanation[edit]

The bug is caused by imprecision in fixed point math where bits are discarded during multiplication. This leads to a linedef mistakenly being considered to be aimed at when it is not, which allows it to be included in a distance calculation not designed for it. This bad input line is considered to be closest, so it is pressed even though it is not being aimed at.

The intended behavior is that a player performs the use action, and a line segment (hereby referred to as the trace) is made from the center of the player to the farthest point they can press a button at, and then the function PIT_AddLineIntercepts in p_maputl.c investigates every linedef in the blockmap blocks passed through by the trace, collecting the linedefs that intersect the trace, as this means they are being aimed at. The function P_PointOnDivlineSide determines whether a point is on one side of the line or the other. Two calls are made, one for each linedef vertex; if the two vertices are on different sides of the trace, this linedef is intersecting the trace, meaning this linedef is being aimed at.

The weak point in this process that leads to the zero press is the P_PointOnDivlineSide function. An abridged version of it modified for clarity is as follows:

int
P_PointOnDivlineSide
( fixed_t	x,     // The parameters x and y are the coordinates of a linedef vertex.
  fixed_t	y,
  divline_t*	line ) // The parameter line is the trace.
{
    // This code implements the cross product formula.
    // The cross product is positive or negative depending on the ordering of vectors,
    // which is useful for determining which side a given vector is on.
    
    dx = (x - line->x);
    dy = (y - line->y);
    
    left  = FixedMul ( line->dy>>8 , dx>>8 );
    right = FixedMul ( dy>>8 , line->dx>>8 );
    
    if (left - right > 0) return FRONT;
    else return BACK;
}

The function FixedMul makes use of the property of the fixed point number format that simply multiplying them as though they were regular integers gives the correct result, albeit with bits in excess. There can be up to 32 excess bits, for a total of 64 bits, in a multiplication result, as a fixed point number is 32 bits. Downwards bit shifting is used to discard these excess bits, and this is equivalent to floor division towards negative infinity, rounding the fractional part down to some smaller amount of binary places. The function FixedMul discards the 16 lowest bits of the product and leaves to the caller the discarding of an additional 16 total bits from the factors, leading to a 32 bit final result. This precision loss is also responsible for some wiggle room when performing the glitch, as the least significant bits of all coordinates, and therefore those of the player's position, are discarded and are therefore treated identically.

The function's bias toward the back side, returning BACK if the point is on the line (making the cross product 0), provides a simple situation where the wrong side can be returned: Downwards bit shifting of a positive number results in exactly 0 if the number is small enough or the shift is big enough, switching the side of the line that would have been returned if there was no precision loss.

This collection of equations involving only positive quantities, representing fixed point numbers plainly as integers and borrowing some syntax from the C language, is an example of calculations from a real zero press that compares scenarios of no precision loss and real world precision loss to demonstrate how a cross product of 0 can cause a side switch:

//// Determining the side of both vertices with full precision:
// 64 bit results are acceptable and no bitshifting is performed.
// This results in correctly determining that both vertices are on the same line side.
// The symbol _ is a digit separator for ease of reading.

left  = 0x0000_0640 * 0x0010_0000 = 0x0000_0000_6400_0000
right = 0x0000_0100 * 0x003F_FFC0 = 0x0000_0000_3FFF_C000
vertex1side = FRONT // because left - right > 0

left  = 0x0000_0640 * 0x0090_0000 = 0x0000_0003_8400_0000
right = 0x0000_0100 * 0x003F_FFC0 = 0x0000_0000_3FFF_C000
vertex2side = FRONT // because left - right > 0

//// Determining the side of both vertices with precision loss:
// 32 bit results are required and bitshifting was used to discard excess bits.
// 8 bits have been discarded from the factors, and 16 bits have been discarded from the products.
// This results in incorrectly determining that the vertices are on different sides.

left  = FixedMul(0x00_0006, 0x00_1000) = 0x0000_0000
right = FixedMul(0x00_0001, 0x00_3FFF) = 0x0000_0000
vertex1side = BACK  // because left - right <= 0

left  = FixedMul(0x00_0006, 0x00_9000) = 0x0000_0003
right = FixedMul(0x00_0001, 0x00_3FFF) = 0x0000_0000
vertex2side = FRONT // because left - right > 0

Linedefs that have been determined to intersect the trace go on to have their distance to the trace base, the player, calculated. This is done in the function P_InterceptVector, which takes the trace and a linedef and calculates relative distance between them by measuring how close the intersection point between the infinite linedef line and the trace line is to the base of the trace. The relative distance is a real fixed point number in the range [0, 1] when the finite trace line is intersected, with lower values within that range representing intersection points closer to the base of the trace, the player. Values outside this range represent intersections behind the player or past the use range and are discarded.

fixed_t
P_InterceptVector
( divline_t* v2,  // The parameter v2 is the trace.
  divline_t* v1 ) // The parameter v1 is the linedef.
{
    fixed_t frac;
    fixed_t num;
    fixed_t den;
 
    den =
      FixedMul ( v1->dy>>8 , v2->dx )
    - FixedMul ( v1->dx>>8 , v2->dy );
 
    if (den == 0) return 0; // parallel
    
    num =
     FixedMul ( (v1->x - v2->x)>>8 , v1->dy )
    +FixedMul ( (v2->y - v1->y)>>8 , v1->dx );
 
    frac = FixedDiv (num , den);
 
    return frac;
}

Ignoring the special zero denominator case, and the precision loss less important than in the side-finding function from before, this distance-finding function boils down to the following equation:

// T.1 and T.2 are the points of the use trace line (where T.1 is the base)
// L.1 and L.2 are the points of the linedef
// T.1.x is the x coordinate of the point T.1
num = (L.1.x - T.1.x)*(L.2.y - L.1.y) + (T.1.y - L.1.y)*(L.2.x - L.1.x)
den = (L.2.y - L.1.y)*(T.2.x - T.1.x) - (L.2.x - L.1.x)*(T.2.y - T.1.y)
frac = num / den

This equation can be plugged into an interactive graphing tool to gain an intuitive understanding of what the frac value, the relative distance, represents. This does not require an understanding of the equation itself.

Results from this function are sorted, and the shortest button or wall linedef is chosen and pressed on.

Due to this distance-finding function regarding linedefs as infinite in length, it relies on the finite linedefs having been previously correctly determined to intersect the trace, as otherwise either the denominator is zero or the point of intersection is on a nonsensical imaginary extension of the linedef. In a zero press a non-intersecting linedef is erroneously allowed to have its relative distance calculated leading to the latter kind of nonsense result, the following being an exaggerated and not-to-scale illustration of this:

P represents the player, at the base of the trace.
/ represents the trace.
| represents a wall linedef being aimed at.
# represents a button linedef behind the wall.
- represents the imaginary extension of the button linedef.
       |
      /|
     / |
    /  |
   /---|--#########
  /    |
 /     |
P      |

As the imaginary extension of the button linedef intersects closer to the base than any other linedef, it is chosen for the use action. The extended use range property of zero presses is caused by pressing on this imaginary extension, as the relative distance is calculated to be within the allowed [0, 1] range and is not discarded.

External links[edit]