Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions internal/format/video/ahead.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
25 changes: 0 additions & 25 deletions internal/format/video/choose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
75 changes: 61 additions & 14 deletions internal/format/video/grid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading