Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Computer Graphics (rasterisation, ray tracing, WebGPU)  Module 1 – Foundations: pixels, displays, colour and the pipeline

How computer graphics makes images

Raster and vector images, pixels as samples, rasterisation and ray tracing, the seven stages of the GPU pipeline, and the time and memory every frame costs.

  • Beginner
  • 20 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Distinguish raster and vector images and explain what a pixel sample represents
  • Compare rasterisation and ray tracing as two answers to the visibility problem
  • Trace the real-time pipeline from vertices to framebuffer and name WebGPU's term for each stage
  • Compute frame-time budgets and framebuffer memory for a resolution, pixel format and refresh rate
What you’ll learn

What you will learn

  • Distinguish raster and vector images and explain what a pixel sample represents
  • Compare rasterisation and ray tracing as two answers to the visibility problem
  • Trace the real-time pipeline from vertices to framebuffer and name WebGPU's term for each stage
  • Compute frame-time budgets and framebuffer memory for a resolution, pixel format and refresh rate
On this page

A 1920 × 1080 screen that refreshes 60 times a second needs 2,073,600 new colours every 16.7 milliseconds, about 124 million a second, and each one has to be worked out from a description of a scene: shapes, lights, a camera. Computer graphics is that work: deciding the colour of every pixel, correctly and in time.

Five ideas carry the rest of this track: what a pixel really holds, the two ways a renderer finds the surface each pixel shows, the seven stages a GPU runs for every draw call, the time and memory one frame costs, and the display and input vocabulary that exams still ask about. The first four come with a program you can run and change.

A picture is a grid of samples, or a list of shapes

There are two ways to keep a picture of a circle. A vector image: An image stored as shapes, such as lines, curves and fills, given by their coordinates. It is turned into pixels when it is drawn, at any size. keeps the shape itself, “circle, centre (0.5, 0.5), radius 0.4”, and can be drawn again at any size; the SVG standard describes graphics this way, as shapes such as paths of straight lines and curves, plus images and text. A raster image: An image stored as a grid of pixels, each with its own colour. Its detail is fixed by its width and height in pixels. keeps a grid of colours instead, one for each position. Screens, cameras and the memory a GPU draws into are all raster, so every vector drawing becomes a grid before anyone sees it.

What does one cell of that grid hold? It is tempting to call a pixel: One sample of a raster image: the colour stored for one position of its grid. A renderer decides it from the scene at a single point, the pixel's centre, not from a little square. a little square of colour, but Alvy Ray Smith’s memo on the subject puts it plainly: a pixel is a point sample, a value taken at one point. Renderers work exactly that way. Without multisampling, WebGPU makes a triangle cover a pixel when the pixel’s centre, the point (x + 0.5, y + 0.5) in framebuffer coordinates, lies inside the triangle. Look at the dashed pixel in the figure: the circle’s edge crosses its square, yet it stays empty, because its centre is outside.

A circle drawn over an 8 by 8 grid of pixels; the 32 pixels whose centre point lies inside the circle are shaded.One circle on an 8 × 8 grid of pixelsEach pixel is decided by the point at its centrecentre inside the circle: pixel set (32 of 64)centre outside: pixel left emptypixel (1, 1): edge crosses it, centre outsidethe vector shape: centre (0.5, 0.5), radius 0.4

The shape is exact; the raster keeps one point per pixel

Text description of the diagram

An 8 by 8 grid of square pixels fills most of the figure, and a circle is drawn over it: its centre is the middle of the grid and its radius is 0.4 of the grid's width, which is the circle that examples/samples/sample_circle.mjs samples.

Every pixel has a dot at its centre. Where that centre point lies inside the circle the dot is filled and the pixel is shaded; this happens for 32 of the 64 pixels, which form a blocky disc two pixels narrower at the top and bottom rows than in the middle. Where the centre lies outside, the dot is hollow and the pixel stays empty.

Pixel (1, 1), second from the left in the second row, has a dashed outline. The circle's edge crosses the corner of its square, so part of the square is inside the circle, but its centre point is outside, so the pixel is left empty: the raster records only what happens at the sample point.

The legend under the grid repeats the four marks: a filled dot for a centre inside (the pixel is set), a hollow dot for a centre outside, the dashed outline of pixel (1, 1), and the solid curve of the shape itself, described exactly by its centre (0.5, 0.5) and radius 0.4.

The figure is the 8 × 8 run of the program below. It describes the circle once and samples it at the centre of every pixel on three grids. The share of samples that land inside is an estimate of the circle’s area, which is exactly π × 0.4² = 0.5027 of the square:

One circle sampled at pixel centres on three grids JavaScript · samples/sample_circle.mjs
// One circle, described once as a shape and then sampled at the centre of every pixel of three grids.
// The shape lives in a unit square: centre (0.5, 0.5), radius 0.4. Change RADIUS or SIZES and run it again.
const CENTRE = { x: 0.5, y: 0.5 };
const RADIUS = 0.4;
const SIZES = [8, 16, 64];

console.log(`vector description: circle centre (${CENTRE.x}, ${CENTRE.y}) radius ${RADIUS}`);
console.log(`exact area: ${(Math.PI * RADIUS * RADIUS).toFixed(4)} of the square\n`);

for (const n of SIZES) {
  let covered = 0;
  const rows = [];
  for (let y = 0; y < n; y++) {
    let row = '';
    for (let x = 0; x < n; x++) {
      // The sample point of pixel (x, y) is its centre, (x + 0.5, y + 0.5) in pixel units.
      const px = (x + 0.5) / n;
      const py = (y + 0.5) / n;
      const inside = (px - CENTRE.x) ** 2 + (py - CENTRE.y) ** 2 <= RADIUS ** 2;
      if (inside) covered++;
      row += inside ? '#' : '.';
    }
    rows.push(row);
  }
  const share = covered / (n * n);
  console.log(`${n} x ${n} grid: ${covered} of ${n * n} samples inside, area estimate ${share.toFixed(4)}`);
  if (n <= 16) console.log(rows.join('\n') + '\n');
}

