diff --git a/CHANGELOG.md b/CHANGELOG.md index c416b90b..9130fde5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,9 +39,13 @@ because it turns other people's test suites red. are - `width`, `height` and `quality`. The picture is the gradient with the self describing label the image formats draw, with the clock under the label and the square below. On a picture too small for the clock the film - says so in the manifest. It can be up to 4096 pixels wide and at most - 4096x2304 pixels in all, the largest the built-in encoder writes - correctly - a larger one is refused with a pair that fits. The manifest says what a + says so in the manifest. Either side can be up to 16384 pixels and the + picture up to 8192x4352 pixels in all, the largest any AV1 level + describes, so 7680x4320 and 8192x4320 films can be made - a larger + picture is refused before anything is written. A picture taller than 8704 + pixels belongs to no AV1 level, which a player may hold against it. An 8K + film takes a few seconds and about 250 MB of memory, sixteen of them made + at once about 1.1 GB. The manifest says what a test can check: `duration_ms`, `frame_count`, `frame_rate`, `keyframe_count`, `change_count`, `change_interval_ms`, `width`, `height`, `compression: av1` and `audio: false`. A length that does not end on a diff --git a/internal/format/video/ahead.go b/internal/format/video/ahead.go index bbc477c8..c9f5e65f 100644 --- a/internal/format/video/ahead.go +++ b/internal/format/video/ahead.go @@ -124,7 +124,7 @@ type crew struct { // newCrew is the crew for a film of at most this many tiles to code. A film of // one tile takes no helper, and neither does a process of one thread. func newCrew(f *film, ks keyer, qindex int, jobs int64) *crew { - c := &crew{film: f, keys: ks, qindex: qindex, own: f.painter()} + c := &crew{film: f, keys: ks, qindex: qindex, own: f.painter(ks)} if most := min(int64(runtime.GOMAXPROCS(0)-1), jobs-1); most > 0 { c.most = int(most) c.queue = make(chan *job, c.ahead()*len(ks.tiles)) @@ -169,7 +169,7 @@ func (c *crew) offer(j *job) { func (c *crew) help() { defer c.wg.Done() defer helping.Add(-1) - p := c.film.painter() + p := c.film.painter(c.keys) for j := range c.queue { if c.stopped.Load() { continue @@ -192,8 +192,8 @@ func (c *crew) code(p *painter, j *job, beside bool) { j.coded, j.err = c.encode(j.change, beside) return } - r := c.keys.tiles[j.tile].rect - j.coded, j.err = encodeTile(p.drawIn(j.look, r), r, c.qindex) + src, at := p.source(j.look, c.keys.tiles[j.tile].rect) + j.coded, j.err = encodeTile(src, at, c.qindex) } // wait is the tile of j, coded here when no helper has taken it. diff --git a/internal/format/video/choose.go b/internal/format/video/choose.go index 583579af..8dad90b8 100644 --- a/internal/format/video/choose.go +++ b/internal/format/video/choose.go @@ -240,9 +240,6 @@ func named(formatID string, r format.Request, s Settings, c Choice) (Choice, Str if err := checkJointLimits(formatID, w, h); err != nil { return Choice{}, Stream{}, err } - if err := checkOneTile(formatID, w, h); err != nil { - return Choice{}, Stream{}, err - } tiles, reserve, err := sampleReserve(w, h, c.Seed, c.Label, s.Timeline, c.QIndex) if err != nil { return Choice{}, Stream{}, err @@ -270,28 +267,6 @@ func checkJointLimits(formatID string, w, h int) error { return nil } -// checkOneTile refuses a picture that would need a second AV1 tile, before -// the encoder makes a frame nobody can decode out of it. -// -// The declared limit counts pixels and this counts blocks of 64 by 64, each -// side rounded up, which is what the encoder goes by - 4000x2359 is under the -// pixels and one block too many. Said as a setting the request can change, -// not as a fault of the program. -func checkOneTile(formatID string, w, h int) error { - blocks := ((w + 63) / 64) * ((h + 63) / 64) - if blocks <= tileBlocks { - return nil - } - return &format.PropertyValueError{ - Format: formatID, Key: imagedim.SettingWidth + " and " + imagedim.SettingHeight, - Subject: core.Says("format.TwoSettings", "%s and %s", core.A("Of", core.LabelTerm(imagedim.SettingWidth)), core.A("By", core.LabelTerm(imagedim.SettingHeight))), - Value: fmt.Sprintf("%dx%d", w, h), - Reason: core.Says("video.MoreThanOneTile", - "a %dx%d picture is %d blocks of 64 by 64 pixels once its sides are rounded up to whole blocks, and the encoder codes one AV1 tile, which holds %d. Ask for a smaller pair, such as 4096x2304 or 3840x2160", - core.A("Width", w), core.A("Height", h), core.A("Blocks", blocks), core.A("Most", tileBlocks)), - } -} - // Facts is what a film's plan tells the manifest. The keys an image and a // sound already use mean the same here - width, height, frame_count, // duration_ms, compression - because they are public names a test asserts diff --git a/internal/format/video/grid.go b/internal/format/video/grid.go index 0dc0e793..f859f017 100644 --- a/internal/format/video/grid.go +++ b/internal/format/video/grid.go @@ -22,7 +22,8 @@ import ( // The layout is part of the bytes of every film (D11), so it is a rule rather // than a search, measured against two others (section 9.1): one tile row under // the clock's band, two around the square's, each superblock the clock's -// characters reach a column of its own and the rest of the width one column. +// characters reach a column of its own and the rest of the width one column - +// cut further only where a frame cannot carry it (cutToFit). // The clock's tile is coded at every change, so it is small. The square has // ten places, so its tiles are coded ten times a film at most, and only have // to be low. The rest is coded once. More columns cost bytes - every seam @@ -50,16 +51,17 @@ func oneTile(width, height int) grid { // than one below it (annex.a.levels.md lines 99-114, MaxTiles and MaxTileCols // rise with the level), so a layout that fits the lowest fits every level the // film can end up declaring. Over the limits, the square's rows go first, then -// the clock's columns are joined, then the picture is one tile. +// the clock's columns are joined, then the picture is the fewest tiles it can +// be - one, for any picture of 4096 by 2304 or less. // -// A picture is one tile when its clock's tiles would be more than a quarter of -// it - coding them at every change would save less than three quarters of the -// time a whole picture takes, and every seam costs bytes. That is every rung -// of the ladder from 160x90 down. +// A picture is that fewest when its clock's tiles would be more than a quarter +// of it - coding them at every change would save less than three quarters of +// the time a whole picture takes, and every seam costs bytes. That is every +// rung of the ladder from 160x90 down. func gridFor(g geometry, fps int) grid { maxTiles, maxCols := tileLimits(chooseLevel(g.width, g.height, fps, 0, 1)) for _, try := range [...]struct{ square, apart bool }{{true, true}, {false, true}, {false, false}} { - out := grid{width: g.width, height: g.height, rows: tileRows(g, try.square), cols: tileCols(g, try.apart)} + out := cutToFit(grid{width: g.width, height: g.height, rows: tileRows(g, try.square), cols: tileCols(g, try.apart)}) if out.tiles() > maxTiles || len(out.cols) > maxCols { continue } @@ -68,7 +70,56 @@ func gridFor(g geometry, fps int) grid { } return out } - return oneTile(g.width, g.height) + return cutToFit(oneTile(g.width, g.height)) +} + +// Layout is the columns and rows, in superblocks, a film's picture is cut +// into - for the guard that holds every layout to what tile_info and Annex A +// allow, from the specification rather than from this package. +func Layout(width, height int, label string, t Timeline) (cols, rows []int) { + g := gridFor(geometryOf(width, height, label, t), t.FPS) + return g.cols, g.rows +} + +// cutToFit cuts a layout further where a frame cannot carry it: a column wider +// than MAX_TILE_WIDTH, and a row taller than tile_info lets a tile be once the +// frame is too large for one (tileBounds.maxArea). Each is cut into the fewest +// equal parts, the larger ones last. A picture of 4096 by 2304 or less needs +// neither, so its layout is the one it had before pictures this large were +// allowed - every such size in a sweep of 10.4 million layouts on 2026-10-07 +// (docs/WEBM-LIMIT-2026-10-07.md section 2), and the golden film of tiles. +// +// A column is also no wider than half that area when the frame has more than +// one row of superblocks, so that a row of tiles can be two superblocks tall. +// Without it, a picture wider than 4096 and only a little over 64 tall came out +// with a last row of one superblock holding a pixel or two, and Annex A asks +// every tile to be at least 8 pixels tall (CroppedTileHeight, annex.a.levels.md +// lines 265-267) - 81 900 layouts of that sweep did. The larger parts go last +// for the same reason: the last part is then at least two superblocks. +func cutToFit(g grid) grid { + b := boundsOf(g.width, g.height) + area := b.maxArea() + widest := maxTileWidth / superblock + if b.sbRows > 1 { + widest = min(widest, max(area/2, 1)) + } + g.cols = cutEach(g.cols, widest) + g.rows = cutEach(g.rows, max(area/slices.Max(g.cols), 1)) + return g +} + +// cutEach cuts every size over most into the fewest parts no larger than it, +// as equal as whole superblocks allow, the larger ones last. +func cutEach(sizes []int, most int) []int { + out := make([]int, 0, len(sizes)) + for _, s := range sizes { + n := (s + most - 1) / most + for k := range n { + // (k+s%n)/n is one for the last s%n parts and nought before them. + out = append(out, s/n+(k+s%n)/n) + } + } + return out } // tileLimits is MaxTiles and MaxTileCols of a level, annex.a.levels.md lines @@ -122,7 +173,7 @@ func tileCols(g geometry, apart bool) []int { // at the edges or past them dropped, and a last size under 8 pixels joined to // the one before it, because Annex A asks every tile to be at least 8 by 8 of // the picture (CroppedTileWidth and CroppedTileHeight, annex.a.levels.md -// lines 263-266). +// lines 265-267). func sizesBetween(cuts []int, total, px int) []int { slices.Sort(cuts) cuts = slices.Compact(cuts) @@ -184,13 +235,9 @@ func (g grid) writeTileInfo(w *bitWriter, tileSizeBytes int) { w.ns(s-1, min(b.sbCols-start, maxTileWidth/superblock)) // width_in_sbs_minus_1 widest, start = max(widest, s), start+s } - area := b.sbRows * b.sbCols - if b.minLog2Tiles > 0 { - area >>= b.minLog2Tiles + 1 - } start = 0 for _, s := range g.rows { - w.ns(s-1, min(b.sbRows-start, max(area/widest, 1))) // height_in_sbs_minus_1 + w.ns(s-1, min(b.sbRows-start, max(b.maxArea()/widest, 1))) // height_in_sbs_minus_1 start += s } // context_update_tile_id says which tile's probabilities a frame keeps for diff --git a/internal/format/video/picture.go b/internal/format/video/picture.go index 29dc7517..e2eded53 100644 --- a/internal/format/video/picture.go +++ b/internal/format/video/picture.go @@ -34,19 +34,27 @@ var ink = color.RGBA{R: 240, G: 240, B: 240, A: 255} // Picture draws picture c of a film: the gradient every image format here // draws, moved by the seed, with the label burned into the top of it when // there is room, the clock under it reading when the picture starts, and the -// square at its step. It is drawn the way a film draws it, and a film draws -// its pictures through painters of its own, so this is for the probes and -// guards that measure any one of them. +// square at its step. It is put together the way a film codes it, tile by +// tile, each from where the film takes it (painter.source), so this is for +// the probes and guards that measure any one of them. func Picture(width, height int, seed uint64, label string, t Timeline, c int64) Planes { f := newFilm(width, height, seed, label, t) - return f.painter().draw(f.lookOf(c)) + ks := newKeyer(f.geometry, gridFor(f.geometry, t.FPS)) + p, l := f.painter(ks), f.lookOf(c) + out, _ := newPlanes(width, height) + for _, tile := range ks.tiles { + src, at := p.source(l, tile.rect) + copyRect(out, tile.rect.Min, src, at) + } + return out } // WholePicture is picture c painted whole - the gradient copied, the clock and // the square drawn on the copy, and every pixel of it converted - which is how // every picture of a film was painted until 2026-10-06. A film now paints only -// the rows that can change (painter.draw), and a guard holds the two to the -// same planes, because the bytes of every film depend on them being the same. +// the tiles that can change, and only their rows that can (painter.source), and +// a guard holds the two to the same planes, because the bytes of every film +// depend on them being the same. func WholePicture(width, height int, seed uint64, label string, t Timeline, c int64) Planes { f := newFilm(width, height, seed, label, t) return f.whole(f.lookOf(c)) @@ -133,23 +141,100 @@ func (g geometry) clockRight() int { } // film is what every picture of one film has in common, made once and only -// read after: the gradient with the label burned in, the same gradient in -// planes, and where the clock and the square go. The painters of one film -// share it, which is what lets several of them paint at once (ahead.go). +// read after: the gradient with the label burned in, in planes, the same +// gradient in pixels along the rows a picture can change in, and where the +// clock and the square go. The painters of one film share it, which is what +// lets several of them paint at once (ahead.go). +// +// The gradient is kept in pixels along the strips and nowhere else, and a +// painter has room for one tile and nothing else, because the whole picture +// in each was what a film's memory was: at 7680x4320, 133 MB of pixels the +// painters read a sixth of, and 50 MB of planes copied to every one of +// fifteen helpers - 1.3 GB at the peak and a live heap of 1028 MB, against +// 236 MB on one thread. Without them the same film peaks at 187 to 241 MB, and +// sixteen such films at once at 1.1 GB where they took 3.8 (2026-10-07, three +// rounds each, docs/WEBM-LIMIT-2026-10-07.md section 11). type film struct { geometry t Timeline - base *image.RGBA + seed uint64 + label string basePlanes Planes - planesBuf []uint8 // what basePlanes lie over, copied whole by a painter square *image.Uniform // strips are the only rows a picture can differ from the gradient in - // the clock's band and the square's - each widened to whole pairs of // rows, because one chroma sample covers two, and merged where they meet. - strips []image.Rectangle + // baseStrips are the gradient and the label along each of them, which a + // painter starts every picture's strip from. + strips []image.Rectangle + baseStrips []image.RGBA } func newFilm(width, height int, seed uint64, label string, t Timeline) *film { + f := &film{geometry: geometryOf(width, height, label, t), t: t, seed: seed, label: label, square: image.NewUniform(ink)} + f.strips = changingRows(width, height, [][2]int{{f.clockFrom, f.clockEnd}, {f.squareY, f.squareY + f.side}}) + f.baseStrips = stripsOver(f.strips, width) + g := newGround(width, height, seed, label) + for i := range f.baseStrips { + g.fill(&f.baseStrips[i]) + } + f.basePlanes, _ = newPlanes(width, height) + g.fillPlanes(f.basePlanes) + return f +} + +// ground makes the picture every picture of a film starts from - the +// gradient, moved by the seed, with the label burned into the top - a band of +// rows at a time, so the film never holds it whole: at 7680x4320 that whole +// was 133 MB that every film of a run made at once held at its start. The +// label's band is the one part that is not a sum of the row and the column, +// and Draw paints it solid before the text, so it is drawn once on a band of +// its own and copied - the pixels Draw makes on the whole picture, which the +// reference painter (gradient, whole) still draws whole and a guard compares. +type ground struct { + off int + label *image.RGBA +} + +func newGround(width, height int, seed uint64, label string) ground { + g := ground{off: int(seed % 256), label: &image.RGBA{}} + if band := labelBand(width, label); band > 0 { + g.label = image.NewRGBA(image.Rect(0, 0, width, min(height, band))) + imagelabel.Draw(g.label, label) + } + return g +} + +// fill paints the ground's rows into dst, which is the width of the picture. +func (g ground) fill(dst *image.RGBA) { + for y := dst.Rect.Min.Y; y < dst.Rect.Max.Y; y++ { + row := dst.Pix[(y-dst.Rect.Min.Y)*dst.Stride:][:dst.Stride] + if y < g.label.Rect.Max.Y { + copy(row, g.label.Pix[y*g.label.Stride:]) + continue + } + for x := range dst.Rect.Dx() { + row[4*x], row[4*x+1], row[4*x+2], row[4*x+3] = uint8((x+g.off)%256), uint8((y+g.off)%256), uint8((x+y+g.off)%256), 255 + } + } +} + +// fillPlanes converts the whole ground into p, sixty four rows at a time - an +// even number, so no chroma sample straddles two bands. +func (g ground) fillPlanes(p Planes) { + stride := 4 * p.Width + pix := make([]uint8, min(p.Height, superblock)*stride) + for y := 0; y < p.Height; y += superblock { + r := image.Rect(0, y, p.Width, min(p.Height, y+superblock)) + band := &image.RGBA{Pix: pix[:r.Dy()*stride], Stride: stride, Rect: r} + g.fill(band) + convertRect(band, p, image.Point{}, r) + } +} + +// gradient is the ground drawn whole, the way every picture of a film was +// until 2026-10-07, for the reference painter only (whole). +func gradient(width, height int, seed uint64, label string) *image.RGBA { off := int(seed % 256) base := image.NewRGBA(image.Rect(0, 0, width, height)) for y := range height { @@ -161,21 +246,49 @@ func newFilm(width, height int, seed uint64, label string, t Timeline) *film { if Labelled(width, label) { imagelabel.Draw(base, label) } - f := &film{geometry: geometryOf(width, height, label, t), t: t, base: base, square: image.NewUniform(ink)} - f.basePlanes, f.planesBuf = newPlanes(width, height) - toPlanes(base, f.basePlanes) - f.strips = changingRows(width, height, [][2]int{{f.clockFrom, f.clockEnd}, {f.squareY, f.squareY + f.side}}) - return f + return base +} + +// stripsOver is a picture's strips in pixels, the width of the picture, all +// of them over one allocation. +func stripsOver(strips []image.Rectangle, width int) []image.RGBA { + stride := 4 * width + rows := 0 + for _, r := range strips { + rows += r.Dy() + } + pix := make([]uint8, rows*stride) + out := make([]image.RGBA, len(strips)) + for i, r := range strips { + n := r.Dy() * stride + out[i] = image.RGBA{Pix: pix[:n:n], Stride: stride, Rect: r} + pix = pix[n:] + } + return out +} + +// changes says whether a picture can differ from the gradient anywhere in r. +func (f *film) changes(r image.Rectangle) bool { + for _, s := range f.strips { + if s.Overlaps(r) { + return true + } + } + return false } // newPlanes is the three planes of a picture this size in one allocation, -// and that allocation, which a painter copies whole (film.painter). +// and that allocation. func newPlanes(width, height int) (Planes, []uint8) { - cw, ch := (width+1)/2, (height+1)/2 - buf := make([]uint8, width*height+2*cw*ch) + buf := make([]uint8, planesSize(width, height)) return planesOver(buf, width, height), buf } +// planesSize is the bytes of the three planes of a picture this size. +func planesSize(width, height int) int { + return width*height + 2*((width+1)/2)*((height+1)/2) +} + // planesOver lays the three planes out over buf, luma first. Each is capped // at its own length, so no plane can grow into the next. func planesOver(buf []uint8, width, height int) Planes { @@ -229,87 +342,102 @@ func lookAt(g geometry, t Timeline, c int64) look { } // whole paints a picture the long way, into planes of its own: the whole -// gradient copied, the clock and the square drawn on the copy, every pixel -// converted. It is the reference painter.draw is held to (WholePicture). +// gradient drawn again, the clock and the square drawn on it, every pixel +// converted. It is the reference painter.source is held to (WholePicture). func (f *film) whole(l look) Planes { - work := image.NewRGBA(f.base.Rect) - copy(work.Pix, f.base.Pix) + work := gradient(f.width, f.height, f.seed, f.label) if f.showClock() { b := work.Rect imagelabel.Draw(work.SubImage(image.Rect(0, f.clockFrom, b.Dx(), b.Dy())).(*image.RGBA), l.clock) } draw.Draw(work, image.Rect(l.x, f.squareY, l.x+f.side, f.squareY+f.side), f.square, image.Point{}, draw.Src) - planes, _ := newPlanes(f.base.Rect.Dx(), f.base.Rect.Dy()) + planes, _ := newPlanes(f.width, f.height) toPlanes(work, planes) return planes } -// painter draws pictures of one film into planes of its own. They start as -// the gradient's, and every row outside the film's strips stays the -// gradient's in every picture, so a picture repaints and converts the strips -// and nothing else - at 1920x1080 the clock's band and the square's are about -// a sixth of the rows. One painter is used by one goroutine at a time. +// painter draws the tiles of one film's pictures that can change into planes +// of its own, one tile at a time. Every row outside the film's strips stays +// the gradient's in every picture, so a tile is the film's planes copied and +// its strips repainted and converted over them - at 1920x1080 the clock's band +// and the square's are about a sixth of the rows. One painter is used by one +// goroutine at a time. type painter struct { f *film - planes Planes + tile []uint8 // the planes of the largest tile that can change strips []image.RGBA } -// painter is a painter of this film, in four allocations whatever the size: -// itself, its planes, its strips, and the pixels of all of them. Every helper -// coding a film makes one, and the allocations a file may cost are counted -// (the AllocCeiling of the formats built on this). -func (f *film) painter() *painter { - w := f.base.Rect.Dx() - stride := 4 * w - rows := 0 - for _, r := range f.strips { - rows += r.Dy() - } - pix := make([]uint8, rows*stride) - p := &painter{f: f, planes: planesOver(slices.Clone(f.planesBuf), w, f.base.Rect.Dy()), strips: make([]image.RGBA, len(f.strips))} - for i, r := range f.strips { - n := r.Dy() * stride - p.strips[i] = image.RGBA{Pix: pix[:n:n], Stride: stride, Rect: r} - pix = pix[n:] +// painter is a painter of this film's tiles, in four allocations whatever the +// size: itself, its tile, its strips, and the pixels of all of them. Every +// helper coding a film makes one, and the allocations a file may cost are +// counted (the AllocCeiling of the formats built on this). +func (f *film) painter(ks keyer) *painter { + most := 0 + for _, t := range ks.tiles { + if f.changes(t.rect) { + most = max(most, planesSize(t.rect.Dx(), t.rect.Dy())) + } } - return p + return &painter{f: f, tile: make([]uint8, most), strips: stripsOver(f.strips, f.width)} } -// draw paints the whole picture with this look into the painter's planes and -// returns them. They are overwritten by the next call. -func (p *painter) draw(l look) Planes { return p.drawIn(l, p.f.base.Rect) } +// source is the planes tile r of the picture with look l is coded from, and +// where r lies in them: the film's own planes for a tile no picture changes, +// read by every goroutine coding the film and written by none, and for a tile +// that can change, the painter's, overwritten by its next call. +func (p *painter) source(l look, r image.Rectangle) (Planes, image.Rectangle) { + if !p.f.changes(r) { + return p.f.basePlanes, r + } + return p.drawIn(l, r), image.Rectangle{Max: r.Size()} +} -// drawIn paints the picture with this look and converts only the pixels -// inside r - the tile a film is about to code - so only r of the planes it -// returns may be read: the rest holds whatever an earlier picture left. r -// starts on an even row and an even column, as every tile does. +// drawIn paints tile r of the picture with this look into the painter's planes +// of the tile's size: the film's planes there copied, and each strip crossing +// r the gradient's rows copied, the clock drawn when its band starts in the +// strip, the square drawn where it crosses the strip, and the part of it inside +// r converted - the same steps, in the same order, as whole, on fewer pixels. +// r starts on an even row and an even column, as every tile does. // -// Each strip crossing r is the gradient's rows copied, the clock drawn when -// its band starts in the strip, the square drawn where it crosses the strip, -// and the part of it inside r converted - the same steps, in the same order, -// as whole, on fewer pixels. The rows are copied across the whole width, -// because the clock's characters are placed from the picture's left edge. -// The clock's band always lies whole inside one strip, because the strips -// were cut around it, so Draw sizes it as it would on the whole picture. +// The strip's rows are copied across the whole width, because the clock's +// characters are placed from the picture's left edge. The clock's band always +// lies whole inside one strip, because the strips were cut around it, so Draw +// sizes it as it would on the whole picture. func (p *painter) drawIn(l look, r image.Rectangle) Planes { f := p.f - w := f.base.Rect.Dx() + out := planesOver(p.tile, r.Dx(), r.Dy()) + copyRect(out, image.Point{}, f.basePlanes, r) for i := range p.strips { - s := &p.strips[i] + s, base := &p.strips[i], &f.baseStrips[i] in := s.Rect.Intersect(r) if in.Empty() { continue } - rows := s.Pix[(in.Min.Y-s.Rect.Min.Y)*s.Stride : (in.Max.Y-s.Rect.Min.Y)*s.Stride] - copy(rows, f.base.Pix[in.Min.Y*f.base.Stride:in.Max.Y*f.base.Stride]) + from, to := (in.Min.Y-s.Rect.Min.Y)*s.Stride, (in.Max.Y-s.Rect.Min.Y)*s.Stride + copy(s.Pix[from:to], base.Pix[from:to]) if f.showClock() && s.Rect.Min.Y <= f.clockFrom && f.clockFrom < s.Rect.Max.Y { - imagelabel.Draw(s.SubImage(image.Rect(0, f.clockFrom, w, s.Rect.Max.Y)).(*image.RGBA), l.clock) + imagelabel.Draw(s.SubImage(image.Rect(0, f.clockFrom, f.width, s.Rect.Max.Y)).(*image.RGBA), l.clock) } draw.Draw(s, image.Rect(l.x, f.squareY, l.x+f.side, f.squareY+f.side), f.square, image.Point{}, draw.Src) - convertRect(s, p.planes, in) + convertRect(s, out, r.Min, in) + } + return out +} + +// copyRect copies the part r of src into dst with its top left at at. Both +// start on an even row and column, so the chroma samples r covers are whole +// samples of both. +func copyRect(dst Planes, at image.Point, src Planes, r image.Rectangle) { + for y := r.Min.Y; y < r.Max.Y; y++ { + copy(dst.Y[(at.Y+y-r.Min.Y)*dst.Width+at.X:][:r.Dx()], src.Y[y*src.Width+r.Min.X:]) + } + scw, dcw, cw := (src.Width+1)/2, (dst.Width+1)/2, (r.Dx()+1)/2 + for cy := r.Min.Y / 2; cy < (r.Max.Y+1)/2; cy++ { + d, s := (at.Y/2+cy-r.Min.Y/2)*dcw+at.X/2, cy*scw+r.Min.X/2 + copy(dst.U[d:][:cw], src.U[s:]) + copy(dst.V[d:][:cw], src.V[s:]) } - return p.planes } // toPlanes converts to BT.709 studio range in whole numbers. @@ -327,26 +455,30 @@ func (p *painter) drawIn(l look, r image.Rectangle) Planes { // // Chroma is the rounded mean of the two by two block it covers, so an odd // width or height averages the pixels that are there. -func toPlanes(img *image.RGBA, p Planes) { convertRect(img, p, img.Rect) } - -// convertRect converts the pixels inside r of the picture p is, read from img -// - the whole picture, or a strip of it whose rows hold r's. r starts on an -// even row and column and ends on even ones or the picture's edge, so every -// chroma sample written here covers pixels inside r. -func convertRect(img *image.RGBA, p Planes, r image.Rectangle) { - w, h := p.Width, p.Height +func toPlanes(img *image.RGBA, p Planes) { convertRect(img, p, image.Point{}, img.Rect) } + +// convertRect converts the pixels inside r of a picture, read from img - the +// whole picture, or a strip of it whose rows hold r's - into p, the planes of +// the part of the picture whose top left is at: the whole of it, or one tile. +// r starts on an even row and column and ends on even ones or the picture's +// edge, so every chroma sample written here covers pixels inside r. p ends +// where the picture does or on a multiple of 64, so a two by two block cut at +// p's edge is cut at the picture's. +func convertRect(img *image.RGBA, p Planes, at image.Point, r image.Rectangle) { + w := p.Width top := img.Rect.Min.Y for y := r.Min.Y; y < r.Max.Y; y++ { row := img.Pix[(y-top)*img.Stride:] for x := r.Min.X; x < r.Max.X; x++ { red, g, b := int(row[4*x]), int(row[4*x+1]), int(row[4*x+2]) - p.Y[y*w+x] = uint8(16 + (47*red+157*g+16*b+128)>>8) + p.Y[(y-at.Y)*w+x-at.X] = uint8(16 + (47*red+157*g+16*b+128)>>8) } } cw := (w + 1) / 2 for cy := r.Min.Y / 2; cy < (r.Max.Y+1)/2; cy++ { for cx := r.Min.X / 2; cx < (r.Max.X+1)/2; cx++ { - p.U[cy*cw+cx], p.V[cy*cw+cx] = chroma(img, 2*cx, 2*cy, w, h) + i := (cy-at.Y/2)*cw + cx - at.X/2 + p.U[i], p.V[i] = chroma(img, 2*cx, 2*cy, at.X+w, at.Y+p.Height) } } } diff --git a/internal/format/video/pictures.go b/internal/format/video/pictures.go index 36fc8980..85323e95 100644 --- a/internal/format/video/pictures.go +++ b/internal/format/video/pictures.go @@ -226,10 +226,11 @@ func (p *Pictures) CopySample() []byte { return p.copied } func (c Choice) CodedSize(t Timeline, change int64) (int, error) { f := newFilm(c.Width, c.Height, c.Seed, c.Label, t) ks := newKeyer(f.geometry, gridFor(f.geometry, t.FPS)) - p, l := f.painter(), f.lookOf(change) + p, l := f.painter(ks), f.lookOf(change) size := 0 for _, tile := range ks.tiles { - coded, err := encodeTile(p.drawIn(l, tile.rect), tile.rect, c.QIndex) + src, at := p.source(l, tile.rect) + coded, err := encodeTile(src, at, c.QIndex) if err != nil { return 0, err } diff --git a/internal/format/video/settings.go b/internal/format/video/settings.go index ae715893..37a441a5 100644 --- a/internal/format/video/settings.go +++ b/internal/format/video/settings.go @@ -41,25 +41,24 @@ const ( maxQuality = 100 defaultQuality = 60 - // The picture has to fit one AV1 tile, because gav1d codes one: a frame - // wider than 4096 or larger than 2304 blocks of 64 by 64 pixels comes out - // of its encoder with a header that announces several tiles over the data - // of one, and both libaom and gav1d's own decoder refuse it. Measured on - // 2026-10-06 (docs/REVIEW-165-2026-10-06.md): 4096x2304 decodes, 4096x2305 - // and 4097x64 do not. That is a limit of the encoder this tool carries, not - // of AV1, so it is the number to raise if a later gav1d codes several tiles. + // The picture's bounds are AV1's, not the encoder's. gav1d codes one tile + // correctly and no more - a larger frame comes out of it with a header + // that announces several tiles over the data of one (measured 2026-10-06, + // docs/REVIEW-165-2026-10-06.md), and until 2026-10-07 that held the + // picture to 4096x2304. A picture is cut into tiles here now (grid.go) and + // every tile is within one of gav1d's, so the bound left is the format's: + // maxPixels is the largest picture an AV1 level describes, 8192 by 4352 + // (MaxPicSize of the 6.x levels, annex.a.levels.md lines 89-92). A larger + // one could declare no level but the maximum parameters one, which a + // player is free to refuse. The owner's decision of 2026-10-07 + // (docs/WEBM-LIMIT-2026-10-07.md section 10). // - // maxWidth is the widest tile. maxHeight is gav1d's longest side - a tall - // narrow picture is one tile as long as it stays within the area. - maxWidth = 4096 + // Either side reaches 16384, the widest picture of those levels. A picture + // taller than their 8704 declares the maximum parameters level, as it did + // before the bound moved - a tall narrow picture was always allowed. + maxWidth = 16384 maxHeight = 16384 - // tileBlocks is how many 64 by 64 blocks one tile holds, and - // maxTilePixels the same area in pixels, which is what the registry can - // declare: a product of the two sides. The blocks are what decides, because - // a side is rounded up to a whole block, so a pair just under the pixels - // can still be one block too many - checked before coding, see named. - tileBlocks = 2304 - maxTilePixels = 4096 * 2304 + maxPixels = 8192 * 4352 ) // frameRates are whole rates only. 23.976, 29.97 and 59.94 need a time scale @@ -104,16 +103,20 @@ func Properties() []format.Property { } // JointLimits is the bound on the picture, declared once for both formats: -// the area of one AV1 tile. In pixels rather than megapixels, because 9 437 184 -// read as "9 megapixels" would be a limit nobody can aim at. +// the largest picture an AV1 level describes. In pixels rather than +// megapixels, because 35 651 584 read as "36 megapixels" would be a limit +// nobody can aim at. // -// The memory bound AVIF declares is not here, and not by omission: one tile is -// far below it, so it could never be the limit a request meets. +// Memory is not what bounds it, and AVIF's bound of that kind is not here by +// choice: a film keeps its picture as planes, a byte and a half a pixel, and +// its painters one tile each (picture.go), so a 7680x4320 film peaked at 187 +// to 241 MB on sixteen threads, and sixteen of them written at once at 1.1 GB +// (measured 2026-10-07). func JointLimits() []format.JointLimit { return []format.JointLimit{{ - Of: imagedim.SettingWidth, By: imagedim.SettingHeight, Max: maxTilePixels, + Of: imagedim.SettingWidth, By: imagedim.SettingHeight, Max: maxPixels, Unit: "pixels", Base: "pixels", - Why: "the encoder codes a picture as one AV1 tile, which holds 4096 by 2304 pixels", + Why: "the largest picture any AV1 level describes is 8192 by 4352 pixels", }} } diff --git a/internal/format/video/still.go b/internal/format/video/still.go index a061c93f..2b92dd89 100644 --- a/internal/format/video/still.go +++ b/internal/format/video/still.go @@ -56,12 +56,13 @@ func (t tileCoded) writeRest(w *bitWriter) { } } -// encodeTile codes the part r of a picture as a picture of its own. gav1d -// reads its source a row at a time by the stride and never past Width and -// Height (av1/encode_intra.go, residualEdge, the edge clamped to sw-1 and -// sh-1), so the tile is read where it lies in the picture's planes rather -// than copied out of them. r starts on an even row and column, as a tile does, -// so its chroma is the picture's. +// encodeTile codes the part r of the planes p as a picture of its own - p the +// film's planes and r a tile no picture changes, or a painter's planes of one +// tile and r all of them (painter.source). gav1d reads its source a row at a +// time by the stride and never past Width and Height (av1/encode_intra.go, +// residualEdge, the edge clamped to sw-1 and sh-1), so the tile is read where +// it lies rather than copied out. r starts on an even row and column, as a +// tile does, so its chroma is the picture's. // // gav1d's output is read here by the specification rather than by what // gav1d's writer happens to do, and anything other than the one shape this @@ -171,6 +172,19 @@ type tileBounds struct { maxLog2Rows, minLog2Tiles int } +// maxArea is maxTileAreaSb as tile_info derives it for tiles of coded sizes, +// 06.bitstream.syntax.md lines 1243-1247: the whole frame while it fits one +// tile, and once it does not, the frame shifted right by minLog2Tiles plus +// one - a quarter to a half of MAX_TILE_AREA. A row of tiles is at most this +// over the widest column, and a height tile_info cannot code is no height. +func (b tileBounds) maxArea() int { + area := b.sbRows * b.sbCols + if b.minLog2Tiles > 0 { + area >>= b.minLog2Tiles + 1 + } + return area +} + func boundsOf(width, height int) tileBounds { b := tileBounds{sbCols: sbOf(width), sbRows: sbOf(height)} b.minLog2Cols = tileLog2(maxTileWidth/superblock, b.sbCols) diff --git a/internal/guard/film_test.go b/internal/guard/film_test.go index e699b84b..d8e44121 100644 --- a/internal/guard/film_test.go +++ b/internal/guard/film_test.go @@ -553,6 +553,8 @@ func TestEveryFrameOfAFilmSurvivesItsReferenceTool(t *testing.T) { {}, {"duration": "10m", "keyframe_interval": "30s", "frame_rate": "30"}, {"duration": "40ms", "frame_rate": "25"}, + // Wider than one tile can be (grid.go, cutToFit). + {"width": "4240", "height": "1000", "duration": "2s"}, } tiled := 0 for _, props := range cases { @@ -585,29 +587,34 @@ func TestEveryFrameOfAFilmSurvivesItsReferenceTool(t *testing.T) { } } -// A picture the encoder would code as more than one AV1 tile is refused as a -// setting, before coding - not left to come out as an internal error. +// A picture larger than any AV1 level describes is refused as a setting, +// before coding, and the pictures that used to be refused for not fitting one +// tile are not. // -// gav1d codes one tile, and a larger frame comes out of it with a header that -// announces several tiles over the data of one, which libaom and gav1d's own -// decoder both refuse (measured 2026-10-06, docs/REVIEW-165-2026-10-06.md). -// Until the review of #165 the format declared sides up to 16384 and such a -// picture ended the run with exit 1 - "the program broke" - for a request the -// declaration had invited. 4000x2359 is the case the declared limit cannot -// catch: under the pixels, one block too many once its sides are rounded up. -func TestAFilmPictureLargerThanOneTileIsRefusedNotBroken(t *testing.T) { - for _, size := range [][2]string{{"4096", "2305"}, {"4000", "2359"}, {"4097", "64"}, {"7680", "4320"}} { - props := map[string]string{"width": size[0], "height": size[1], "duration": "1s"} +// Until 2026-10-07 the bound was one AV1 tile, 4096x2304, because gav1d codes +// one tile correctly and no more (docs/REVIEW-165-2026-10-06.md). A picture +// is cut into tiles of at most that now, and the bound is AV1's own: 8192 by +// 4352, the 6.x levels' MaxPicSize, with either side up to 16384 (the owner's +// decision, docs/WEBM-LIMIT-2026-10-07.md section 10). The refused pairs are +// one pixel over the area each way and a side over 16384. The accepted ones +// are the four the old bound refused, cheap to plan, and the two longest +// sides - not 8192x4352 itself, whose plan codes a sample of 8K pictures. +func TestAFilmPictureLargerThanAnyAV1LevelIsRefusedNotBroken(t *testing.T) { + plan := func(w, h string) error { + props := map[string]string{"width": w, "height": h, "duration": "1s"} _, err := engine.Plan([]engine.Target{filmTarget(32*1024*1024, props)}, engine.Options{OutDir: t.TempDir(), Seed: goldenSeed, Command: "test"}) + return err + } + for _, size := range [][2]string{{"8192", "4353"}, {"8193", "4352"}, {"16384", "2177"}, {"16385", "64"}, {"64", "16385"}} { var refused *format.PropertyValueError - if !errors.As(err, &refused) { + if err := plan(size[0], size[1]); !errors.As(err, &refused) { t.Errorf("%sx%s was not refused as a setting: %v", size[0], size[1], err) } } - // And the largest picture one tile holds is not refused. - props := map[string]string{"width": "4096", "height": "2304", "duration": "1s"} - if _, err := engine.Plan([]engine.Target{filmTarget(32*1024*1024, props)}, engine.Options{OutDir: t.TempDir(), Seed: goldenSeed, Command: "test"}); err != nil { - t.Errorf("4096x2304 is one tile and was refused: %v", err) + for _, size := range [][2]string{{"4096", "2305"}, {"4000", "2359"}, {"4097", "64"}, {"4352", "512"}, {"16384", "64"}, {"64", "16384"}} { + if err := plan(size[0], size[1]); err != nil { + t.Errorf("%sx%s is inside every bound the format declares and was refused: %v", size[0], size[1], err) + } } } diff --git a/internal/guard/filmlayout_test.go b/internal/guard/filmlayout_test.go new file mode 100644 index 00000000..c88f054b --- /dev/null +++ b/internal/guard/filmlayout_test.go @@ -0,0 +1,178 @@ +package guard + +import ( + "fmt" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/video" +) + +// annexATiles is MaxTiles and MaxTileCols of each level a film can declare, by +// seq_level_idx, copied from the specification's own table (AOMediaCodec/av1-spec +// 5e04f3f, annex.a.levels.md lines 99-115) rather than from the package, so +// the two are held to the source and not to each other. The maximum parameters +// level, 31, has no row: no level constraint applies to it. +var annexATiles = map[int][2]int{ + 0: {8, 4}, 1: {8, 4}, 4: {16, 6}, 5: {16, 6}, 8: {32, 8}, 9: {32, 8}, + 12: {64, 8}, 13: {64, 8}, 14: {64, 8}, 15: {64, 8}, + 16: {128, 16}, 17: {128, 16}, 18: {128, 16}, 19: {128, 16}, +} + +// Every way a film's picture is cut into tiles is one the AV1 specification +// allows - over every size the format declares, up to its bound. +// +// A picture is cut by a rule (internal/format/video, grid.go) rather than by +// asking an encoder, and a decoder takes a layout it should refuse without a +// word: libaom, dav1d and Chromium all played a film whose last row of tiles +// was one pixel tall, which Annex A forbids (docs/WEBM-LIMIT-2026-10-07.md +// section 2 - 81 900 such layouts in a sweep before the rule was mended). So +// this asks the specification, not a decoder: tile_info's own bounds +// (06.bitstream.syntax.md lines 1176-1268 - a column at most 64 superblocks, +// a row at most the area tile_info leaves over the widest column, one tile +// only where the frame fits one), MAX_TILE_AREA, 64 columns and rows at most, +// and Annex A for the lowest level the size and rate allow (annex.a.levels.md +// lines 240-271 - tiles and columns per level, the last tile at least 8 pixels +// each way, the largest tile times the frames a second under 588 251 136). +// +// The sizes are every side from 1 to 17 and each side next to a multiple of +// 64, where a superblock starts or ends, paired up to the declared bound. Two +// clocks and two rates, alternating, because the clock decides the columns +// and the rate the level. +func TestEveryFilmLayoutIsOneTheAV1SpecificationAllows(t *testing.T) { + d, err := format.Get("webm") + if err != nil { + t.Fatal(err) + } + if len(d.JointLimits) != 1 { + t.Fatalf("webm declares %d joint limits and this guard reads one", len(d.JointLimits)) + } + most := d.JointLimits[0].Max + var sides []int + for s := 1; s <= 17; s++ { + sides = append(sides, s) + } + for k := 1; k <= 256; k++ { + for _, s := range []int{64*k - 1, 64 * k, 64*k + 1, 64*k + 8} { + if s > 17 && s <= 16384 { + sides = append(sides, s) + } + } + } + var films [2]video.Timeline + for i, v := range [][2]int64{{1000, 30}, {500, 60}} { + if films[i], err = video.NewTimeline("webm", 10_000, 60_000, v[0], int(v[1])); err != nil { + t.Fatal(err) + } + } + labels := [2]string{core.Label("webm", 268435456, 7741), ""} + seen := layoutsSeen{} + n := 0 + for _, w := range sides { + for _, h := range sides { + if int64(w)*int64(h) > most { + continue + } + film := films[n%2] + cols, rows := video.Layout(w, h, labels[n/2%2], film) + n++ + for _, why := range layoutBreaks(w, h, film.FPS, cols, rows, &seen) { + if seen.reported < 20 { + t.Errorf("%dx%d at %d frames a second, columns %v, rows %v: %s", w, h, film.FPS, cols, rows, why) + } + seen.reported++ + } + } + } + // The guard has to have asked about the layouts the rule cuts, or it says + // nothing about them (O118). + if seen.wide < 1000 || seen.byArea < 1000 || seen.narrowed < 100 || seen.mostTiles < 32 { + t.Fatalf("%d layouts, %d with a column cut for width, %d with rows cut for area, %d with columns narrowed for a row of two, %d tiles at most - the sweep did not reach the layouts it is for", + n, seen.wide, seen.byArea, seen.narrowed, seen.mostTiles) + } + t.Logf("%d layouts, %d wide, %d cut for area, %d narrowed, %d tiles at most", n, seen.wide, seen.byArea, seen.narrowed, seen.mostTiles) + if seen.reported > 0 { + t.Errorf("%d breaks in %d layouts", seen.reported, n) + } +} + +type layoutsSeen struct { + wide, byArea, narrowed, mostTiles, reported int +} + +// layoutBreaks is every rule of the specification this layout of a w by h +// picture at fps breaks, and counts what kind of layout it is. +func layoutBreaks(w, h, fps int, cols, rows []int, seen *layoutsSeen) []string { + var out []string + sbCols, sbRows := (w+63)/64, (h+63)/64 + if sum(cols) != sbCols || sum(rows) != sbRows { + return []string{fmt.Sprintf("the sizes add up to %dx%d superblocks and the picture is %dx%d", sum(cols), sum(rows), sbCols, sbRows)} + } + minTiles := max(log2Up(64, sbCols), log2Up(2304, sbRows*sbCols)) + area := sbRows * sbCols + if minTiles > 0 { + area >>= minTiles + 1 + } + widest := 0 + for _, c := range cols { + widest = max(widest, c) + } + tiles := len(cols) * len(rows) + switch { + case tiles == 1 && minTiles > 0: + out = append(out, "one tile, and tile_info asks this frame for more") + case tiles > 1: + for _, r := range rows { + if r > max(area/widest, 1) { + out = append(out, fmt.Sprintf("a row of %d superblocks, and tile_info codes at most %d", r, max(area/widest, 1))) + } + } + } + if widest > 64 { + out = append(out, fmt.Sprintf("a column of %d superblocks, wider than MAX_TILE_WIDTH", widest)) + } + if len(cols) > 64 || len(rows) > 64 { + out = append(out, "more than 64 columns or rows") + } + largest := 0 + for _, r := range rows { + largest = max(largest, r*widest*64*64) + } + if largest > 4096*2304 { + out = append(out, "a tile larger than MAX_TILE_AREA") + } + if limits, ok := annexATiles[video.LevelFor(w, h, fps, 0, 1)]; ok { + if tiles > limits[0] || len(cols) > limits[1] { + out = append(out, fmt.Sprintf("%d tiles in %d columns, and the level allows %d in %d", tiles, len(cols), limits[0], limits[1])) + } + if len(cols) > 1 && w-64*(sbCols-cols[len(cols)-1]) < 8 { + out = append(out, "the last column is under 8 pixels") + } + if len(rows) > 1 && h-64*(sbRows-rows[len(rows)-1]) < 8 { + out = append(out, "the last row is under 8 pixels") + } + if largest*fps > 588_251_136 { + out = append(out, "the largest tile times the frames a second is over 588 251 136") + } + } + if sbCols > 64 { + seen.wide++ + } + if minTiles > 0 && sbCols <= 64 { + seen.byArea++ + } + if sbCols > 64 && sbRows > 1 && area/2 < 64 { + seen.narrowed++ + } + seen.mostTiles = max(seen.mostTiles, tiles) + return out +} + +func sum(sizes []int) int { + n := 0 + for _, s := range sizes { + n += s + } + return n +} diff --git a/internal/guard/filmtiles_test.go b/internal/guard/filmtiles_test.go index 9f76b767..60d50387 100644 --- a/internal/guard/filmtiles_test.go +++ b/internal/guard/filmtiles_test.go @@ -280,6 +280,12 @@ func TestEveryTileOfAFilmIsGav1dsCodingOfItsPixelsAndDecodesAsItDoesAlone(t *tes {map[string]string{"width": "640", "height": "360", "duration": "3s", "change_interval": "100ms"}, 4 << 20, rangeOf(30)}, {map[string]string{"width": "1001", "height": "563", "duration": "12s"}, 4 << 20, rangeOf(12)}, {map[string]string{"width": "1920", "height": "1080", "duration": "2s"}, 4 << 20, rangeOf(2)}, + // Wider than a tile can be, so the rest of the width is cut in two, + // and a row cut for the area tile_info leaves a tile. Then wider and + // only a superblock and a pixel tall, where the columns are narrowed so + // a row of tiles can be two superblocks (grid.go, cutToFit). + {map[string]string{"width": "4240", "height": "1000", "duration": "2s"}, 4 << 20, rangeOf(2)}, + {map[string]string{"width": "4097", "height": "65", "duration": "3s"}, 4 << 20, rangeOf(3)}, } tilesAsked, mostTiles := 0, 0 for _, c := range cases { diff --git a/internal/guard/generatorbytes_test.go b/internal/guard/generatorbytes_test.go index ad4b9e98..335dd8ac 100644 --- a/internal/guard/generatorbytes_test.go +++ b/internal/guard/generatorbytes_test.go @@ -164,6 +164,16 @@ func goldenCases() map[string]engine.Target { Properties: map[string]string{"width": "64", "height": "48", "duration": "2s", "keyframe_interval": "1s", "frame_rate": "25"}}, "webm_odd_size": {ID: "g", Format: "webm", Sizes: engine.Uniform(1, 65537), Label: true, Properties: map[string]string{"width": "64", "height": "48"}}, + // The three above are films of one AV1 tile. This one is cut into + // tiles - the clock's, the square's and the rest - so a change to how + // a picture is cut or to which tile is coded again moves its bytes. + "webm_tiles": {ID: "g", Format: "webm", Sizes: engine.Uniform(1, 65536), Label: true, + Properties: map[string]string{"width": "640", "height": "360", "duration": "3s"}}, + // Wider than one tile can be: the rest of the width is cut in two + // unequal columns, 32 and 33 superblocks, and a row of nine into four + // and five for the area tile_info leaves a tile, the larger last. + "webm_wide": {ID: "g", Format: "webm", Sizes: engine.Uniform(1, 524288), Label: true, + Properties: map[string]string{"width": "4240", "height": "1000", "duration": "2s"}}, // An odd size. The free box takes any length at all, so this is the case // that would catch padding that could only step in twos. diff --git a/internal/guard/testdata/generator-golden.json b/internal/guard/testdata/generator-golden.json index 48d25391..1627e6cc 100644 --- a/internal/guard/testdata/generator-golden.json +++ b/internal/guard/testdata/generator-golden.json @@ -346,6 +346,16 @@ "bytes": 65537, "sha256": "d4fc121f6b6714609207bd98d1f52fb232bfdf0520009b1b264b2151b732b3db", "measured_on": "2026-10-06" + }, + "webm_tiles": { + "bytes": 65536, + "sha256": "8ff27de05a089745a6510703c5c593651fc604cf0e3bd30c336adfad484f79b3", + "measured_on": "2026-10-07" + }, + "webm_wide": { + "bytes": 524288, + "sha256": "270aab596f2a59abe47cf9ab35236964b88dcd1be2ea24247cbce8c72c3c1cd0", + "measured_on": "2026-10-07" } }, "remeasured": [ diff --git a/internal/gui/text/locale/registry/en.json b/internal/gui/text/locale/registry/en.json index a5245ecf..902aedda 100644 --- a/internal/gui/text/locale/registry/en.json +++ b/internal/gui/text/locale/registry/en.json @@ -1591,8 +1591,8 @@ }, "Joint.format/webm.width.height": { "description": "Why width times height of the webm format have a ceiling, at the end of the note under the two, after the word because.", - "hash": "sha256-ed5704060118", - "other": "the encoder codes a picture as one AV1 tile, which holds 4096 by 2304 pixels" + "hash": "sha256-72e26aa125ce", + "other": "the largest picture any AV1 level describes is 8192 by 4352 pixels" }, "Joint.format/xlsx.rows.columns": { "description": "Why rows times columns of the xlsx format have a ceiling, at the end of the note under the two, after the word because.", diff --git a/internal/gui/text/locale/registry/pl.json b/internal/gui/text/locale/registry/pl.json index c8c0698e..61320d25 100644 --- a/internal/gui/text/locale/registry/pl.json +++ b/internal/gui/text/locale/registry/pl.json @@ -317,7 +317,7 @@ "Joint.format/jpg.width.height": { "hash": "sha256-0bed7aa664d8", "other": "obraz jest trzymany w pamięci podczas kodowania" }, "Joint.format/jxl.width.height": { "hash": "sha256-269a6c476761", "other": "koder trzyma cały obraz w pamięci podczas pracy" }, "Joint.format/png.width.height": { "hash": "sha256-0bed7aa664d8", "other": "obraz jest trzymany w pamięci podczas kodowania" }, - "Joint.format/webm.width.height": { "hash": "sha256-ed5704060118", "other": "koder zapisuje obraz jako jeden kafel AV1, a ten mieści 4096 na 2304 piksele" }, + "Joint.format/webm.width.height": { "hash": "sha256-72e26aa125ce", "other": "największy obraz, jaki opisuje którykolwiek poziom AV1, ma 8192 na 4352 piksele" }, "Joint.format/xlsx.rows.columns": { "hash": "sha256-e1ef283f3c5d", "other": "arkusz powstaje w pamięci, zanim zostanie spakowany" }, "Kind.file": { "hash": "sha256-3b9c358f36f0", "other": "plik" }, "Kind.folder": { "hash": "sha256-034a00624882", "other": "folder" }, diff --git a/internal/gui/text/locale/said/en.json b/internal/gui/text/locale/said/en.json index e1a2c504..860694f9 100644 --- a/internal/gui/text/locale/said/en.json +++ b/internal/gui/text/locale/said/en.json @@ -1591,7 +1591,7 @@ "other": "this size needs a padding chunk and the smallest one costs {{.ChunkHeader}} B, so nothing between {{.Fixed}} and {{.Fixed2}} B can be reached" }, "format.TwoSettings": { - "description": "Said by internal/format/video - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Of}}, {{.By}}, written the way the program writes them - keep each spelled exactly that way.", + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Of}}, {{.By}}, written the way the program writes them - keep each spelled exactly that way.", "hash": "sha256-a61ae8d11eb8", "other": "{{.Of}} and {{.By}}" }, @@ -3141,11 +3141,6 @@ "hash": "sha256-a53588601259", "other": "use one of: {{.Known}}" }, - "video.MoreThanOneTile": { - "description": "Said by internal/format/video - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Width}}, {{.Height}}, {{.Blocks}}, {{.Most}}, written the way the program writes them - keep each spelled exactly that way.", - "hash": "sha256-319218310f03", - "other": "a {{.Width}}x{{.Height}} picture is {{.Blocks}} blocks of 64 by 64 pixels once its sides are rounded up to whole blocks, and the encoder codes one AV1 tile, which holds {{.Most}}. Ask for a smaller pair, such as 4096x2304 or 3840x2160" - }, "video.NoClock": { "description": "Said by internal/format/webm - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Width}}, {{.Height}}, written the way the program writes them - keep each spelled exactly that way.", "hash": "sha256-9fb32f58b247", diff --git a/internal/gui/text/locale/said/pl.json b/internal/gui/text/locale/said/pl.json index a9fb5748..77069513 100644 --- a/internal/gui/text/locale/said/pl.json +++ b/internal/gui/text/locale/said/pl.json @@ -625,7 +625,6 @@ "tool.ThereIsNoToolCalled": { "hash": "sha256-151e818f434e", "other": "nie ma narzędzia o nazwie {{.ID}}" }, "tool.ThisBuildHasNoTools": { "hash": "sha256-42b26a93974c", "other": "ta wersja nie ma narzędzi" }, "tool.UseOneOf": { "hash": "sha256-a53588601259", "other": "użyj jednego z: {{.Known}}" }, - "video.MoreThanOneTile": { "hash": "sha256-319218310f03", "other": "obraz {{.Width}}x{{.Height}} po zaokrągleniu boków w górę do pełnych bloków 64 na 64 piksele ma ich {{.Blocks}}, a koder zapisuje jeden kafel AV1, który mieści ich najwyżej {{.Most}}. Poproś o mniejszą parę, na przykład 4096x2304 albo 3840x2160" }, "video.NoClock": { "hash": "sha256-9fb32f58b247", "other": "Obraz ma {{.Width}}x{{.Height}} i zegar potrzebuje więcej miejsca, więc ten film nie pokazuje zegara. Kwadrat dalej przesuwa się po nim tam, gdzie ma miejsce na ruch." }, "video.NotOnAFrame": { "hash": "sha256-4471642f4d67", "other": "przy {{.FPS}} klatkach na sekundę długość musi kończyć się na klatce, a przy tej szybkości długości idą co {{.Step}}. Poproś o {{.Below}} albo {{.Above}}" }, "video.NotOnAFrameShortest": { "hash": "sha256-e6b01f8c0aeb", "other": "przy {{.FPS}} klatkach na sekundę długość musi kończyć się na klatce, a przy tej szybkości długości idą co {{.Step}}. Poproś o {{.Above}}, najkrótszy film przy tej szybkości" }, diff --git a/web/public/ar/formats/index.html b/web/public/ar/formats/index.html index 1ea97409..d399f72f 100644 --- a/web/public/ar/formats/index.html +++ b/web/public/ar/formats/index.html @@ -780,7 +780,7 @@

