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
12 changes: 6 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ because it turns other people's test suites red.
shows a running clock and one that freezes shows a stopped one. The clock
reads the time the picture starts, the way a player's position does -
`00:00:07`, with milliseconds when the changes do not fall on whole
seconds. Each change is a whole new picture in bytes and in coding time,
and when a film has more than one picture they are coded on several cores
at once - a thirty-minute 1920x1080 film took 68 s on sixteen threads, and
the
progress bar counts the pictures as well as the bytes, so the time it says
is left holds while they are coded. The frames
seconds. Each change is a whole new picture in bytes, but only the part of
it that moved is coded again: from 256x144 up the picture is cut into AV1
tiles, and a tile nothing changes is coded once a film. What is left to
code is coded on several cores at once - a thirty-minute 1920x1080 film of
2 GB takes about 3 s on sixteen threads - and the progress bar counts that
coding as well as the bytes, so the time it says is left holds. The frames
between changes cost a few bytes each, so an hour at thirty
frames a second with a change every second fits in about a megabyte
(1 075 831 B at the smallest), and a `change_interval` as long as the film
Expand Down
18 changes: 18 additions & 0 deletions internal/format/imagelabel/imagelabel.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,24 @@ func Fits(width, chars int) bool {
return scaleFor(width, chars) > 0
}

// Cells is where Draw puts the characters of a text: character k covers the
// columns from First + k*Step, Width of them, and every row of the band. A
// film asks it to know which characters of its clock fall in which part of
// the picture, so it is Draw's own arithmetic rather than a copy of it.
type Cells struct {
First, Step, Width int
}

// CellsOf is where Draw puts a text of chars characters across width - the
// zero Cells when Draw would draw nothing.
func CellsOf(width, chars int) Cells {
s := scaleFor(width, chars)
if s == 0 {
return Cells{}
}
return Cells{First: pad, Step: (glyphWidth + 1) * s, Width: glyphWidth * s}
}

// BandHeight is how tall the band Draw paints for a label of chars characters
// across width is, before it is cut to the picture - 0 when the label does not
// fit. A film draws a second line under the first and asks this before it
Expand Down
125 changes: 67 additions & 58 deletions internal/format/video/ahead.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,21 @@ import (
// from one count for the whole process, GOMAXPROCS less one. A film that
// finds none free codes alone, as every film did before, and asks again at its
// next picture.
//
// Since a picture is cut into tiles (grid.go), what a helper codes is a tile:
// a new tile of the clock at every change, the square's ten places once each,
// and the rest of the picture once. The pictures still come out in order and
// the bytes are still those of one goroutine.

// helping is how many helpers are coding beside the writers right now.
var helping atomic.Int64

// codedBeside counts the pictures a helper coded rather than the goroutine
// that asked for them, since the process started.
// codedBeside counts the tiles a helper coded rather than the goroutine that
// asked for them, since the process started.
var codedBeside atomic.Int64

// Helpers is how many helpers are coding right now and how many pictures
// helpers have coded since the process started. Guards read it: a film held to
// Helpers is how many helpers are coding right now and how many tiles helpers
// have coded since the process started. Guards read it: a film held to
// the bytes of one goroutine says nothing unless several really coded it, and
// a film abandoned half way has to leave none of them running.
func Helpers() (running, coded int64) { return helping.Load(), codedBeside.Load() }
Expand All @@ -58,44 +63,50 @@ func takeHelper() bool {
}
}