Output

vector description: circle centre (0.5, 0.5) radius 0.4
exact area: 0.5027 of the square

8 x 8 grid: 32 of 64 samples inside, area estimate 0.5000
........
..####..
.######.
.######.
.######.
.######.
..####..
........

16 x 16 grid: 124 of 256 samples inside, area estimate 0.4844
................
................
.....######.....
...##########...
...##########...
..############..
..############..
..############..
..############..
..############..
..############..
...##########...
...##########...
.....######.....
................
................

64 x 64 grid: 2056 of 4096 samples inside, area estimate 0.5020

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sample_circle.mjs

The estimate does not simply improve as the grid grows: 0.5000 on 8 × 8, then 0.4844 on 16 × 16, then 0.5020 on 64 × 64. Each grid puts its sample points in different places relative to the edge, so each one errs differently. That wobble is aliasing, the price of storing a continuous shape as samples, and the textures module returns to it with the theory of sampling.

Move the edge across the samples

Press Edit, set RADIUS to 0.38 and run it: the exact area falls by about a tenth, from 0.5027 to 0.4536, yet the 8 × 8 grid still counts 32 samples, because no pixel centre lies between the old edge and the new one. At 0.36 a ring of 8 centres falls outside at once, and the count drops to 24.

Checkpoint

Check: Pixels as point samples

  1. Question 1 of 2 A triangle's edge crosses pixel (4, 2) so that most of the pixel's square is inside the triangle, but the point (4.5, 2.5) is just outside it. Without multisampling, does WebGPU produce a fragment for this pixel?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, because coverage is decided at the pixel's centre, which is outside

    A pixel is a point sample. With one sample per pixel, WebGPU tests the point at the pixel's centre, so a pixel whose centre is outside stays empty however much of its square the triangle covers, exactly like the dashed pixel in the figure.

  2. Question 2 of 2 Which version of the circle can be drawn at four times the size with no loss of detail?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The vector description, centre (0.5, 0.5) and radius 0.4

    A vector image keeps the shape and is turned into pixels when it is drawn, at whatever size. A raster image is already samples, so enlarging it can only spread or blend what it has.

Rasterisation and ray tracing answer the same question

Every renderer has to solve the visibility problem: Deciding, for every pixel, which surface of the scene is nearest the camera along that pixel's line of sight, and so shows there.: where several surfaces line up behind one pixel, which one is nearest the camera and therefore shows? There are two classic answers, and comparing them is one of the learning outcomes the CS2023 curriculum lists for graphics courses.

rasterisation: Rendering by looping over the shapes, usually triangles, and finding the pixels each one covers, keeping the nearest surface per pixel. GPUs render this way. loops over the shapes. For each one it finds the pixels it covers and keeps, in a depth buffer: An image the size of the framebuffer that stores, for each pixel, the depth of the nearest surface drawn so far, so that a farther surface drawn later is rejected., the distance of the nearest surface drawn so far at every pixel; a later surface that is farther away is thrown out. ray tracing: Rendering by looping over the pixels and following a ray from the camera through each one to the first surface it hits; more rays from that point give shadows and reflections. loops over the pixels. For each one it follows a ray from the camera through the pixel’s centre, tests it against the shapes, and keeps the hit nearest along the ray. The program draws the same three spheres both ways, with a parallel camera that looks straight along z:

Three spheres, rasterised and ray cast JavaScript · visibility/two_renderers.mjs
// The same three spheres drawn twice into a 32 x 12 grid of characters, seen through a parallel camera that looks
// along +z: once by a rasteriser (loop over the shapes, then their pixels) and once by a ray caster (loop over the
// pixels, then the shapes). Both keep, for every pixel, the nearest surface, so their pictures must be identical.
const W = 32;
const H = 12;
const spheres = [
  { name: 'A', x: 11, y: 6, z: 10, r: 5 },
  { name: 'B', x: 18, y: 6, z: 6, r: 4 },
  { name: 'C', x: 22, y: 5, z: 14, r: 6 },
];

// Depth of the front of sphere s at pixel centre (px, py), or Infinity if the pixel misses it.
function frontDepth(s, px, py) {
  const d2 = (px - s.x) ** 2 + (py - s.y) ** 2;
  return d2 <= s.r * s.r ? s.z - Math.sqrt(s.r * s.r - d2) : Infinity;
}

function rasterise() {
  const color = Array(W * H).fill('.');
  const depth = Array(W * H).fill(Infinity);
  let visited = 0;
  for (const s of spheres) {
    // Only the pixels of the sphere's bounding square are visited.
    for (let y = Math.max(0, Math.floor(s.y - s.r)); y < Math.min(H, Math.ceil(s.y + s.r)); y++) {
      for (let x = Math.max(0, Math.floor(s.x - s.r)); x < Math.min(W, Math.ceil(s.x + s.r)); x++) {
        visited++;
        const z = frontDepth(s, x + 0.5, y + 0.5);
        if (z < depth[y * W + x]) {
          depth[y * W + x] = z; // the depth test: keep the nearer surface
          color[y * W + x] = s.name;
        }
      }
    }
  }
  return { color, work: `${visited} pixel visits` };
}

function rayCast() {
  const color = Array(W * H).fill('.');
  let tests = 0;
  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      let nearest = Infinity;
      for (const s of spheres) {
        tests++;
        const z = frontDepth(s, x + 0.5, y + 0.5); // where the ray through this pixel enters s
        if (z < nearest) {
          nearest = z;
          color[y * W + x] = s.name;
        }
      }
    }
  }
  return { color, work: `${tests} ray-sphere tests` };
}