الإعدادات التي تقبلها كل صيغة

webm width - 1 - 4096 بكسل + 1 - 16384 بكسل diff --git a/web/public/cs/formaty/index.html b/web/public/cs/formaty/index.html index 9aed34c2..96fb704d 100644 --- a/web/public/cs/formaty/index.html +++ b/web/public/cs/formaty/index.html @@ -781,7 +781,7 @@

Nastavení, která každý formát přijímá

webm width - 1 - 4096 pixelů + 1 - 16384 pixelů diff --git a/web/public/de/formate/index.html b/web/public/de/formate/index.html index d2aef027..bc06e3e5 100644 --- a/web/public/de/formate/index.html +++ b/web/public/de/formate/index.html @@ -783,7 +783,7 @@

Einstellungen, die jedes Format kennt

webm width - 1 - 4096 Pixel + 1 - 16384 Pixel diff --git a/web/public/es/formatos/index.html b/web/public/es/formatos/index.html index a310a9f0..869494ee 100644 --- a/web/public/es/formatos/index.html +++ b/web/public/es/formatos/index.html @@ -784,7 +784,7 @@

Ajustes que admite cada formato

webm width - 1 - 4096 píxeles + 1 - 16384 píxeles diff --git a/web/public/formats/index.html b/web/public/formats/index.html index 453afecb..f9a74933 100644 --- a/web/public/formats/index.html +++ b/web/public/formats/index.html @@ -782,7 +782,7 @@

