From 40773efb7beffa157078cc111cf2470f6f4e6ad7 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Mon, 28 Sep 2026 11:58:29 +0200 Subject: [PATCH] Add advanced control and block-for-target-time render options --- render.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ render_cgo.go | 36 ++++++++++++++++++---------------- render_nocgo.go | 27 ++++++++++++++++++-------- render_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 24 deletions(-) diff --git a/render.go b/render.go index 1b96443..0cea5ec 100644 --- a/render.go +++ b/render.go @@ -15,6 +15,57 @@ const ( // video frame should be rendered. const RenderUpdateFrame uint64 = 1 << 0 +// RenderContextOption configures a render context at creation. +type RenderContextOption func(*renderContextOptions) + +type renderContextOptions struct { + advancedControl bool +} + +// WithAdvancedControl sets MPV_RENDER_PARAM_ADVANCED_CONTROL. Call +// SetUpdateCallback right after creation; see the requirements in render.h. +func WithAdvancedControl() RenderContextOption { + return func(o *renderContextOptions) { o.advancedControl = true } +} + +func newRenderContextOptions(opts []RenderContextOption) renderContextOptions { + var o renderContextOptions + for _, opt := range opts { + opt(&o) + } + return o +} + +// RenderOption configures a RenderSW or RenderGL call. +type RenderOption func(*renderOptions) + +type renderOptions struct { + blockForTargetTime bool +} + +// WithoutBlockForTargetTime clears MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, so +// rendering returns without waiting for the frame's display time; pace frames +// yourself, see render.h. +func WithoutBlockForTargetTime() RenderOption { + return func(o *renderOptions) { o.blockForTargetTime = false } +} + +func newRenderOptions(opts []RenderOption) renderOptions { + o := renderOptions{blockForTargetTime: true} + for _, opt := range opts { + opt(&o) + } + return o +} + +// cFlag converts b to an mpv flag. +func cFlag(b bool) int32 { + if b { + return 1 + } + return 0 +} + // Render callbacks live in a token-keyed registry; the token is passed to C as the // callback context, so one trampoline dispatches without handing Go pointers to C. var ( diff --git a/render_cgo.go b/render_cgo.go index 4fdd46b..c1e7db5 100644 --- a/render_cgo.go +++ b/render_cgo.go @@ -12,27 +12,29 @@ package mpv void *goMpvGetProcAddress(void *ctx, const char *name); void goMpvRenderUpdate(void *ctx); -static int render_create_sw(mpv_render_context **ctx, mpv_handle *mpv) { +static int render_create_sw(mpv_render_context **ctx, mpv_handle *mpv, int advanced) { mpv_render_param params[] = { {MPV_RENDER_PARAM_API_TYPE, (void *)MPV_RENDER_API_TYPE_SW}, + {MPV_RENDER_PARAM_ADVANCED_CONTROL, &advanced}, {0}, }; return mpv_render_context_create(ctx, mpv, params); } -static int render_sw(mpv_render_context *ctx, int w, int h, const char *format, size_t stride, void *ptr) { +static int render_sw(mpv_render_context *ctx, int w, int h, const char *format, size_t stride, void *ptr, int block) { int size[2] = {w, h}; mpv_render_param params[] = { {MPV_RENDER_PARAM_SW_SIZE, &size[0]}, {MPV_RENDER_PARAM_SW_FORMAT, (void *)format}, {MPV_RENDER_PARAM_SW_STRIDE, &stride}, {MPV_RENDER_PARAM_SW_POINTER, ptr}, + {MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &block}, {0}, }; return mpv_render_context_render(ctx, params); } -static int render_create_gl(mpv_render_context **ctx, mpv_handle *mpv, uintptr_t cb_ctx) { +static int render_create_gl(mpv_render_context **ctx, mpv_handle *mpv, uintptr_t cb_ctx, int advanced) { mpv_opengl_init_params gl = { .get_proc_address = goMpvGetProcAddress, .get_proc_address_ctx = (void *)cb_ctx, @@ -40,16 +42,18 @@ static int render_create_gl(mpv_render_context **ctx, mpv_handle *mpv, uintptr_t mpv_render_param params[] = { {MPV_RENDER_PARAM_API_TYPE, (void *)MPV_RENDER_API_TYPE_OPENGL}, {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl}, + {MPV_RENDER_PARAM_ADVANCED_CONTROL, &advanced}, {0}, }; return mpv_render_context_create(ctx, mpv, params); } -static int render_gl(mpv_render_context *ctx, int fbo, int w, int h, int flip_y) { +static int render_gl(mpv_render_context *ctx, int fbo, int w, int h, int flip_y, int block) { mpv_opengl_fbo gl_fbo = {.fbo = fbo, .w = w, .h = h}; mpv_render_param params[] = { {MPV_RENDER_PARAM_OPENGL_FBO, &gl_fbo}, {MPV_RENDER_PARAM_FLIP_Y, &flip_y}, + {MPV_RENDER_PARAM_BLOCK_FOR_TARGET_TIME, &block}, {0}, }; return mpv_render_context_render(ctx, params); @@ -73,11 +77,12 @@ type RenderContext struct { // NewRenderContextSW creates a software (CPU) render context. The mpv instance // must have the "vo" option set to "libmpv". -func (m *Mpv) NewRenderContextSW() (*RenderContext, error) { +func (m *Mpv) NewRenderContextSW(opts ...RenderContextOption) (*RenderContext, error) { + o := newRenderContextOptions(opts) id := registerRenderCallbacks() var ctx *C.mpv_render_context - err := newError(int(C.render_create_sw(&ctx, m.handle))) + err := newError(int(C.render_create_sw(&ctx, m.handle, C.int(cFlag(o.advancedControl))))) if err != nil { unregisterRenderCallbacks(id) return nil, err @@ -88,12 +93,13 @@ func (m *Mpv) NewRenderContextSW() (*RenderContext, error) { // NewRenderContextGL creates an OpenGL render context; getProcAddress resolves GL // functions. Requires vo=libmpv and the GL context current on the calling thread. -func (m *Mpv) NewRenderContextGL(getProcAddress func(name string) unsafe.Pointer) (*RenderContext, error) { +func (m *Mpv) NewRenderContextGL(getProcAddress func(name string) unsafe.Pointer, opts ...RenderContextOption) (*RenderContext, error) { + o := newRenderContextOptions(opts) id := registerRenderCallbacks() setRenderProcAddress(id, getProcAddress) var ctx *C.mpv_render_context - err := newError(int(C.render_create_gl(&ctx, m.handle, C.uintptr_t(id)))) + err := newError(int(C.render_create_gl(&ctx, m.handle, C.uintptr_t(id), C.int(cFlag(o.advancedControl))))) if err != nil { unregisterRenderCallbacks(id) return nil, err @@ -104,22 +110,20 @@ func (m *Mpv) NewRenderContextGL(getProcAddress func(name string) unsafe.Pointer // RenderSW renders the current frame into buf, which must hold stride*height // bytes. format is one of "rgb0", "bgr0", "0bgr", "0rgb". -func (rc *RenderContext) RenderSW(width, height, stride int, format string, buf []byte) error { +func (rc *RenderContext) RenderSW(width, height, stride int, format string, buf []byte, opts ...RenderOption) error { + o := newRenderOptions(opts) cformat := C.CString(format) defer C.free(unsafe.Pointer(cformat)) - return newError(int(C.render_sw(rc.ctx, C.int(width), C.int(height), cformat, C.size_t(stride), unsafe.Pointer(&buf[0])))) + return newError(int(C.render_sw(rc.ctx, C.int(width), C.int(height), cformat, C.size_t(stride), unsafe.Pointer(&buf[0]), C.int(cFlag(o.blockForTargetTime))))) } // RenderGL renders the current frame into the given OpenGL framebuffer object // (0 for the default framebuffer). Set flipY for bottom-up coordinate systems. -func (rc *RenderContext) RenderGL(fbo, width, height int, flipY bool) error { - var flip C.int - if flipY { - flip = 1 - } +func (rc *RenderContext) RenderGL(fbo, width, height int, flipY bool, opts ...RenderOption) error { + o := newRenderOptions(opts) - return newError(int(C.render_gl(rc.ctx, C.int(fbo), C.int(width), C.int(height), flip))) + return newError(int(C.render_gl(rc.ctx, C.int(fbo), C.int(width), C.int(height), C.int(cFlag(flipY)), C.int(cFlag(o.blockForTargetTime))))) } // SetUpdateCallback sets fn to run when a new frame is ready. fn runs on an mpv diff --git a/render_nocgo.go b/render_nocgo.go index b4359e4..3f4092b 100644 --- a/render_nocgo.go +++ b/render_nocgo.go @@ -15,6 +15,8 @@ const ( renderParamOpenGLInitParams = 2 renderParamOpenGLFBO = 3 renderParamFlipY = 4 + renderParamAdvancedControl = 10 + renderParamBlockForTarget = 12 renderParamSWSize = 17 renderParamSWFormat = 18 renderParamSWStride = 19 @@ -81,12 +83,15 @@ type RenderContext struct { // NewRenderContextSW creates a software (CPU) render context. The mpv instance // must have the "vo" option set to "libmpv". -func (m *Mpv) NewRenderContextSW() (*RenderContext, error) { +func (m *Mpv) NewRenderContextSW(opts ...RenderContextOption) (*RenderContext, error) { + o := newRenderContextOptions(opts) id := registerRenderCallbacks() apiType := cStr(RenderAPITypeSW) + advanced := cFlag(o.advancedControl) params := []cRenderParam{ {typ: renderParamAPIType, data: unsafe.Pointer(apiType)}, + {typ: renderParamAdvancedControl, data: unsafe.Pointer(&advanced)}, {}, } @@ -102,16 +107,19 @@ func (m *Mpv) NewRenderContextSW() (*RenderContext, error) { // NewRenderContextGL creates an OpenGL render context; getProcAddress resolves GL // functions. Requires vo=libmpv and the GL context current on the calling thread. -func (m *Mpv) NewRenderContextGL(getProcAddress func(name string) unsafe.Pointer) (*RenderContext, error) { +func (m *Mpv) NewRenderContextGL(getProcAddress func(name string) unsafe.Pointer, opts ...RenderContextOption) (*RenderContext, error) { + o := newRenderContextOptions(opts) ensureRenderCallbacks() id := registerRenderCallbacks() setRenderProcAddress(id, getProcAddress) apiType := cStr(RenderAPITypeOpenGL) gl := cOpenGLInitParams{getProcAddress: procAddrCb, ctx: id} + advanced := cFlag(o.advancedControl) params := []cRenderParam{ {typ: renderParamAPIType, data: unsafe.Pointer(apiType)}, {typ: renderParamOpenGLInitParams, data: unsafe.Pointer(&gl)}, + {typ: renderParamAdvancedControl, data: unsafe.Pointer(&advanced)}, {}, } @@ -127,15 +135,18 @@ func (m *Mpv) NewRenderContextGL(getProcAddress func(name string) unsafe.Pointer // RenderSW renders the current frame into buf, which must hold stride*height // bytes. format is one of "rgb0", "bgr0", "0bgr", "0rgb". -func (rc *RenderContext) RenderSW(width, height, stride int, format string, buf []byte) error { +func (rc *RenderContext) RenderSW(width, height, stride int, format string, buf []byte, opts ...RenderOption) error { + o := newRenderOptions(opts) size := [2]int32{int32(width), int32(height)} cformat := cStr(format) cstride := uintptr(stride) + block := cFlag(o.blockForTargetTime) params := []cRenderParam{ {typ: renderParamSWSize, data: unsafe.Pointer(&size[0])}, {typ: renderParamSWFormat, data: unsafe.Pointer(cformat)}, {typ: renderParamSWStride, data: unsafe.Pointer(&cstride)}, {typ: renderParamSWPointer, data: unsafe.Pointer(&buf[0])}, + {typ: renderParamBlockForTarget, data: unsafe.Pointer(&block)}, {}, } @@ -144,15 +155,15 @@ func (rc *RenderContext) RenderSW(width, height, stride int, format string, buf // RenderGL renders the current frame into the given OpenGL framebuffer object // (0 for the default framebuffer). Set flipY for bottom-up coordinate systems. -func (rc *RenderContext) RenderGL(fbo, width, height int, flipY bool) error { +func (rc *RenderContext) RenderGL(fbo, width, height int, flipY bool, opts ...RenderOption) error { + o := newRenderOptions(opts) gl := cOpenGLFBO{fbo: int32(fbo), w: int32(width), h: int32(height)} - flip := int32(0) - if flipY { - flip = 1 - } + flip := cFlag(flipY) + block := cFlag(o.blockForTargetTime) params := []cRenderParam{ {typ: renderParamOpenGLFBO, data: unsafe.Pointer(&gl)}, {typ: renderParamFlipY, data: unsafe.Pointer(&flip)}, + {typ: renderParamBlockForTarget, data: unsafe.Pointer(&block)}, {}, } diff --git a/render_test.go b/render_test.go index 5bf5d0b..fb0be0c 100644 --- a/render_test.go +++ b/render_test.go @@ -105,3 +105,49 @@ func TestRenderUpdateCallback(t *testing.T) { t.Fatal("update callback was not called") } + +// TestRenderAdvancedControl renders with both render options set. +func TestRenderAdvancedControl(t *testing.T) { + m := New() + defer m.TerminateDestroy() + + if err := m.SetOptionString("vo", "libmpv"); err != nil { + t.Fatalf("set vo=libmpv: %v", err) + } + if err := m.SetOptionString("ao", "null"); err != nil { + t.Fatalf("set ao=null: %v", err) + } + if err := m.Initialize(); err != nil { + t.Fatalf("Initialize: %v", err) + } + + rc, err := m.NewRenderContextSW(WithAdvancedControl()) + if err != nil { + t.Fatalf("NewRenderContextSW: %v", err) + } + defer rc.Free() + // Required with advanced control. + rc.SetUpdateCallback(func() {}) + + if err := m.Command([]string{"loadfile", "testdata/test.mpg"}); err != nil { + t.Fatalf("loadfile: %v", err) + } + + const w, h, stride = 320, 240, 320 * 4 + buf := make([]byte, stride*h) + for i := 0; i < 200; i++ { + if rc.Update()&RenderUpdateFrame != 0 { + if err := rc.RenderSW(w, h, stride, "rgb0", buf, WithoutBlockForTargetTime()); err != nil { + t.Fatalf("RenderSW: %v", err) + } + for _, b := range buf { + if b != 0 { + return + } + } + } + m.WaitEvent(0.05) + } + + t.Fatal("no non-black frame rendered") +}