const a = rasterise();
const b = rayCast();
for (let y = 0; y < H; y++) console.log(a.color.slice(y * W, (y + 1) * W).join(''));
console.log(`\nrasteriser: ${a.work}; ray caster: ${b.work}`);
console.log(`same picture: ${a.color.every((c, i) => c === b.color[i])}`);
for (const s of spheres) console.log(`${s.name} (front at z = ${s.z - s.r}): ${a.color.filter((c) => c === s.name).length} pixels`);

Output

..................CCCCCCCC......
.........AAAA....CCCCCCCCCC.....
.......AAAAAAAA.BBBBCCCCCCC.....
.......AAAAAAAABBBBBBCCCCCCC....
......AAAAAAAABBBBBBBBCCCCCC....
......AAAAAAAABBBBBBBBCCCCCC....
......AAAAAAAABBBBBBBBCCCCCC....
......AAAAAAAABBBBBBBBCCCCC.....
.......AAAAAAAABBBBBBCCCCCC.....
.......AAAAAAAA.BBBBCCCCCC......
.........AAAA.......CCCC........
................................

rasteriser: 296 pixel visits; ray caster: 1152 ray-sphere tests
same picture: true
A (front at z = 5): 72 pixels
B (front at z = 2): 52 pixels
C (front at z = 8): 71 pixels

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node two_renderers.mjs

The two pictures are identical, character for character, because both keep the nearest surface at every pixel centre. B’s front is at z = 2, nearer than A’s at 5 and C’s at 8, so B covers part of each. The work differs: the rasteriser visited 296 pixels, only those inside each sphere’s bounding square, while the ray caster made 1,152 ray-sphere tests, 32 × 12 pixels times 3 spheres, because a ray cannot know in advance which shapes it will meet. Ray tracers fix that with acceleration structures, which the ray-tracing module builds.

Aspect Rasterisation Ray tracing
Outer loop Shapes (object order) Pixels (image order)
Nearest surface kept by A depth buffer per pixel The nearest hit per ray
Cost grows with Pixels each shape covers Pixels × shapes tested per ray
Comes easily Many triangles, fast, on GPU hardware Shadows, mirrors and glass: more rays from each hit

One question, two loop orders

Swap the two loops of a renderer and you move between the two methods: shapes outside and pixels inside is rasterisation, pixels outside and shapes inside is ray tracing. Real systems mix them, rasterising what the camera sees and tracing rays for the effects that need them.

Checkpoint

Check: Rasterisation and ray tracing

  1. Question 1 of 2 A ray caster with no acceleration structure renders a 640 × 480 image of a scene of 10 spheres, one ray through each pixel centre. How many ray-sphere tests does it make?

    Type a number.

    A hint

    The two_renderers program made 32 × 12 × 3 = 1,152 tests.

    Show the answer: Question 1 of 2

    Answer: 3072000 tests

    Every pixel's ray is tested against every sphere: 640 × 480 = 307,200 rays, times 10 spheres, is 3,072,000 tests. A rasteriser would instead visit only the pixels each sphere covers.

  2. Question 2 of 2 A rasteriser draws sphere C and then sphere B. At one pixel C's surface is at depth 9.2 and B's at 2.5. What does the pixel show, and would drawing B first change it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: B, whichever is drawn first

    With a depth buffer, the result for opaque surfaces does not depend on the order of the draws: each pixel ends with the nearest surface, the same picture a ray caster finds by keeping the nearest hit.

A draw call runs seven stages, and two of them are your code

A GPU rasterises in a fixed order of stages called the render pipeline. The WebGPU standard names seven: index resolution, vertex processing, primitive assembly, primitive clipping, rasterization, fragment processing and output merging. Two of them run programs you write, called shader: A program the GPU runs for each item of a draw call: a vertex shader once per vertex, returning its position, and a fragment shader once per fragment, returning its colour.: the vertex shader in vertex processing, which runs once per vertex and returns its position in The four coordinates (x, y, z, w) a vertex shader returns for a vertex. In WebGPU a point is inside the visible volume when −w ≤ x ≤ w, −w ≤ y ≤ w and 0 ≤ z ≤ w., and the fragment shader in fragment processing, which runs once per covered pixel and returns its colour. The other five are fixed-function: settings configure them, but you cannot replace them.

Follow one triangle through all seven. Each step names what one stage receives and what it hands on, from three vertices to the pixels written:

Interactive · stage by stage One triangle through the seven stages of the render pipeline Step through one draw call. Each stage takes what the stage above produced; watch the counts change from 3 vertices to 34 written pixels.
One triangle through the seven stages of the render pipeline, step 8 of 8. Showing Index resolution (3 vertices: 0, 1, 2), Vertex processing (3 clip-space positions), Primitive assembly (1 triangle), Primitive clipping (nothing cut), Rasterization (34 fragments), Fragment processing (34 colours), Output merging (34 pixels written). In focus: Vertex processing, Fragment processing.the next draw callIndex resolution3 vertices: 0, 1, 2Vertex processing3 clip-space positionsPrimitive assembly1 trianglePrimitive clippingnothing cutRasterization34 fragmentsFragment processing34 coloursOutput merging34 pixels writtenthe next draw call

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The draw call. A draw call asks for three vertices. Index resolution reads the index buffer, [0, 1, 2], and lists the vertices to process in that order.
  2. The vertex shader. The vertex shader runs once for each vertex and returns a clip-space position (x, y, z, w). This one passes the positions through, so the first vertex stays at (−0.75, −0.75, 0.5, 1).
  3. Triangles from vertices. The topology is a triangle list, so primitive assembly takes the vertices three at a time. Three vertices make one triangle.
  4. Clipping. A point is inside the clip volume when −w ≤ x ≤ w, −w ≤ y ≤ w and 0 ≤ z ≤ w. All three vertices are inside, so clipping cuts nothing and adds no vertex.
  5. Pixels the triangle covers. Rasterization divides by w and maps to framebuffer coordinates, where y points down, giving (2, 7), (14, 6) and (6, 1). It then tests the centre of every pixel; 34 of the 128 centres are inside, so it produces 34 fragments.
  6. The fragment shader. The fragment shader runs once for each fragment and returns its colour. This one picks the colour of the vertex whose weight at that pixel is largest, so the picture splits into three regions.
  7. Depth test and write. Output merging compares each fragment's depth with the depth buffer, cleared to 1.0. Every depth here is 0.5, which is nearer, so all 34 fragments pass and their colours are written to the framebuffer.
  8. Programmable or fixed. Only vertex processing and fragment processing run code you write, the shaders. The other five stages are fixed-function; the pipeline's settings, such as the topology, the culling mode and the depth test, configure them. The next draw call starts again at the top.