Settings each format accepts

webm width - 1 - 4096 pixels + 1 - 16384 pixels diff --git a/web/public/fr/formats/index.html b/web/public/fr/formats/index.html index 1bbfd88e..8b31bc21 100644 --- a/web/public/fr/formats/index.html +++ b/web/public/fr/formats/index.html @@ -784,7 +784,7 @@

Réglages que chaque format accepte

webm width - 1 - 4096 pixels + 1 - 16384 pixels diff --git a/web/public/hi/formats/index.html b/web/public/hi/formats/index.html index 4891d3d9..d58ec728 100644 --- a/web/public/hi/formats/index.html +++ b/web/public/hi/formats/index.html @@ -782,7 +782,7 @@

हर फ़ॉर्मैट की स्वीकार की जा webm width - 1 - 4096 पिक्सेल + 1 - 16384 पिक्सेल diff --git a/web/public/id/format/index.html b/web/public/id/format/index.html index f4c72dc4..918f7bf4 100644 --- a/web/public/id/format/index.html +++ b/web/public/id/format/index.html @@ -782,7 +782,7 @@

Pengaturan yang diterima setiap format

webm width - 1 - 4096 piksel + 1 - 16384 piksel diff --git a/web/public/it/formati/index.html b/web/public/it/formati/index.html index 7a33e0ac..32c6ad41 100644 --- a/web/public/it/formati/index.html +++ b/web/public/it/formati/index.html @@ -784,7 +784,7 @@

