Skip to content
Open
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
17 changes: 17 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# History

## [0.66.0] - 2026-08-22

The I-frame size lever the Star6E firmware actually honours gets exposed.
Contract `0.18.2` → `0.18.3`.

- **`video0.maxIpProp`** (live, CBR only): exposes the RC I-to-P frame size
proportion cap (`u32MaxIPProp`). The byte caps (`maxIBytes`/`maxPBytes`)
are dead on Star6E firmware — probed live 2026-08-22, identical 42-44 KB
IDRs with caps at 2000, 26000, and 8 — so the proportion cap is the only
supported mechanism for bounding I-frame size under CBR. Range
`0..100`; `0` = SDK default, and the first write captures the driver
default so `0` can restore it live, mirroring the `minQp`/`maxQp`
pattern. Validated like `minQp`: out-of-range values and a non-zero
value under a non-CBR `rcMode` are rejected with `409` (and at config
load), so the backend apply never sees them. Applied at boot when
persisted, live via `/set` and `/live/set`, on Star6E and Maruko.

## [0.65.2] - 2026-08-15

CV610 gets a scaler, one mode table instead of five copies of it, and a
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.65.2
0.66.0
1 change: 1 addition & 0 deletions config/waybeam.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"qpDelta": -4,
"maxIBytes": 0,
"maxPBytes": 0,
"maxIpProp": 0,
"minQp": 0,
"maxQp": 0,
"sceneThreshold": 0,
Expand Down
14 changes: 13 additions & 1 deletion documentation/HTTP_API_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- `read_only` — cannot be changed via API.

## Contract Version
- `contract_version`: `0.18.2`
- `contract_version`: `0.18.3`
- `status`: `active`

## Governance Rules
Expand Down Expand Up @@ -1694,9 +1694,21 @@ divergence is listed. As of `contract_version: 0.12.1`:
| `detect.model_path` / `model_id` / `conf_thresh` / `nms_iou` | **live** | **live** | Both backends hot-swap the NPU detector on the pipeline thread without respawning video. Star6E uses VPE port 1; Maruko uses SCL port 3 and its drain-while-disable teardown. A model whose reported input geometry disagrees with the configured tap is refused and leaves detection off. |
| `detect.net_width` / `net_height` | restart | restart | Tap geometry is fixed when the VPE/SCL detector port is created. |
| `video0.min_qp` / `max_qp` | live | **501** | Star6E-only RC QP bounds; Maruko capabilities report these fields unsupported. |
| `video0.max_ip_prop` | live | live | RC I/P size proportion cap, `0..100`, `0` = driver default; CBR only (non-CBR writes and out-of-range values are rejected with 409 at validation). CV610 reports it unsupported. |
| `isp.aeEngine` ("sdk" only) | applied | applied | Unified AE selector landed in 0.10.13. `custom` (userspace AE governor) is RETIRED — Maruko in 0.22.0, Star6E in 0.47.0 — and the value was **removed** in 0.47.0. `sdk` is the only accepted value; any other (e.g. a stale `custom`) warns and falls back to `sdk`. Both backends run the SDK firmware/bin AE for convergence plus a supervisory thread that enforces the `isp.gain*`/`isp.shutter*` limits. |