The primitive: A shape the GPU draws as one unit, a point, a line or a triangle, assembled from vertices in primitive assembly. here is one triangle, and each pixel centre it covers becomes a fragment: What rasterisation produces for one pixel a primitive covers: its position, depth and interpolated values, before the fragment shader gives it a colour., the data the fragment shader colours. The program runs the same seven stages on the CPU, with the formulas WebGPU defines: the clip-volume test, the division by w, the mapping to framebuffer coordinates in which y points down, the test at pixel centres and the depth test:

One triangle through the seven stages JavaScript · pipeline/one_triangle.mjs
// One triangle through the stages of WebGPU's render pipeline (spec section 23.2), on a 16 x 8 framebuffer.
// Each vertex carries a clip-space position (x, y, z, w) and a colour letter; try moving a vertex and run it again.
const W = 16;
const H = 8;
const vertexBuffer = [
  { pos: [-0.75, -0.75, 0.5, 1], color: 'r' },
  { pos: [0.75, -0.5, 0.5, 1], color: 'g' },
  { pos: [-0.25, 0.75, 0.5, 1], color: 'b' },
];
const indexBuffer = [0, 1, 2];

// 1 Index resolution and 2 vertex processing (this vertex shader passes positions through unchanged).
const processed = indexBuffer.map((i) => ({ clip: vertexBuffer[i].pos, color: vertexBuffer[i].color }));
// 3 Primitive assembly: every three vertices of a triangle list make one triangle.
const triangle = processed.slice(0, 3);
// 4 Primitive clipping: inside the clip volume means -w <= x, y <= w and 0 <= z <= w.
const inside = triangle.every(({ clip: [x, y, z, w] }) => Math.abs(x) <= w && Math.abs(y) <= w && z >= 0 && z <= w);
// 5 Rasterization: divide by w (NDC), map to framebuffer coordinates (y points down), test pixel centres.
const fb = triangle.map(({ clip: [x, y, z, w] }) => ({ x: 0.5 * (x / w + 1) * W, y: 0.5 * (-y / w + 1) * H, z: z / w }));
const edge = (a, b, p) => (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
const area = edge(fb[0], fb[1], fb[2]);
const fragments = [];
for (let y = 0; y < H; y++) {
  for (let x = 0; x < W; x++) {
    const p = { x: x + 0.5, y: y + 0.5 };
    const w0 = edge(fb[1], fb[2], p) / area;
    const w1 = edge(fb[2], fb[0], p) / area;
    const w2 = edge(fb[0], fb[1], p) / area;
    if (w0 >= 0 && w1 >= 0 && w2 >= 0) fragments.push({ x, y, weights: [w0, w1, w2] });
  }
}
// 6 Fragment processing: this fragment shader picks the colour of the vertex with the largest weight.
const shaded = fragments.map((f) => ({ ...f, color: triangle[f.weights.indexOf(Math.max(...f.weights))].color }));
// 7 Output merging: depth test against a buffer cleared to 1.0, then write the colour.
const color = Array(W * H).fill('.');
const depth = Array(W * H).fill(1.0);
let written = 0;
for (const f of shaded) {
  const z = f.weights[0] * fb[0].z + f.weights[1] * fb[1].z + f.weights[2] * fb[2].z;
  if (z < depth[f.y * W + f.x]) {
    depth[f.y * W + f.x] = z;
    color[f.y * W + f.x] = f.color;
    written++;
  }
}

console.log(`vertices processed: ${processed.length}; triangles assembled: 1; inside the clip volume: ${inside}`);
console.log(`framebuffer positions: ${fb.map((v) => `(${v.x}, ${v.y})`).join(' ')}`);
console.log(`fragments: ${fragments.length} of ${W * H} pixel centres; pixels written: ${written}`);
for (let y = 0; y < H; y++) console.log(color.slice(y * W, (y + 1) * W).join(''));

Output

vertices processed: 3; triangles assembled: 1; inside the clip volume: true
framebuffer positions: (2, 7) (14, 6) (6, 1)
fragments: 34 of 128 pixel centres; pixels written: 34
................
......b.........
.....bbb........
....bbbbbb......
....rrrbgggg....
...rrrrrggggg...
..rrrrrr........
................

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node one_triangle.mjs

The run reports 3 vertices, 1 triangle, 34 fragments and 34 pixels written, the same counts the stepped figure shows. Its picture is printed with y going down, as the framebuffer stores it: the vertex at y = 0.75, near the top of clip space, lands on row 1.

Clip space is not framebuffer space

A position of (−0.25, 0.75) is not a pixel address. Clip space runs from −1 to 1 with y pointing up; the framebuffer counts pixels from the top-left corner with y pointing down. Rasterization converts one into the other, here into (6, 1) on a 16 × 8 framebuffer. A shader that writes (6, 1) itself into its position draws nothing at that pixel: x = 6 lies far outside the clip volume, so clipping cuts that corner off the triangle.

Checkpoint

Check: The seven pipeline stages

  1. Question 1 of 2 Which stages of WebGPU's render pipeline run code that you write? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • Fragment processing
    • Vertex processing

    Two of the seven stages are programmable, the vertex and fragment stages, which run the shaders. Index resolution, primitive assembly, primitive clipping, rasterization and output merging are fixed-function.

  2. Question 2 of 2 On the 16 × 8 framebuffer the triangle of one_triangle.mjs produced 34 fragments. With the same clip-space vertices on a 32 × 16 framebuffer, how many fragments does it produce?

    Type a number.

    A hint

    Clip space does not change, so the triangle covers the same share of a framebuffer with four times as many pixels.

    Show the answer: Question 2 of 2

    Answer: 136 fragments (anything from 132 to 140 counts)

    Doubling both sides gives four times as many pixel centres under the same share of the screen: the program produces 136 fragments at 32 × 16, exactly 4 × 34. The work of the fragment stage grows with the pixels covered, not with the number of vertices.

Every frame has a deadline and a memory size

A browser updates a page’s rendering when the display can show a new image. The HTML standard’s own example is a browser aiming at 60 Hz, which gets a rendering opportunity at most every 60th of a second, about 16.7 milliseconds. Everything a frame needs, reading input, moving things, drawing, has to fit inside that interval, the refresh rate: How many times a second a display shows a new image, in hertz. It sets the frame budget: 1000 ÷ 60 ≈ 16.7 ms at 60 Hz.’s budget: 1000 ÷ 60 = 16.7 ms, and only 8.3 ms at 120 Hz.

Memory follows from three numbers. A framebuffer: The memory a renderer draws into: one or more images, such as colour and depth, holding a value for every pixel.’s colour image holds width × height pixels of a fixed size: 4 bytes in WebGPU’s rgba8unorm format (8 bits for each of red, green, blue and alpha) and 8 bytes in rgba16float (16-bit floating point per channel, used for high dynamic range). The program prints the budgets, the size of one colour buffer at five resolutions, and the bytes written each second if every frame writes every pixel once:

Frame budgets, buffer sizes and bytes per second JavaScript · budget/pixel_budget.mjs
// Frame budgets and framebuffer sizes. A colour buffer holds width x height pixels of bytesPerPixel each;
// writing it once per frame at a refresh rate of hz moves that many bytes hz times a second.
const RESOLUTIONS = [[640, 360], [1280, 720], [1920, 1080], [2560, 1440], [3840, 2160]];
const FORMATS = [['rgba8unorm', 4], ['rgba16float', 8]];
const RATES = [30, 60, 120];

/** 1234567 -> "1,234,567" (written out, because the browser runner has no Intl). */
const group = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');

console.log('frame budget: ' + RATES.map((hz) => `${hz} Hz = ${(1000 / hz).toFixed(1)} ms`).join(', '));
console.log('');
console.log(`${'size'.padEnd(10)} ${'format'.padEnd(11)} ${'bytes'.padStart(10)}  ${'MiB'.padStart(6)}  GB/s at 30, 60, 120 Hz`);
for (const [w, h] of RESOLUTIONS) {
  for (const [format, bpp] of FORMATS) {
    const bytes = w * h * bpp;
    const mib = (bytes / 2 ** 20).toFixed(2);
    const rates = RATES.map((hz) => ((bytes * hz) / 1e9).toFixed(2)).join('  ');
    console.log(`${`${w}x${h}`.padEnd(10)} ${format.padEnd(11)} ${group(bytes).padStart(10)}  ${mib.padStart(6)}  ${rates}`);
  }
}
const fullHd = 1920 * 1080 * 4;
console.log(`\ndouble-buffered 1920x1080 rgba8unorm: 2 x ${group(fullHd)} = ${group(2 * fullHd)} bytes`);
console.log(`10 s of 1920x1080 rgba8unorm frames at 30 Hz, uncompressed: ${group(10 * 30 * fullHd)} bytes`);

Output

frame budget: 30 Hz = 33.3 ms, 60 Hz = 16.7 ms, 120 Hz = 8.3 ms

size       format           bytes     MiB  GB/s at 30, 60, 120 Hz
640x360    rgba8unorm     921,600    0.88  0.03  0.06  0.11
640x360    rgba16float  1,843,200    1.76  0.06  0.11  0.22
1280x720   rgba8unorm   3,686,400    3.52  0.11  0.22  0.44
1280x720   rgba16float  7,372,800    7.03  0.22  0.44  0.88
1920x1080  rgba8unorm   8,294,400    7.91  0.25  0.50  1.00
1920x1080  rgba16float 16,588,800   15.82  0.50  1.00  1.99
2560x1440  rgba8unorm  14,745,600   14.06  0.44  0.88  1.77
2560x1440  rgba16float 29,491,200   28.13  0.88  1.77  3.54
3840x2160  rgba8unorm  33,177,600   31.64  1.00  1.99  3.98
3840x2160  rgba16float 66,355,200   63.28  1.99  3.98  7.96

double-buffered 1920x1080 rgba8unorm: 2 x 8,294,400 = 16,588,800 bytes
10 s of 1920x1080 rgba8unorm frames at 30 Hz, uncompressed: 2,488,320,000 bytes

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node pixel_budget.mjs

A 1920 × 1080 rgba8unorm buffer is 8,294,400 bytes, 7.91 MiB, and two of them for double buffering are 16,588,800. At 3840 × 2160 in rgba16float one buffer is 66,355,200 bytes, and writing it once a frame at 120 Hz moves 7.96 GB a second, before any depth buffer, texture or overdraw is counted. The same arithmetic sizes video: 10 seconds of uncompressed 1920 × 1080 frames at 30 Hz is 2,488,320,000 bytes, which is why video is always compressed.

Key idea

A frame must be finished within 1000 ÷ refresh rate milliseconds, and each image it writes costs width × height × bytes per pixel; work out both before you choose a resolution or a format.

What Is My Screen Resolution? See your own screen's resolution, scaling and refresh rate, then work out its frame budget and the size of one rgba8unorm buffer.

Checkpoint

Check: Frame budgets and buffer sizes

  1. Question 1 of 2 What is the frame budget of a display that refreshes 144 times a second, in milliseconds?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 6.944 ms (anything from 6.934 to 6.954 counts)

    1000 ÷ 144 = 6.94 ms. Input, simulation and drawing all have to fit inside it, less than half the 16.7 ms of a 60 Hz display.

  2. Question 2 of 2 How many bytes does one 3840 × 2160 colour buffer in the rgba8unorm format take?

    Type a number.

    A hint

    rgba8unorm stores 4 bytes for each pixel.

    Show the answer: Question 2 of 2

    Answer: 33177600 bytes

    3840 × 2160 = 8,294,400 pixels, times 4 bytes, is 33,177,600 bytes, the 31.64 MiB in the program's table. Double buffering needs twice that.

Raster displays redraw a grid, vector displays redraw lines

Displays have drawn pictures in two ways. A raster-scan display shows a grid: every refresh it reads the whole framebuffer, row by row from the top, so a refresh takes the same time whether the picture is a single dot or a crowded scene. Between two refreshes comes a short pause, the vertical blanking interval, which the next lesson uses to swap images without tearing. The flat panels of today’s phones and monitors are raster displays.

A random-scan display, also called a vector display, keeps a list of points and lines instead and steers its beam straight along each one, in any order, with no grid at all. Ivan Sutherland’s Sketchpad thesis describes one: the TX-2’s display could plot up to about 100,000 spots a second, from a table of spot positions in memory, the display file. Its refresh time therefore grew with the picture: even at that top rate a drawing of 4,000 spots could be redrawn only 25 times a second, and Sketchpad, which gave each spot 20 microseconds to leave time for computing, managed half that. The thesis fought the flicker of large drawings by interlacing the spots or by showing them in random order.

Aspect Raster scan Random scan (vector)
Draws from A framebuffer of pixels A display list of points and lines
Time per refresh Fixed by the resolution Grows with the number of lines
Draws well Filled areas, images, shading Sharp lines with no stair steps
Struggles with Stair steps on edges (aliasing) Filled areas; flicker on big drawings

Input devices close the loop. Sketchpad’s user pointed at the screen with a light pen; today’s programs read a keyboard, a mouse, a pen tablet, a touch screen or a game controller. A browser reports mouse, pen and touch input as pointer events and lets a program poll game controllers once a frame, which the next lesson turns into code.

UGC NET Computer Science, Unit 3

The UGC NET syllabus for Computer Science and Applications opens its computer graphics topics with video-display devices, raster-scan and random-scan systems, graphics monitors and input devices. The table above is that vocabulary, and two of its rows carry most of it: which system needs a framebuffer (raster scan) and whose refresh time depends on the picture (random scan).

What a renderer draws also reaches real people. The WCAG 2.2 accessibility standard forbids content that flashes more than three times in any one second, unless the flashes stay below its flash thresholds, because flashing can trigger seizures, and it forbids colour as the only way to convey information. CS2023 adds the questions every graphics developer meets: ownership of images and models, deepfakes, facial recognition and privacy.

Checkpoint

Check: Displays and devices

  1. Question 1 of 2 A random-scan display plots up to 100,000 spots a second, like the TX-2's display in the Sketchpad thesis. How many times a second can it redraw a picture of 5,000 spots?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 20 times a second

    100,000 ÷ 5,000 = 20 redraws a second. A bigger picture means fewer redraws and more flicker, because a random-scan display's refresh time grows with what it draws.

  2. Question 2 of 2 Which kind of display reads a framebuffer row by row on every refresh, however simple the picture is?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A raster-scan display

    Raster scan works from a framebuffer and redraws every pixel each refresh. Random scan works from a display list, so its refresh time depends on how much it has to draw.

Practise: budgets, buffer sizes and the order of the stages

Put the numbers of this lesson into code. The sample tests cover five display configurations, from a small 640 × 360 window to a 4K display with two rgba16float buffers, and the order of the seven stages:

Exercise · Easy · JavaScript

Frame budgets, framebuffer sizes and the order of the stages

Write three things in budget.mjs:

  • frameBudgetMs(hz): the milliseconds one frame may take on a display that refreshes hz times a second. A rate of 0 or less has no budget: throw a RangeError.
  • framebufferBytes(width, height, bytesPerPixel, buffers): the bytes of buffers colour buffers of width × height pixels, each pixel bytesPerPixel bytes (4 for rgba8unorm, 8 for rgba16float). When buffers is left out, count one buffer; fewer than one buffer is a RangeError.
  • PIPELINE: the seven stages of WebGPU's render pipeline in the order a draw call runs them. The starter lists the right names in the wrong order; reorder them without changing any name.

The sample tests check five display configurations, from 640 × 360 at 30 Hz to 3840 × 2160 at 120 Hz with two rgba16float buffers, the two errors, and the order of the stages.

Starter code · budget.mjs

/** Milliseconds one frame may take at a refresh rate of hz frames a second (a RangeError when hz <= 0). */
export function frameBudgetMs(hz) {
  return 0; // replace with the budget
}

/**
 * Bytes of `buffers` colour buffers of width x height pixels, bytesPerPixel bytes each: one buffer if left out, and a
 * RangeError for fewer than one.
 */
export function framebufferBytes(width, height, bytesPerPixel, buffers) {
  return 0; // replace with the size
}

/** The seven stages of WebGPU's render pipeline, in the order a draw call runs them. Reorder these names. */
export const PIPELINE = [
  'rasterization',
  'output merging',
  'vertex processing',
  'primitive clipping',
  'index resolution',
  'fragment processing',
  'primitive assembly',
];
The sample tests · budget.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { frameBudgetMs, framebufferBytes, PIPELINE } from './budget.mjs';

const close = (got, want, what) => assert.ok(Math.abs(got - want) < 1e-9, `${what}: expected ${want}, got ${got}`);

test('frame budgets at 30, 60, 90, 120 and 144 Hz', () => {
  close(frameBudgetMs(30), 1000 / 30, '30 Hz');
  close(frameBudgetMs(60), 16.666666666666668, '60 Hz');
  close(frameBudgetMs(90), 11.11111111111111, '90 Hz');
  close(frameBudgetMs(120), 8.333333333333334, '120 Hz');
  close(frameBudgetMs(144), 6.944444444444445, '144 Hz');
});

test('five display configurations, one or more buffers', () => {
  const cases = [
    [640, 360, 4, 1, 921600],
    [1280, 720, 4, 2, 7372800],
    [1920, 1080, 4, 1, 8294400],
    [2560, 1440, 8, 3, 88473600],
    [3840, 2160, 8, 2, 132710400],
  ];
  for (const [w, h, bpp, n, want] of cases) {
    assert.equal(framebufferBytes(w, h, bpp, n), want, `${n} buffer(s) of ${w} x ${h} at ${bpp} bytes a pixel`);
  }
});

test('no budget without a positive refresh rate, and at least one buffer', () => {
  assert.throws(() => frameBudgetMs(0), RangeError, 'a rate of 0 Hz');
  assert.throws(() => frameBudgetMs(-60), RangeError, 'a negative rate');
  assert.throws(() => framebufferBytes(1920, 1080, 4, 0), RangeError, 'zero buffers');
  close(frameBudgetMs(1), 1000, '1 Hz');
});

test('one buffer when buffers is left out', () => {
  assert.equal(framebufferBytes(1920, 1080, 4), 8294400, 'one 1920 x 1080 rgba8unorm buffer');
  assert.equal(framebufferBytes(3, 2, 8), 48, 'a 3 x 2 rgba16float buffer');
});

test('the seven stages in the order a draw call runs them', () => {
  assert.deepEqual(PIPELINE, [
    'index resolution',
    'vertex processing',
    'primitive assembly',
    'primitive clipping',
    'rasterization',
    'fragment processing',
    'output merging',
  ]);
});
A hint

A second has 1,000 milliseconds, and a display that refreshes hz times a second shares them equally between its frames. A buffer holds one value of bytesPerPixel bytes for each of its width * height pixels, and every extra buffer is another copy of the same size. For the order, follow one triangle: its vertices have to be chosen and processed before they can form a triangle, and a pixel can only be coloured once the triangle has become fragments.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Pixel Art Maker Draw a raster image by hand, one pixel at a time, then zoom out to see the samples become a picture.

Then check every idea of the lesson in one session. Questions about the ideas you have answered least well come first, and each wrong choice says what it misses:

Practice that adapts to you

Practise this lesson

13 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 What does one pixel of a rendered raster image hold?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: One colour, decided from the scene at a single point

    Alvy Ray Smith's memo and WebGPU's rasterization rule agree: a pixel holds a value taken at one point, so a raster image is a grid of point samples.

  2. Question 2 of 13 Sampling the circle gave area estimates of 0.5000 on 8 × 8, 0.4844 on 16 × 16 and 0.5020 on 64 × 64, against the exact 0.5027. Why did doubling the resolution from 8 to 16 make the estimate worse?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: The new grid put its sample points in different places relative to the edge, and this time more of them fell just outside

    A raster records only the samples. Each grid places them differently around the curved edge, so each grid errs by a different amount; the error shrinks with finer grids on average, but not monotonically.

  3. Question 3 of 13 Which renderer has the pixels in its outer loop?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: A ray tracer

    Swap the loops of one renderer and you get the other: shapes outside is rasterisation, pixels outside is ray tracing. Both keep the nearest surface at every pixel.

  4. Question 4 of 13 A scene has a mirror that must show objects behind the camera. Why is that natural for a ray tracer and awkward for a plain rasteriser?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: A ray tracer just sends a new ray from the point on the mirror; a rasteriser only finds what each shape covers from the camera

    Ray tracing answers "what does this ray hit?" from any point in any direction, so reflections, refraction and shadows are more rays. Rasterisation answers "which pixels does this shape cover as seen from the camera?", so effects that need other viewpoints need other passes.

  5. Question 5 of 13 Put the stages of WebGPU's render pipeline in the order a draw call runs them.

    Give each item its position, from 1 (first).

    Show the answer: Question 5 of 13

    Answer:

    1. Index resolution
    2. Vertex processing
    3. Primitive assembly
    4. Primitive clipping
    5. Rasterization
    6. Fragment processing
    7. Output merging

    Vertices are chosen and shaded, assembled into primitives and clipped; the primitives are rasterized into fragments, the fragments are shaded, and output merging tests and writes them.

  6. Question 6 of 13 A vertex shader returns the clip-space position (0.5, 1.5, 0.2, 1.0). Is this vertex inside WebGPU's clip volume?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: No, because y = 1.5 is greater than w = 1.0

    WebGPU keeps the part of a primitive with −w ≤ x ≤ w, −w ≤ y ≤ w and 0 ≤ z ≤ w. With w = 1.0, y = 1.5 is above the top of the volume, so clipping removes that part of the triangle.

  7. Question 7 of 13 On a 16 × 8 framebuffer, a vertex has the normalised device coordinate y = −0.5. What is its framebuffer y coordinate? (WebGPU uses y = 0.5 × (−ndc.y + 1) × height.)

    Type a number.

    Show the answer: Question 7 of 13

    Answer: 6

    0.5 × (0.5 + 1) × 8 = 6. NDC y points up and framebuffer y points down, so a vertex below the middle of clip space lands below the middle row, near the bottom of the 8 rows.

  8. Question 8 of 13 How many milliseconds may one frame take on a display that refreshes hz times a second?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: 1000 ÷ hz

    The budget is 1000 ÷ hz milliseconds: 33.3 ms at 30 Hz, 16.7 ms at 60 Hz and 8.3 ms at 120 Hz.

  9. Question 9 of 13 How many bytes do two 2560 × 1440 rgba8unorm colour buffers take, for double buffering?

    Type a number.

    Show the answer: Question 9 of 13

    Answer: 29491200 bytes

    2560 × 1440 × 4 = 14,745,600 bytes for one buffer, as in the program's table, so two take 29,491,200 bytes.

  10. Question 10 of 13 A 4K game at 120 Hz switches its colour target from rgba8unorm to rgba16float. What happens to the bytes one full-screen write moves each second, and why can that matter more than the extra memory?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: They double, from 3.98 to 7.96 GB a second, and every pass over the screen spends that bandwidth inside an 8.3 ms frame

    Bytes per second are width × height × bytes per pixel × refresh rate. Doubling the bytes per pixel doubles the traffic of every full-screen pass, 7.96 GB a second at 4K and 120 Hz for a single write.

  11. Question 11 of 13 What does a random-scan (vector) display draw better than a raster-scan display?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: Sharp lines with no stair steps, because the beam moves along each line itself

    A random-scan display traces each line directly, so lines are smooth, but its refresh time grows with the number of lines, so large drawings flicker and filled areas are impractical.

  12. Question 12 of 13 Sketchpad's display could plot about 100,000 spots a second. Why did a large drawing flicker on it, when a raster display shows a large drawing as steadily as a small one?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: Each redraw had to plot every spot, so more spots meant fewer redraws a second; a raster display's redraw always takes the same time

    A random-scan display's refresh time is proportional to what it draws. Sketchpad's thesis describes interlacing the spots and showing them in random order to reduce the flicker of large drawings.

  13. Question 13 of 13 An effect alternates full-screen black and white frames ten times a second. What does WCAG 2.2 say about it?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: It fails Three Flashes or Below Threshold, because it flashes more than three times in one second

    WCAG 2.2 success criterion 2.3.1 forbids content that flashes more than three times in any one second unless the flashes are below the general and red flash thresholds. A renderer's effects are content too.

Interview questions

Warm-up (fresher to 2-5 years): How do rasterisation and ray tracing differ, and which one does a GPU’s render pipeline use?

Both solve the same problem: for every pixel, find the surface nearest the camera. Rasterisation loops over the primitives, usually triangles. For each one it finds the pixel centres it covers and keeps the nearest surface per pixel in a depth buffer. Ray tracing loops over the pixels. For each one it shoots a ray through the pixel centre and keeps the nearest intersection along it. The render pipelines of WebGPU and OpenGL rasterise: their stages assemble triangles, rasterise them into fragments and depth-test them. Rasterisation’s cost grows with the pixels each triangle covers; a ray tracer’s grows with pixels times the shapes each ray must test, which is why it needs an acceleration structure such as a BVH. Ray tracing’s strength is that shadows, reflections and refraction are just more rays from the hit point.

Two harder questions grow out of this lesson, each with its first step and follow-ups on the module’s question page:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A game renders at 3840 × 2160 and 120 Hz into an rgba16float colour target with double buffering. Estimate the memory and the bytes per second of its colour output alone, and say what you would change first if frames miss their budget.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A product viewer must show a car with accurate reflections and soft shadows at 60 Hz on mid-range laptops. Which parts of the frame would you rasterise, which would you ray trace, and how would you decide?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A raster image is a grid of point samples, each decided at the pixel’s centre, while a vector image keeps the shapes and can be sampled again at any size.
  • Rasterisation and ray tracing both keep the nearest surface for every pixel; one loops over shapes with a depth buffer, the other over pixels with a ray each.
  • A draw call runs seven stages, from index resolution to output merging; the vertex and fragment stages run your shaders and the other five are fixed-function.
  • A frame has 1000 ÷ refresh rate milliseconds, and one colour buffer costs width × height × bytes per pixel, 8,294,400 bytes for 1920 × 1080 in rgba8unorm.
  • Raster-scan displays redraw the whole framebuffer every refresh; random-scan displays draw a list of lines, so their refresh time grows with the picture.

Terms in this lesson

depth buffer
An image the size of the framebuffer that stores, for each pixel, the depth of the nearest surface drawn so far, so that a farther surface drawn later is rejected.
fragment
What rasterisation produces for one pixel a primitive covers: its position, depth and interpolated values, before the fragment shader gives it a colour.
framebuffer
The memory a renderer draws into: one or more images, such as colour and depth, holding a value for every pixel.
pixel
One sample of a raster image: the colour stored for one position of its grid. A renderer decides it from the scene at a single point, the pixel's centre, not from a little square.
primitive
A shape the GPU draws as one unit, a point, a line or a triangle, assembled from vertices in primitive assembly.
raster image
An image stored as a grid of pixels, each with its own colour. Its detail is fixed by its width and height in pixels.
rasterisation
Rendering by looping over the shapes, usually triangles, and finding the pixels each one covers, keeping the nearest surface per pixel. GPUs render this way.
ray tracing
Rendering by looping over the pixels and following a ray from the camera through each one to the first surface it hits; more rays from that point give shadows and reflections.
refresh rate
How many times a second a display shows a new image, in hertz. It sets the frame budget: 1000 ÷ 60 ≈ 16.7 ms at 60 Hz.
shader
A program the GPU runs for each item of a draw call: a vertex shader once per vertex, returning its position, and a fragment shader once per fragment, returning its colour.
vector image
An image stored as shapes, such as lines, curves and fills, given by their coordinates. It is turned into pixels when it is drawn, at any size.
visibility problem
Deciding, for every pixel, which surface of the scene is nearest the camera along that pixel's line of sight, and so shows there.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.