Impostazioni che ogni formato accetta

webm width - 1 - 4096 pixel + 1 - 16384 pixel diff --git a/web/public/ja/formats/index.html b/web/public/ja/formats/index.html index 778a2e56..348698c9 100644 --- a/web/public/ja/formats/index.html +++ b/web/public/ja/formats/index.html @@ -774,7 +774,7 @@

形式ごとに受け付ける設定

webm width - 1 - 4096 ピクセル + 1 - 16384 ピクセル diff --git a/web/public/ko/formats/index.html b/web/public/ko/formats/index.html index c0fa96e2..a71984f2 100644 --- a/web/public/ko/formats/index.html +++ b/web/public/ko/formats/index.html @@ -775,7 +775,7 @@

각 형식이 받는 설정

webm width - 1 - 4096 픽셀 + 1 - 16384 픽셀 diff --git a/web/public/nl/formaten/index.html b/web/public/nl/formaten/index.html index a2a65232..1cafb76c 100644 --- a/web/public/nl/formaten/index.html +++ b/web/public/nl/formaten/index.html @@ -784,7 +784,7 @@

Instellingen die elk formaat accepteert

webm width - 1 - 4096 pixels + 1 - 16384 pixels diff --git a/web/public/pl/formaty/index.html b/web/public/pl/formaty/index.html index cdcafe60..8d7c0167 100644 --- a/web/public/pl/formaty/index.html +++ b/web/public/pl/formaty/index.html @@ -782,7 +782,7 @@