## Change Log (Contract)
- `0.18.3` (additive — `video0.maxIpProp`):
- **`video0.maxIpProp`** (canonical `video0.max_ip_prop`, live): RC cap on
the I-to-P frame size proportion (`u32MaxIPProp`), range `0..100`,
`0` = SDK default. CBR only — a non-zero value with any other
`video0.rc_mode`, or a value above 100, is rejected with `409` by the
common validator (`/set`, `/live/set`, and persisted configs at load)
and never reaches the backend. This is the
mechanism the SigmaStar CBR rate controller actually honours for
I-frame size; `maxIBytes`/`maxPBytes` are ignored by the Star6E RC
(probed 2026-08-22: identical 42-44 KB IDRs at caps 2000, 26000, 8).
Star6E and Maruko; CV610 reports the field unsupported.
- `0.18.2` (additive — CV610 `video0.size` becomes a real control):
- **`video0.size` on CV610 now accepts any geometry the mode can be scaled
down to**, not just the capture size. VI has no scaler, so the backend
Expand Down
5 changes: 5 additions & 0 deletions include/venc_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ typedef struct {
* struct and set RC priority to FRAMEBITS_FIRST when either is >0.
* Both values in bytes; 0 = unlimited (restores BITRATE_FIRST). */
int (*apply_max_frame_size)(uint32_t max_i_bytes, uint32_t max_p_bytes);
/* Live-update the RC I-to-P frame size proportion cap (u32MaxIPProp).
* 0 restores the driver default. Unlike the byte caps above, this is
* the knob the SigmaStar CBR rate controller actually honours for
* I-frame size. NULL on backends without the RC param. */
int (*apply_max_ip_prop)(uint32_t prop);
/* Live-update the RC QP bounds (MinQp/MaxQp) in the RC param struct.
* 0 leaves that bound at whatever the driver reported, so a config
* that sets neither behaves exactly as before. Unlike the frame-size
Expand Down
6 changes: 6 additions & 0 deletions include/venc_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ typedef struct {
int qp_delta; /* relative I/P QP delta, -12..12 */
uint32_t max_i_bytes; /* per-frame I-frame size cap (bytes); 0=unlimited */
uint32_t max_p_bytes; /* per-frame P-frame size cap (bytes); 0=unlimited */
/* RC I-to-P frame size proportion cap (u32MaxIPProp); 0 = driver
* default. This is the CBR mechanism the SigmaStar firmware actually
* honours for I-frame size — maxIBytes/maxPBytes are dead on Star6E
* (identical 42-44 KB IDRs at caps 2000, 26000, and 8, probed
* 2026-08-22). */
uint32_t max_ip_prop;
uint32_t min_qp; /* RC QP floor; 0 = leave the driver default */
uint32_t max_qp; /* RC QP ceiling; 0 = leave the driver default */
uint16_t scene_threshold; /* frame size spike ratio x100 for scene IDR (0=off, 150=1.5x) */
Expand Down
51 changes: 51 additions & 0 deletions src/maruko_controls.c
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ typedef struct {
MI_SYS_ChnPort_t venc_port;
volatile sig_atomic_t *output_enabled_ptr;
volatile uint32_t *stored_fps_ptr;
/* Driver-default I/P proportion, captured on the first RC-param read
* before any write so a later 0 can restore it — see the matching
* note in src/star6e_controls.c. */
struct {
int captured;
uint32_t prop;
} ip_prop_default;
} MarukoControlContext;

enum {
Expand Down Expand Up @@ -387,6 +394,49 @@ static int maruko_apply_max_frame_size(uint32_t max_i_bytes, uint32_t max_p_byte
return 0;
}

static int maruko_apply_max_ip_prop(uint32_t prop)
{
i6c_venc_chn attr = {0};
MI_VENC_RcParam_t param = {0};
uint32_t *pprop;

if (prop == 0 && !g_ctx.ip_prop_default.captured)
return 0; /* never written — driver default already in force */
if (maruko_mi_venc_get_chn_attr(g_ctx.venc_dev,
g_ctx.venc_chn, &attr) != 0)
return -1;
if (maruko_mi_venc_get_rc_param(g_ctx.venc_dev,
g_ctx.venc_chn, &param) != 0)
return -1;

/* CBR only — the proportion cap is how CBR bounds I-frame size;
* VBR/AVBR bound them through maxBitrate instead. validate_field_cfg()
* rejects non-CBR writes with 409 before we get here. */
switch (attr.rate.mode) {
case MARUKO_VENC_RC_H265_CBR:
pprop = &param.stParamH265Cbr.u32MaxIPProp;
break;
case MARUKO_VENC_RC_H264_CBR:
pprop = &param.stParamH264Cbr.u32MaxIPProp;
break;
default:
return -1;
}

if (!g_ctx.ip_prop_default.captured) {
g_ctx.ip_prop_default.prop = *pprop;
g_ctx.ip_prop_default.captured = 1;
}
*pprop = prop ? prop : g_ctx.ip_prop_default.prop;

if (maruko_mi_venc_set_rc_param(g_ctx.venc_dev,
g_ctx.venc_chn, &param) != 0)
return -1;
printf("> maxIpProp changed to %u (0 = driver default %u)\n",
prop, g_ctx.ip_prop_default.prop);
return 0;
}

static int maruko_apply_fps(uint32_t fps)
{
i6c_venc_chn attr = {0};
Expand Down Expand Up @@ -1497,6 +1547,7 @@ static const VencApplyCallbacks g_maruko_apply_cb = {
.apply_iq_param = maruko_iq_set,
.apply_max_payload_size = maruko_apply_max_payload_size,
.apply_max_frame_size = maruko_apply_max_frame_size,
.apply_max_ip_prop = maruko_apply_max_ip_prop,
.query_transport_status = maruko_query_transport_status,
.query_audio_status = maruko_query_audio_status,
.apply_zoom = maruko_apply_zoom,
Expand Down
5 changes: 5 additions & 0 deletions src/maruko_runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ static int maruko_runner_init(void *opaque)
ctx->vcfg.video0.max_i_bytes,
ctx->vcfg.video0.max_p_bytes);
}
if (ctx->vcfg.video0.max_ip_prop > 0 &&
maruko_controls_callbacks()->apply_max_ip_prop) {
maruko_controls_callbacks()->apply_max_ip_prop(
ctx->vcfg.video0.max_ip_prop);
}

return 0;
}
Expand Down
50 changes: 50 additions & 0 deletions src/star6e_controls.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ typedef struct {
uint32_t frame_height;
Star6ePipelineState *pipeline;
VencConfig *vcfg;
/* Driver-default I/P proportion, captured on the first RC-param read
* before any write so a later 0 can restore it — same lifecycle
* argument as g_qp_defaults: the rate mode is fixed for the process
* lifetime, so one capture is enough. */
struct {
int captured;
uint32_t prop;
} ip_prop_default;
} Star6eControlContext;

typedef struct {
Expand Down Expand Up @@ -489,6 +497,47 @@ static int apply_max_frame_size(uint32_t max_i_bytes, uint32_t max_p_bytes)
return 0;
}

static int apply_max_ip_prop(uint32_t prop)
{
MI_VENC_ChnAttr_t attr = {0};
MI_VENC_RcParam_t param = {0};
uint32_t *pprop;

if (prop == 0 && !g_star6e_control_ctx.ip_prop_default.captured)
return 0; /* never written — driver default already in force */
if (MI_VENC_GetChnAttr(g_star6e_control_ctx.venc_chn, &attr) != 0)
return -1;
if (MI_VENC_GetRcParam(g_star6e_control_ctx.venc_chn, &param) != 0)
return -1;

/* CBR only: the proportion cap is how CBR bounds I-frame size; VBR/AVBR
* carry the field too but bound I-frames through maxBitrate instead.
* validate_field_cfg() rejects non-CBR writes with 409 before we get
* here, so the default arm is a guard, not the user-facing error. */
switch (attr.rate.mode) {
case I6_VENC_RATEMODE_H265CBR:
pprop = &param.stParamH265Cbr.u32MaxIPProp;
break;
case I6_VENC_RATEMODE_H264CBR:
pprop = &param.stParamH264Cbr.u32MaxIPProp;
break;
default:
return -1;
Comment thread
vertexodessa marked this conversation as resolved.
}

if (!g_star6e_control_ctx.ip_prop_default.captured) {
g_star6e_control_ctx.ip_prop_default.prop = *pprop;
g_star6e_control_ctx.ip_prop_default.captured = 1;
}
*pprop = prop ? prop : g_star6e_control_ctx.ip_prop_default.prop;

if (MI_VENC_SetRcParam(g_star6e_control_ctx.venc_chn, &param) != 0)
return -1;
printf("> maxIpProp changed to %u (0 = driver default %u)\n",
prop, g_star6e_control_ctx.ip_prop_default.prop);
return 0;
}

static int apply_encoder_fps(uint32_t fps)
{
MI_VENC_ChnAttr_t attr = {0};
Expand Down Expand Up @@ -1777,6 +1826,7 @@ static const VencApplyCallbacks g_star6e_apply_callbacks = {
.apply_zoom = apply_zoom,
.apply_isp_bin = apply_isp_bin,
.apply_max_frame_size = apply_max_frame_size,
.apply_max_ip_prop = apply_max_ip_prop,
.apply_snapshot_quality = venc_jpeg_set_quality,
.apply_pause_stab = apply_pause_stab,
.query_attitude = query_attitude,
Expand Down
5 changes: 5 additions & 0 deletions src/star6e_runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,11 @@ static int star6e_runtime_apply_startup_controls(Star6eRunnerContext *ctx)
cb->apply_max_frame_size(vcfg->video0.max_i_bytes,
vcfg->video0.max_p_bytes);
}
if (vcfg->video0.max_ip_prop > 0) {
const VencApplyCallbacks *cb = star6e_controls_callbacks();
if (cb->apply_max_ip_prop)
cb->apply_max_ip_prop(vcfg->video0.max_ip_prop);
}

if (!ps->output_enabled) {
ps->stored_fps = vcfg->video0.fps;
Expand Down
40 changes: 39 additions & 1 deletion src/venc_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,14 @@ static const FieldUi ui_max_p_bytes = {
"the cap becomes a hard ceiling; both back to 0 restores bitrate-first. "
"An IDR is requested after each apply. Applied live."
};
static const FieldUi ui_max_ip_prop = {
"Video", "Max I/P proportion", "number", 0, 100, 1, NULL,
"RC cap on the I-frame to P-frame size ratio (u32MaxIPProp). "
"0 = SDK default (permissive). This is the CBR mechanism the firmware "
"actually honours for I-frame size — the byte caps above are ignored "
"by the Star6E rate controller. Try 2-4 to tame IDR bursts. "
"Applied live."
};

/* UI descriptors for the snapshot subsystem. The whole section was API-only
* (no static SECTIONS rows), so /snapshot.jpg could not be enabled or tuned
Expand Down Expand Up @@ -566,6 +574,7 @@ static const FieldDesc g_fields[] = {
FIELD(video0, qp_delta, FT_INT, MUT_LIVE),
FIELD_UI(video0, max_i_bytes, FT_UINT, MUT_LIVE, &ui_max_i_bytes),
FIELD_UI(video0, max_p_bytes, FT_UINT, MUT_LIVE, &ui_max_p_bytes),
FIELD_UI(video0, max_ip_prop, FT_UINT, MUT_LIVE, &ui_max_ip_prop),
FIELD_UI(video0, min_qp, FT_UINT, MUT_LIVE, &ui_min_qp),
FIELD_UI(video0, max_qp, FT_UINT, MUT_LIVE, &ui_max_qp),
FIELD(outgoing, enabled, FT_BOOL, MUT_LIVE),
Expand Down Expand Up @@ -734,6 +743,7 @@ static const FieldAlias g_field_aliases[] = {
{ "video0.minQp", "video0.min_qp" },
{ "video0.maxQp", "video0.max_qp" },
{ "video0.maxPBytes", "video0.max_p_bytes" },
{ "video0.maxIpProp", "video0.max_ip_prop" },
{ "outgoing.maxPayloadSize", "outgoing.max_payload_size" },
{ "outgoing.audioPort", "outgoing.audio_port" },
{ "fpv.roiEnabled", "fpv.roi_enabled" },
Expand Down Expand Up @@ -1248,6 +1258,18 @@ static const char *validate_field_cfg(const VencConfig *cfg, const char *key)
cfg->video0.min_qp > cfg->video0.max_qp)
return "video0.min_qp must not exceed max_qp";
}
if (strcmp(key, "video0.max_ip_prop") == 0) {
/* u32MaxIPProp is a proportion in [1, 100]; 0 = driver default.
* The loader reads JSON integers before assigning the unsigned
* field, so a negative value wraps large and is caught by the
* upper bound. The rate-mode rule lives here rather than in the
* backend apply so a non-CBR write answers 409, not 500. */
if (cfg->video0.max_ip_prop > 100)
return "video0.max_ip_prop must be 0..100";
if (cfg->video0.max_ip_prop > 0 &&
strcmp(cfg->video0.rc_mode, "cbr") != 0)
return "video0.max_ip_prop requires video0.rc_mode=cbr";
}
if (strcmp(key, "snapshot.quality") == 0) {
/* JPEG q-factor range. Backend clamps internally too, but
* the validator gives a clean error response instead of a
Expand Down Expand Up @@ -1310,6 +1332,7 @@ const char *venc_api_validate_loaded_config(const VencConfig *cfg)
"video0.qp_delta",
"video0.min_qp",
"video0.max_qp",
"video0.max_ip_prop",
"video0.size",
"video0.scene_holdoff",
"video0.zoom_x",
Expand Down Expand Up @@ -1460,6 +1483,7 @@ typedef enum {
LIVE_GROUP_SNAPSHOT_QUALITY,
LIVE_GROUP_PAUSE_STAB,
LIVE_GROUP_MAX_FRAME_SIZE,
LIVE_GROUP_MAX_IP_PROP,
LIVE_GROUP_QP_BOUNDS,
LIVE_GROUP_DETECT,
LIVE_GROUP_SHM_THROTTLE,
Expand Down Expand Up @@ -1644,6 +1668,8 @@ static LiveApplyGroup live_group_for_key(const char *canonical_key)
if (strcmp(canonical_key, "video0.max_i_bytes") == 0 ||
strcmp(canonical_key, "video0.max_p_bytes") == 0)
return LIVE_GROUP_MAX_FRAME_SIZE;
if (strcmp(canonical_key, "video0.max_ip_prop") == 0)
return LIVE_GROUP_MAX_IP_PROP;
if (strcmp(canonical_key, "video0.min_qp") == 0 ||
strcmp(canonical_key, "video0.max_qp") == 0)
return LIVE_GROUP_QP_BOUNDS;
Expand Down Expand Up @@ -1700,6 +1726,8 @@ static const char *live_group_name(LiveApplyGroup group)
return "video0.pauseStab";
case LIVE_GROUP_MAX_FRAME_SIZE:
return "video0.maxIBytes/maxPBytes";
case LIVE_GROUP_MAX_IP_PROP:
return "video0.maxIpProp";
case LIVE_GROUP_QP_BOUNDS:
return "video0.minQp/maxQp";
case LIVE_GROUP_DETECT:
Expand Down Expand Up @@ -1884,6 +1912,8 @@ static int live_group_supported_for_cfg(const VencConfig *cfg,
return g_cb->apply_pause_stab != NULL;
case LIVE_GROUP_MAX_FRAME_SIZE:
return g_cb->apply_max_frame_size != NULL;
case LIVE_GROUP_MAX_IP_PROP:
return g_cb->apply_max_ip_prop != NULL;
case LIVE_GROUP_QP_BOUNDS:
return g_cb->apply_qp_bounds != NULL;
case LIVE_GROUP_DETECT:
Expand Down Expand Up @@ -1987,6 +2017,9 @@ static void copy_live_group_fields(VencConfig *dst, const VencConfig *src,
dst->video0.max_i_bytes = src->video0.max_i_bytes;
dst->video0.max_p_bytes = src->video0.max_p_bytes;
break;
case LIVE_GROUP_MAX_IP_PROP:
dst->video0.max_ip_prop = src->video0.max_ip_prop;
break;
case LIVE_GROUP_QP_BOUNDS:
dst->video0.min_qp = src->video0.min_qp;
dst->video0.max_qp = src->video0.max_qp;
Expand Down Expand Up @@ -2146,6 +2179,11 @@ static int apply_live_group_for_cfg(const VencConfig *cfg,
case LIVE_GROUP_MAX_FRAME_SIZE:
return g_cb->apply_max_frame_size(cfg->video0.max_i_bytes,
cfg->video0.max_p_bytes);
case LIVE_GROUP_MAX_IP_PROP:
/* Backends without the RC param leave the hook NULL. */
if (!g_cb->apply_max_ip_prop)
return -2;
return g_cb->apply_max_ip_prop(cfg->video0.max_ip_prop);
case LIVE_GROUP_QP_BOUNDS:
/* Backends without RC QP bounds leave the hook NULL. */
if (!g_cb->apply_qp_bounds)
Expand Down Expand Up @@ -2878,7 +2916,7 @@ static int handle_version(int fd, const HttpRequest *req, void *ctx)
snprintf(buf, sizeof(buf),
"{\"ok\":true,\"data\":{"
"\"app_version\":\"%s\","
"\"contract_version\":\"0.18.2\","
"\"contract_version\":\"0.18.3\","
"\"config_schema_version\":\"1.0.0\","
"\"backend\":\"%s\""
"}}", VENC_VERSION, g_backend);
Expand Down
Loading
Loading