// job is one picture to code. Whoever takes it first - a helper, or the
// writer that needs it - codes it, and done is released when it is coded, so
// the writer waits only for a picture somebody else is coding. A WaitGroup
// inside the job rather than a channel beside it, because a channel is one
// more allocation for every picture of every film.
// job is one tile to code, of the picture with this look. Whoever takes it
// first - a helper, or the writer that needs it - codes it, and done is
// released when it is coded, so the writer waits only for a tile somebody else
// is coding. A WaitGroup inside the job rather than a channel beside it,
// because a channel is one more allocation for every tile of every film.
type job struct {
look look
change int64
tile int
taken atomic.Bool
done sync.WaitGroup
coded Coded
coded tileCoded
err error
// panicked is what coding it panicked with. A panic on a helper would end
// the process, where the same panic on the writer becomes the run's error
// (internal/engine, writeWithoutCrashing), so a helper keeps it here and
// the writer panics with it when it reaches the picture.
// the writer panics with it when it reaches the tile.
panicked any
// counted says the tile's bytes are to be added to what the film keeps of
// the clock's tiles once it is coded. Read and written by the owning
// goroutine only.
counted bool
}

func newJob(l look, change int64) *job {
j := &job{look: l, change: change}
func newJob(l look, change int64, tile int) *job {
j := &job{look: l, change: change, tile: tile}
j.done.Add(1)
return j
}

// codedJob is a picture already coded, which nobody codes again.
func codedJob(c Coded) *job {
j := &job{coded: c}
// codedJob is a tile already coded, which nobody codes again.
func codedJob(t tileCoded) *job {
j := &job{coded: t}
j.taken.Store(true)
return j
}

// crew codes the pictures of one film: the goroutine that owns it, and the
// crew codes the tiles of one film: the goroutine that owns it, and the
// helpers it has been able to take. Jobs are offered in the order the film
// shows them and waited for in the same order, so the first error a film
// meets is the first in its order, whoever coded what.
type crew struct {
film *film
keys keyer
qindex int
own *painter
queue chan *job
Expand All @@ -104,19 +115,19 @@ type crew struct {
wg sync.WaitGroup
stopped atomic.Bool
closed bool
// encode stands in for painting a picture and coding it through gav1d,
// encode stands in for painting a tile and coding it through gav1d,
// told whether a helper is the one asking. Nil in every film - only
// CodeBeside sets it.
encode func(change int64, beside bool) (Coded, error)
encode func(change int64, beside bool) (tileCoded, error)
}

// newCrew is the crew for a film of this many pictures. A film of one picture
// takes no helper, and neither does a process of one thread.
func newCrew(f *film, qindex int, pictures int64) *crew {
c := &crew{film: f, qindex: qindex, own: f.painter()}
if most := min(int64(runtime.GOMAXPROCS(0)-1), pictures-1); most > 0 {
// 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()}
if most := min(int64(runtime.GOMAXPROCS(0)-1), jobs-1); most > 0 {
c.most = int(most)
c.queue = make(chan *job, c.ahead())
c.queue = make(chan *job, c.ahead()*len(ks.tiles))
}
return c
}
Expand All @@ -132,8 +143,8 @@ func (c *crew) ahead() int {
return 2 * (c.most + 1)
}

// offer hands a picture to the helpers, taking one more first when the film
// may have more and the process has a place for it. With no helper the
// offer hands a tile to the helpers, taking one more first when the film may
// have more and the process has a place for it. With no helper the
// writer codes it when it gets there. Called by the owning goroutine only.
func (c *crew) offer(j *job) {
if c.queue == nil || c.closed {
Expand All @@ -150,7 +161,7 @@ func (c *crew) offer(j *job) {
select {
case c.queue <- j:
default:
// The queue is as long as the window, so this is a picture the
// The queue holds every tile of the window, so this is a tile the
// writer will reach before any helper would have.
}
}
Expand Down Expand Up @@ -181,11 +192,12 @@ func (c *crew) code(p *painter, j *job, beside bool) {
j.coded, j.err = c.encode(j.change, beside)
return
}
j.coded, j.err = Encode(p.draw(j.look), c.qindex)
r := c.keys.tiles[j.tile].rect
j.coded, j.err = encodeTile(p.drawIn(j.look, r), r, c.qindex)
}

// wait is the picture of j, coded here when no helper has taken it.
func (c *crew) wait(j *job) (Coded, error) {
// wait is the tile of j, coded here when no helper has taken it.
func (c *crew) wait(j *job) (tileCoded, error) {
if j.taken.CompareAndSwap(false, true) {
c.code(c.own, j, false)
}
Expand All @@ -196,7 +208,7 @@ func (c *crew) wait(j *job) (Coded, error) {
return j.coded, j.err
}

// finish says no picture is coming after the ones offered, so the helpers end
// finish says no tile is coming after the ones offered, so the helpers end
// once the queue is empty and give their places back to the process.
func (c *crew) finish() {
if c.queue != nil && !c.closed {
Expand All @@ -206,64 +218,61 @@ func (c *crew) finish() {
}

// stop ends the crew and waits for its helpers. A helper in the middle of a
// picture finishes that picture - gav1d cannot be interrupted, and that was
// so before there were helpers - and codes nothing after it. Nothing a crew
// tile finishes that tile - gav1d cannot be interrupted, and that was so
// before there were helpers - and codes nothing after it. Nothing a crew
// started is running once this returns.
func (c *crew) stop() {
c.stopped.Store(true)
c.finish()
c.wg.Wait()
}

// all offers every job at once and gives back their pictures in order - a
// set known in advance, like the sample planning codes, rather than a film
// coded as it is written.
func (c *crew) all(jobs []*job) ([]Coded, error) {
// all offers every job at once and waits for each in order - a set known in
// advance, like the sample planning codes, rather than a film coded as it is
// written. Each job's tile is in the job once all returns.
func (c *crew) all(jobs []*job) error {
for _, j := range jobs {
c.offer(j)
}
c.finish()
out := make([]Coded, len(jobs))
for i, j := range jobs {
coded, err := c.wait(j)
if err != nil {
return nil, err
for _, j := range jobs {
if _, err := c.wait(j); err != nil {
return err
}
out[i] = coded
}
return out, nil
return nil
}

// CodeBeside codes pictures 0 to n-1 the way a film's are coded - this
// goroutine and the helpers it can take, offered ahead and taken in order -
// with code standing in for painting and gav1d, told whether a helper is the
// one calling, and gives back the size code returned for each.
// CodeBeside codes tiles 0 to n-1 the way a film's are coded - this goroutine
// and the helpers it can take, offered ahead and taken in order - with code
// standing in for painting and gav1d, told whether a helper is the one
// calling, and gives back the size code returned for each.
//
// It is for the guards, and only because what it lets them hold cannot be
// reached another way: a panic on a helper has to come out on the goroutine
// that asked for the picture, where the engine turns it into the run's error,
// that asked for the tile, where the engine turns it into the run's error,
// rather than end the process - and nothing outside makes gav1d panic.
// Measured on 2026-10-06: a quantizer out of range is refused with an error,
// and only planes shorter than the picture panic, which no painter makes.
func CodeBeside(n int64, code func(change int64, beside bool) (int, error)) ([]int, error) {
t := Timeline{FPS: 1, Frames: n, DurationMs: n * 1000, KeyEvery: n, ChangeEvery: 1}
c := newCrew(newFilm(1, 1, 0, "", t), 0, n)
f := newFilm(1, 1, 0, "", t)
c := newCrew(f, newKeyer(f.geometry, oneTile(1, 1)), 0, n)
defer c.stop()
c.encode = func(change int64, beside bool) (Coded, error) {
c.encode = func(change int64, beside bool) (tileCoded, error) {
size, err := code(change, beside)
return Coded{tile: make([]byte, size)}, err
return tileCoded{data: make([]byte, size)}, err
}
jobs := make([]*job, n)
for i := range jobs {
jobs[i] = newJob(look{}, int64(i))
jobs[i] = newJob(look{}, int64(i), 0)
}
coded, err := c.all(jobs)
if err != nil {
if err := c.all(jobs); err != nil {
return nil, err
}
sizes := make([]int, n)
for i, p := range coded {
sizes[i] = p.Size()
for i, j := range jobs {
sizes[i] = len(j.coded.data)
}
return sizes, nil
}
Loading
Loading