Ustawienia, które przyjmuje każdy format

webm width - 1 - 4096 pikseli + 1 - 16384 pikseli diff --git a/web/public/pt-br/formatos/index.html b/web/public/pt-br/formatos/index.html index a86a592b..1485bf15 100644 --- a/web/public/pt-br/formatos/index.html +++ b/web/public/pt-br/formatos/index.html @@ -783,7 +783,7 @@

Configurações que cada formato aceita

webm width - 1 - 4096 pixels + 1 - 16384 pixels diff --git a/web/public/ro/formate/index.html b/web/public/ro/formate/index.html index f3c1a6d9..d06d9826 100644 --- a/web/public/ro/formate/index.html +++ b/web/public/ro/formate/index.html @@ -783,7 +783,7 @@

Setările pe care le acceptă fiecare format

webm width - 1 - 4096 pixeli + 1 - 16384 pixeli diff --git a/web/public/ru/formats/index.html b/web/public/ru/formats/index.html index 1731cac2..47684caa 100644 --- a/web/public/ru/formats/index.html +++ b/web/public/ru/formats/index.html @@ -782,7 +782,7 @@

Настройки, которые принимает каждый форм webm width - 1 - 4096 пикселей + 1 - 16384 пикселей diff --git a/web/public/th/formats/index.html b/web/public/th/formats/index.html index 1ec22c8b..12e24590 100644 --- a/web/public/th/formats/index.html +++ b/web/public/th/formats/index.html @@ -780,7 +780,7 @@

การตั้งค่าที่แต่ละรูปแบบร webm width - 1 - 4096 พิกเซล + 1 - 16384 พิกเซล diff --git a/web/public/tr/bicimler/index.html b/web/public/tr/bicimler/index.html index 09086293..f1796af0 100644 --- a/web/public/tr/bicimler/index.html +++ b/web/public/tr/bicimler/index.html @@ -782,7 +782,7 @@

Her biçimin kabul ettiği ayarlar

webm width - 1 - 4096 piksel + 1 - 16384 piksel diff --git a/web/public/uk/formats/index.html b/web/public/uk/formats/index.html index de5f1540..9f22d304 100644 --- a/web/public/uk/formats/index.html +++ b/web/public/uk/formats/index.html @@ -782,7 +782,7 @@

Налаштування, які приймає кожен формат

webm width - 1 - 4096 пікселів + 1 - 16384 пікселів diff --git a/web/public/vi/dinh-dang/index.html b/web/public/vi/dinh-dang/index.html index b23c19e1..0e50f334 100644 --- a/web/public/vi/dinh-dang/index.html +++ b/web/public/vi/dinh-dang/index.html @@ -780,7 +780,7 @@

Các thiết lập mỗi định dạng nhận

webm width - 1 - 4096 pixel + 1 - 16384 pixel diff --git a/web/public/zh-hans/formats/index.html b/web/public/zh-hans/formats/index.html index 535f5a5b..0405c559 100644 --- a/web/public/zh-hans/formats/index.html +++ b/web/public/zh-hans/formats/index.html @@ -774,7 +774,7 @@

每种格式接受的设置

webm width - 1 - 4096 像素 + 1 - 16384 像素 diff --git a/web/public/zh-hant/formats/index.html b/web/public/zh-hant/formats/index.html index a2363ae5..98f6ab20 100644 --- a/web/public/zh-hant/formats/index.html +++ b/web/public/zh-hant/formats/index.html @@ -774,7 +774,7 @@

每種格式接受的設定

webm width - 1 - 4096 像素 + 1 - 16384 像素