diff options
Diffstat (limited to 'src')
31 files changed, 44219 insertions, 0 deletions
diff --git a/src/agent.zig b/src/agent.zig new file mode 100644 index 0000000..09b0994 --- /dev/null +++ b/src/agent.zig @@ -0,0 +1,3925 @@ +//! The Agent owns the conversation-driving loop: provider streaming + +//! tool dispatch. +//! +//! On each turn, after the provider streams an assistant message, the +//! agent inspects it for ToolUse blocks. If any are present, the agent: +//! +//! 1. Groups them by their *owning registration* in the registry — a +//! single `Tool` is its own group; every `ToolSource`-backed tool +//! whose name maps to the same source forms one group. +//! 2. Spawns one concurrent task per group via `std.Io.Group`. +//! A single-`Tool` group runs the tool's `invoke` once; a +//! `ToolSource` group calls the source's `invoke_batch` with all +//! of its calls at once. We use `Group.concurrent` (not `async`) +//! because tool invocations may block on I/O and we need real +//! concurrency, not just expressed asynchrony. +//! 3. Awaits the group. ToolResult blocks are assembled in the +//! *original* call order (i.e. the order the LLM emitted them). +//! 4. Appends a user message containing the ToolResult blocks back +//! into the conversation and loops. +//! +//! The "thread-safe" promise for single `Tool` registrations is +//! unchanged. For `ToolSource`-backed tools, the source's runtime +//! receives all of its calls on one thread per turn, so it can keep a +//! single-threaded interpreter (Lua, Python, ...) without further +//! synchronization. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Io = std.Io; + +const provider_mod = @import("provider.zig"); +const stream_mod = @import("stream.zig"); +const config_mod = @import("config.zig"); +const conversation = @import("conversation.zig"); +const compaction_mod = @import("compaction.zig"); +const tool_mod = @import("tool.zig"); +const image_mod = @import("image.zig"); +const tool_source_mod = @import("tool_source.zig"); +const tool_registry_mod = @import("tool_registry.zig"); +const session_store_mod = @import("session_store.zig"); +const null_store_mod = @import("null_store.zig"); +const turn_persist = @import("turn_persist.zig"); + +pub const Tool = tool_mod.Tool; +pub const ToolSource = tool_source_mod.ToolSource; +pub const ToolRegistry = tool_registry_mod.ToolRegistry; + +const Event = stream_mod.Event; + +const Entry = tool_registry_mod.Entry; + +pub const Config = config_mod.Config; + +/// Re-export for the `compact` usages parameter (provider-reported token +/// usage per message, used for retention sizing). +pub const conversation_Usage = @import("session.zig").Usage; + +/// Append a single-text user message. `Conversation.addUserMessage` now +/// takes a block slice (symmetric with `addAssistantMessage`); this wraps +/// the common plain-text case used by the agent's interactive turn and the +/// compaction prompt. +fn addUserText(conv: *conversation.Conversation, text: []const u8) !void { + const tb = try conversation.textualBlockFromSlice(conv.allocator, text); + var block: conversation.ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + +/// Deep-copy a message (role + all content blocks) into fresh owned +/// allocations. Used when rebuilding the conversation after compaction. +fn cloneMessage(alloc: Allocator, msg: conversation.Message) !conversation.Message { + var content: std.ArrayList(conversation.ContentBlock) = .empty; + errdefer { + for (content.items) |*b| b.deinit(alloc); + content.deinit(alloc); + } + try content.ensureTotalCapacity(alloc, msg.content.items.len); + for (msg.content.items) |block| { + content.appendAssumeCapacity(try cloneBlock(alloc, block)); + } + return .{ + .role = msg.role, + .content = content, + .usage = msg.usage, + .metadata = if (msg.metadata) |m| try alloc.dupe(u8, m) else null, + // Preserve the producing identity so a kept-verbatim turn isn't + // re-stamped with the compaction model on persist. + .identity = if (msg.identity) |id| try conversation.dupeWireIdentity(alloc, id) else null, + }; +} + +fn cloneBlock(alloc: Allocator, block: conversation.ContentBlock) !conversation.ContentBlock { + return switch (block) { + .Text => |b| .{ .Text = try conversation.textualBlockFromSlice(alloc, b.items) }, + .Thinking => |b| blk: { + const tb = try conversation.textualBlockFromSlice(alloc, b.text.items); + errdefer { + var mut = tb; + mut.deinit(alloc); + } + const sig: ?[]const u8 = if (b.signature) |s| try alloc.dupe(u8, s) else null; + errdefer if (sig) |s| alloc.free(s); + const origin: ?conversation.SignatureOrigin = if (b.signature_origin) |o| try o.dupe(alloc) else null; + break :blk .{ .Thinking = .{ .text = tb, .signature = sig, .signature_origin = origin } }; + }, + .ToolUse => |b| blk: { + const id = try alloc.dupe(u8, b.id); + errdefer alloc.free(id); + const name = try alloc.dupe(u8, b.name); + errdefer alloc.free(name); + const input = try conversation.textualBlockFromSlice(alloc, b.input.items); + break :blk .{ .ToolUse = .{ .id = id, .name = name, .input = input } }; + }, + .ToolResult => |b| blk: { + const tuid = try alloc.dupe(u8, b.tool_use_id); + errdefer alloc.free(tuid); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + errdefer { + for (parts.items) |*p| p.deinit(alloc); + parts.deinit(alloc); + } + try parts.ensureTotalCapacity(alloc, b.parts.items.len); + for (b.parts.items) |src| { + switch (src) { + .text => |tb| { + const t = try conversation.textualBlockFromSlice(alloc, tb.items); + parts.appendAssumeCapacity(.{ .text = t }); + }, + .media => |m| { + const mt = try alloc.dupe(u8, m.media_type); + errdefer alloc.free(mt); + const data = try conversation.textualBlockFromSlice(alloc, m.data.items); + parts.appendAssumeCapacity(.{ .media = .{ .media_type = mt, .data = data } }); + }, + } + } + break :blk .{ .ToolResult = .{ .tool_use_id = tuid, .parts = parts, .is_error = b.is_error } }; + }, + .System => |b| .{ .System = .{ + .text = try conversation.textualBlockFromSlice(alloc, b.text.items), + .mode = b.mode, + } }, + .CompactionSummary => |b| .{ .CompactionSummary = .{ + .text = try conversation.textualBlockFromSlice(alloc, b.text.items), + } }, + }; +} + +fn isValidToolInput(input: []const u8) bool { + if (input.len == 0) return true; + if (input[0] != '{') return true; // legacy tests/tools may use opaque bytes + var parsed = std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, input, .{}) catch return false; + defer parsed.deinit(); + return parsed.value == .object; +} + +fn invalidInputResult(allocator: Allocator, input: []const u8) !tool_mod.ResultParts { + const msg = try std.fmt.allocPrint( + allocator, + "Tool call was not executed: tool input was incomplete or invalid JSON. Partial input: {s}", + .{input}, + ); + return tool_mod.ResultParts.fromTextOwned(allocator, msg); +} + +/// What to do with an error returned by tool dispatch. +const ToolErrorAction = enum { + /// Surface the failure to the model as an error `ToolResult`, then let + /// the agent loop continue. + tool_result, + /// Abort the whole turn and propagate to the embedder. Reserved for + /// failures that belong to the host, not the model/provider exchange. + hard_fail, +}; + +/// Decide how to handle a tool dispatch error. Only genuine host failures +/// abort the turn; everything else becomes a model-visible tool result so +/// the model can correct course (and so every `ToolUse` keeps its matching +/// `ToolResult`, which providers require). +fn classifyToolError(err: anyerror) ToolErrorAction { + return switch (err) { + error.Canceled, error.OutOfMemory => .hard_fail, + else => .tool_result, + }; +} + +/// Build an error `ResultPart` describing a failed tool call, in the +/// model-readable form the plan specifies. +fn toolErrorResult( + allocator: Allocator, + tool_name: []const u8, + err: anyerror, +) !tool_mod.ResultParts { + const msg = try std.fmt.allocPrint( + allocator, + "Tool execution failed for `{s}`: {s}\n" ++ + "You may fix the arguments, try a different tool, or explain the failure to the user.", + .{ tool_name, @errorName(err) }, + ); + return tool_mod.ResultParts.fromTextOwned(allocator, msg); +} + +/// The user's submission that opens a turn: an ordered list of content +/// blocks (text, tool results, images, …), the same shape used to rebuild a +/// persisted user message. Ownership of `blocks` transfers to the agent's +/// conversation on `run`; the caller must not deinit them afterwards. A plain +/// chat turn is one `.Text` block; a turn that resumes after the embedder +/// handled tool calls is one or more `.ToolResult` blocks. +pub const UserMessage = struct { + blocks: []const conversation.ContentBlock, +}; + +/// Outcome of a compaction attempt. +pub const CompactionResult = struct { + /// Whether the conversation was actually compacted. False means the + /// active conversation already fit within the keep-verbatim budget + /// (nothing to summarize) — the conversation is unchanged. + compacted: bool, + /// Number of whole turns kept verbatim after the summary. + kept_turns: usize = 0, + /// Number of conversation messages folded into the summary. + summarized_messages: usize = 0, +}; + +pub const Agent = struct { + _allocator: Allocator, + _io: Io, + /// The active configuration snapshot, consulted fresh at the top of + /// every turn. Immutable while a turn is in flight; swap this pointer + /// (`setConfig`) between turns to change provider/model/base_url + /// atomically. The pointee is owned by the embedder, not the agent. The + /// tool set is no longer part of the snapshot — it lives on `_registry`. + _config: *const Config, + /// The tool set this agent exposes. Owned by the agent: created empty at + /// `init`, populated via `registerTool`/`registerToolSource`, torn down + /// in `deinit`. Read fresh by the agent loop each turn, so a + /// registration between turns is visible at the next turn boundary. + _registry: ToolRegistry, + /// The live conversation the agent drives. Owned by the agent (adopted + /// at `init`); torn down in `deinit`. Turn-driving methods operate on + /// this directly rather than taking a `*Conversation` parameter. + /// + /// This is the one intended-public field (every other field is + /// underscore-prefixed internal state): a borrowed handle on the live + /// conversation, for in-place context-management surgery. Valid for the + /// agent's lifetime; do not retain past it. + conversation: conversation.Conversation, + /// The session this agent appends to. Minted from the store at `init` + /// (fresh: `store.create()`) or supplied by the embedder on resume + /// (`resolve`/`latest`). `Session.append` proxies to the store and + /// updates the session's last-used wire identity. The embedder owns the + /// underlying store, which must outlive the agent. + _session: session_store_mod.Session, + /// Injectable streaming seam. Defaults to the real provider dispatch + /// (`provider_mod.openStream`); tests override it with a stub. + _open_stream_fn: provider_mod.OpenStreamFn = provider_mod.openStream, + /// Set by the embedder after `runStep` returns to learn whether an + /// automatic compaction occurred this turn (so it can persist the + /// rewritten conversation). Reset at the top of each `runStep`. + _auto_compacted: bool = false, + /// PRNG state for backoff jitter. Seeded lazily on first retry. Only + /// touched from the single agent-loop thread (retries are serial), so + /// no synchronization is needed. + _retry_prng: ?std.Random.DefaultPrng = null, + /// High-water mark of messages durably handed to the store: the count of + /// conversation messages already persisted. `flushPersist` appends only + /// `[_persisted_through .. coherent_end)` and advances this, so the store + /// stays current at every `Stream.next()` boundary without re-appending. + /// Initialized to the loaded history length (already persisted) and + /// rewound by `rewriteWithSummary` so a compaction re-persists its summary + /// + restated suffix. + _persisted_through: usize = 0, + /// User-message blocks queued by an embedder seam (the CLI's + /// `panto.ext.agent:submit`) for the host front end to open the next + /// turn with, so the turn runs on the host's own driver (rendering, + /// interrupts) rather than inside the queuing callback. Blocks and the + /// list storage are owned by `conversation.allocator` — the same + /// ownership `run` adopts. + _pending_submission: std.ArrayList(conversation.ContentBlock) = .empty, + + /// Construct an agent. + /// + /// `store` is the persistence backend (use `null_store.store()` to opt + /// out). `maybe_conversation` is adopted (ownership transferred) when + /// non-null — the resume path: open a store, ask it for the + /// conversation, hand it here. When null, a fresh empty conversation is + /// created. Either way the agent owns and tears down the conversation. + /// The inner is heap-pinned at `init`, so the returned `*Agent` is a + /// cheap, movable handle ("don't move the Agent" stops being a rule + /// anyone can violate). The caller owns it and must `deinit` it. + pub fn init( + allocator: Allocator, + io: Io, + config: *const Config, + session: session_store_mod.Session, + maybe_conversation: ?conversation.Conversation, + ) !*Agent { + const self = try allocator.create(Agent); + self.* = .{ + ._allocator = allocator, + ._io = io, + ._config = config, + ._registry = ToolRegistry.init(allocator), + .conversation = maybe_conversation orelse conversation.Conversation.init(allocator), + ._session = session, + }; + // Loaded history is already in the store; only messages produced from + // here on need persisting. + self._persisted_through = self.conversation.messages.items.len; + return self; + } + + pub fn deinit(self: *Agent) void { + // The agent owns the conversation, the tool registry, and the + // session handle's `info` (minted by `store.create()` or resolved + // by the embedder and handed in). It borrows the config snapshot + // and the underlying store, which the embedder tears down. It is + // self-heap-pinned (`init` allocated it), so it frees itself last. + const allocator = self._allocator; + self._registry.deinit(); + for (self._pending_submission.items) |*b| b.deinit(self.conversation.allocator); + self._pending_submission.deinit(self.conversation.allocator); + self.conversation.deinit(); + self._session.info.deinit(allocator); + allocator.destroy(self); + } + + /// The id of the session this agent appends to. + pub fn sessionId(self: *const Agent) []const u8 { + return self._session.info.id; + } + + /// Add a single tool to this agent's tool set. Visible at the next turn. + pub fn registerTool(self: *Agent, tool: Tool) !void { + try self._registry.register(tool); + } + + /// Add a tool source (a dynamic group of tools) to this agent's tool + /// set. Visible at the next turn. + pub fn registerToolSource(self: *Agent, src: ToolSource) !void { + try self._registry.registerSource(src); + } + + /// The wire-format provider identity stamped on persisted entries, + /// derived from the active config snapshot. Ground truth: never a CLI + /// alias, never any `api_key` material. + fn wireIdentity(self: *const Agent) session_store_mod.WireIdentity { + return self._config.provider.wireIdentity(); + } + + /// Swap the active configuration snapshot. Takes effect at the start of + /// the next turn. Safe to call between `runStep` invocations or from a + /// tool handler that runs between provider steps; never mutates a + /// snapshot a turn is currently reading. + pub fn setConfig(self: *Agent, config: *const Config) void { + self._config = config; + } + + /// Add a system message (append or replace mode) to the conversation + /// and persist it. The persisted entry records the mode so replay + /// reconstructs the same effective system prompt. + /// Append a system message (`.append` mode) and persist it. Adds to the + /// effective system prompt. + pub fn addSystemMessage(self: *Agent, text: []const u8) !void { + return self._persistSystemMessage(text, .append); + } + + /// Replace the effective system prompt (`.replace` mode) and persist it. + /// Discards all prior system text; replay reconstructs the same prompt. + pub fn setSystemPrompt(self: *Agent, text: []const u8) !void { + return self._persistSystemMessage(text, .replace); + } + + fn _persistSystemMessage( + self: *Agent, + text: []const u8, + mode: conversation.SystemMode, + ) !void { + switch (mode) { + .append => try self.conversation.addSystemMessage(text), + .replace => try self.conversation.replaceSystemMessage(text), + } + self.flushPersist(); + } + + /// Replace the text of the ToolResult block matching `tool_use_id` in + /// the conversation's LAST message. Media parts are preserved (the new + /// text becomes the single leading text part); `is_error` is untouched. + /// Returns false when the last message holds no such block. + /// + /// This is the application half of the embedder's "tool_result output + /// override" (a `tool_result` event handler assigning `ev.output`): + /// called between tool dispatch and the next provider request, the + /// override becomes canonical — wire, persistence, and compaction all + /// see the replaced text. + pub fn overrideToolResultOutput(self: *Agent, tool_use_id: []const u8, text: []const u8) !bool { + const msgs = self.conversation.messages.items; + if (msgs.len == 0) return false; + const last = &msgs[msgs.len - 1]; + if (last.role != .user) return false; + for (last.content.items) |*block| { + if (block.* != .ToolResult) continue; + const tr = &block.ToolResult; + if (!std.mem.eql(u8, tr.tool_use_id, tool_use_id)) continue; + + var new_parts: std.ArrayList(conversation.ResultPartStored) = .empty; + errdefer new_parts.deinit(self._allocator); + try new_parts.ensureTotalCapacity(self._allocator, tr.parts.items.len + 1); + var new_text: conversation.TextualBlock = .empty; + try new_text.appendSlice(self._allocator, text); + errdefer new_text.deinit(self._allocator); + new_parts.appendAssumeCapacity(.{ .text = new_text }); + for (tr.parts.items) |*p| switch (p.*) { + .text => |*t| t.deinit(self._allocator), + .media => |m| new_parts.appendAssumeCapacity(.{ .media = m }), + }; + tr.parts.deinit(self._allocator); + tr.parts = new_parts; + return true; + } + return false; + } + + /// Replace the input JSON of the ToolUse block matching `tool_use_id` + /// in the conversation's LAST message (the committed assistant message, + /// between response completion and tool dispatch). Returns false when + /// no such block exists there. + /// + /// Application half of the "tool_call_complete input override": the + /// rewritten input is what the tool executes with AND what the model + /// sees of its own call in future requests. + pub fn overrideToolUseInput(self: *Agent, tool_use_id: []const u8, input_json: []const u8) !bool { + const msgs = self.conversation.messages.items; + if (msgs.len == 0) return false; + const last = &msgs[msgs.len - 1]; + if (last.role != .assistant) return false; + for (last.content.items) |*block| { + if (block.* != .ToolUse) continue; + const tu = &block.ToolUse; + if (!std.mem.eql(u8, tu.id, tool_use_id)) continue; + tu.input.clearRetainingCapacity(); + try tu.input.appendSlice(self._allocator, input_json); + return true; + } + return false; + } + + /// Submit a user message and begin a turn, returning a resumable pull + /// `Stream`. + /// + /// The user message is appended to the conversation and durably persisted + /// *immediately* (before any provider call), so a crash before the model + /// replies leaves a recoverable dangling prompt in the store. No provider + /// I/O happens here: the request opens lazily on the first + /// `Stream.next()`. + /// + /// The returned `*Stream` is heap-allocated; the caller owns it and must + /// `deinit` it. Persistence of whatever the turn committed runs when the + /// stream reaches its terminal `turn_complete` or is `deinit`ed early + /// (so a partial turn is still durably logged), mirroring the previous + /// `runStep` exit-path guarantee. + /// + /// The agent re-reads its `config` snapshot at the top of each provider + /// response inside the stream, so a mid-conversation `setConfig` takes + /// effect at the next response boundary, never mid-stream. + /// Append blocks to the pending submission (see `_pending_submission`). + /// Adopts the block *contents*; the `blocks` slice itself stays the + /// caller's. Multiple calls accumulate into one pending user message. + pub fn queueSubmission(self: *Agent, blocks: []const conversation.ContentBlock) !void { + try self._pending_submission.appendSlice(self.conversation.allocator, blocks); + } + + /// Take the pending submission, or null when none is queued. The caller + /// owns the returned slice: typically hand it to `run` (which adopts the + /// block contents) and free the slice itself with + /// `conversation.allocator`. + pub fn takeSubmission(self: *Agent) !?[]conversation.ContentBlock { + if (self._pending_submission.items.len == 0) return null; + return try self._pending_submission.toOwnedSlice(self.conversation.allocator); + } + + pub fn run(self: *Agent, message: UserMessage) !*Stream { + self._auto_compacted = false; + + // Append + persist the user prompt up front (the dangling-prompt + // recovery guarantee). `addMessage` adopts the blocks; persistence is + // brought current through the shared high-water flush. + try self.conversation.addMessage(.user, message.blocks, null); + self.flushPersist(); + + const s = try self._allocator.create(Stream); + s.* = Stream.init(self); + return s; + } + + /// Bring the session store current with the conversation: append every + /// message in `[_persisted_through .. coherent_end)` as a single batch and + /// advance the high-water mark. `coherent_end` excludes a trailing + /// assistant message whose ToolUse blocks have no following results — a + /// turn interrupted between the assistant's tool call and dispatch. Leaving + /// it unpersisted keeps the log replayable and the live conversation in a + /// state `compact()` (or a tool-result `run`) can resume from. Called at + /// every `Stream.next()` boundary, so after any `next()` the store reflects + /// all committed, coherent messages. + pub fn flushPersist(self: *Agent) void { + const msgs = self.conversation.messages.items; + // An operation that rewrote the prefix (replace-system, cancel) can + // leave the mark past the new length; clamp before comparing. + if (self._persisted_through > msgs.len) self._persisted_through = msgs.len; + var end = msgs.len; + if (end > self._persisted_through and + msgs[end - 1].role == .assistant and + turn_persist.hasToolUseWithoutFollowingResults(&self.conversation, end - 1)) + { + end -= 1; + } + if (end <= self._persisted_through) return; + turn_persist.persistRange( + self._allocator, + &self._session, + &self.conversation, + self._persisted_through, + end, + self.wireIdentity(), + &.{}, + ) catch |e| { + std.log.err("session: failed to persist turn: {t}", .{e}); + return; + }; + self._persisted_through = end; + } + + fn hasToolUseBlock(msg: conversation.Message) bool { + return toolUseCount(msg) > 0; + } + + /// Open one provider response with the configured retry policy, pushing + /// any informational `provider_retry` events into `out`. Returns the + /// resumable `ProviderStream` once a request has been successfully + /// opened (headers received), or propagates a terminal error. + /// + /// Decision path for a failed open: + /// - `ContextOverflow`: compact once, then retry the same request a + /// single time against the compacted conversation (a one-shot path, + /// independent of the transient-retry budget). + /// - retryable provider error (rate limit, server, transport, + /// malformed stream): sleep with exponential backoff + jitter + /// (honoring `Retry-After` when present) and retry, up to + /// `retry.max_attempts` total attempts. + /// - anything else (auth, bad request, cancellation, local errors): + /// propagate immediately. + /// + /// A failed open never mutates the conversation (providers commit the + /// assistant message only on success), so each retry runs against the + /// same snapshot. Mid-stream failures (surfaced from `produce`) re-enter + /// here via `Stream`, replaying the response from a fresh open — exactly + /// as the previous push loop replayed a failed `streamStep`. + fn openWithRetries( + self: *Agent, + cfg: *const Config, + out: *stream_mod.EventQueue, + ) !provider_mod.ProviderStream { + const policy = cfg.retry; + var attempt: usize = 1; + while (true) { + var diag: provider_mod.ProviderDiagnostic = .{}; + const ps = self._open_stream_fn(self._allocator, self._io, cfg, &self._registry, &self.conversation, &diag) catch |err| { + if (err == error.ContextOverflow) { + return self.handleContextOverflow(cfg, out, err); + } + if (!provider_mod.isRetryableProviderError(err)) return err; + // Out of attempts: hard-fail with the last error. + if (attempt >= policy.max_attempts) return err; + + const delay_ms = self.backoffDelayMs(policy, attempt, diag.retry_after_ms); + try out.push(.{ .provider_retry = .{ + .attempt = attempt, + .max_attempts = policy.max_attempts, + .delay_ms = delay_ms, + .err = err, + .status_code = diag.status_code, + .retry_after_ms = diag.retry_after_ms, + .message = diag.message, + } }); + if (delay_ms > 0) { + const ms: i64 = @intCast(@min(delay_ms, std.math.maxInt(i64))); + self._io.sleep(.fromMilliseconds(ms), .real) catch |e| return e; + } + attempt += 1; + continue; + }; + return ps; + } + } + + /// One-shot context-overflow recovery: compact once, retry once. Pushes + /// a `provider_retry` event with `compaction = true` and `delay_ms = 0`, + /// then re-opens the request against the compacted context. A second + /// overflow (or any other error) propagates. + fn handleContextOverflow( + self: *Agent, + cfg: *const Config, + out: *stream_mod.EventQueue, + err: anyerror, + ) !provider_mod.ProviderStream { + if (self._auto_compacted) return err; // already retried once this turn + const sys = self._config.compaction.compaction_prompt orelse return err; + const res = try self._compactInPlace(sys, null); + if (!res.compacted) return err; // nothing to shed; give up + self._auto_compacted = true; + try out.push(.{ .provider_retry = .{ + .attempt = 1, + .max_attempts = 2, + .delay_ms = 0, + .err = err, + .compaction = true, + } }); + // Retry the same request against the compacted context. + var diag: provider_mod.ProviderDiagnostic = .{}; + return self._open_stream_fn(self._allocator, self._io, cfg, &self._registry, &self.conversation, &diag); + } + + /// Compute the backoff delay (ms) for the just-failed `attempt` + /// (1-based). Prefers a provider `Retry-After` (capped by policy); + /// otherwise exponential `initial * multiplier^(attempt-1)`, capped, + /// with optional full jitter in `[0, delay)`. + fn backoffDelayMs( + self: *Agent, + policy: config_mod.RetryConfig, + attempt: usize, + retry_after_ms: ?u64, + ) u64 { + if (retry_after_ms) |ra| { + return @min(ra, policy.max_delay_ms); + } + const exp: f64 = @floatFromInt(attempt - 1); + const base: f64 = @as(f64, @floatFromInt(policy.initial_delay_ms)) * + std.math.pow(f64, policy.multiplier, exp); + const capped: f64 = @min(base, @as(f64, @floatFromInt(policy.max_delay_ms))); + var delay: u64 = @intFromFloat(capped); + if (policy.jitter and delay > 0) { + if (self._retry_prng == null) { + const ns = std.Io.Clock.now(.real, self._io).nanoseconds; + const seed: u64 = @truncate(@as(u128, @bitCast(@as(i128, ns)))); + self._retry_prng = std.Random.DefaultPrng.init(seed); + } + const r = self._retry_prng.?.random(); + delay = r.intRangeLessThan(u64, 0, delay + 1); + } + return delay; + } + + /// Compact the conversation: summarize an older prefix into a single + /// `.CompactionSummary` block and keep a recent suffix of whole turns + /// verbatim. Mutates `self.conversation` in place. + /// + /// This is the pure transform — it does **not** persist. The explicit + /// `/compact` entry point is the public `compact` (which persists); the + /// automatic (context-overflow) path persists via `runStep`'s turn tail + /// (the rewritten window is logged as a fresh compaction window). + /// + /// The system prompt survives untouched: all `.system`-role messages + /// are preserved in order, and no `replace` block is written. Only the + /// conversation (user/assistant) prefix is summarized. + /// + /// Per-message provider usage is read directly off the conversation + /// (`Message.usage`, set live by the provider and on replay from disk). + /// `computeSplit` uses it to size the retention window; messages + /// lacking usage fall back to word counting. + /// + /// `extra_instructions`, when non-null, is appended to the compaction + /// system prompt for this run (the `/compact $ARGUMENTS` path). + /// + /// `system_prompt` is the compaction system prompt (resolved by the + /// embedder from its `COMPACTION.md` layers, or a built-in default). + fn _compactInPlace( + self: *Agent, + system_prompt: []const u8, + extra_instructions: ?[]const u8, + ) !CompactionResult { + const conv = &self.conversation; + const messages = conv.messages.items; + + // Project per-message usage off the conversation for sizing. + const usages = try self._allocator.alloc(?conversation_Usage, messages.len); + defer self._allocator.free(usages); + for (messages, 0..) |m, i| usages[i] = m.usage; + + const split = compaction_mod.computeSplit(messages, usages, self._config.compaction.keep_verbatim); + + // Determine the active conversation start (after any prior summary). + const active_start: usize = if (conversation.latestCompactionIndex(messages)) |a| a + 1 else 0; + + // Nothing to summarize: the active conversation already fits, or the + // prefix boundary is at/under the first active turn. + if (split.prefix_end <= active_start) { + return .{ .compacted = false }; + } + // Count how many *conversation* (non-system) messages are in the + // summarized prefix. If none, this is also a no-op. + var summarized: usize = 0; + for (messages[active_start..split.prefix_end]) |m| { + if (m.role != .system) summarized += 1; + } + if (summarized == 0) return .{ .compacted = false }; + + // Serialize the prefix transcript and carry forward the latest + // existing summary (chained-compaction invariant). + const transcript = try compaction_mod.serializeTranscript( + self._allocator, + messages[active_start..split.prefix_end], + ); + defer self._allocator.free(transcript); + + const previous_summary = compaction_mod.latestSummaryText(messages); + const body = try compaction_mod.buildRequestBody(self._allocator, transcript, previous_summary); + defer self._allocator.free(body); + + const summary = try self.runCompactionRequest(system_prompt, body, extra_instructions); + defer self._allocator.free(summary.text); + + try self.rewriteWithSummary(conv, split.prefix_end, summary.text, summary.size); + + return .{ + .compacted = true, + .kept_turns = split.kept_turns, + .summarized_messages = summarized, + }; + } + + /// Compact and persist the result to the session store. This is the + /// explicit `/compact` entry point: it summarizes (via the private + /// `_compactInPlace` transform) and, if anything was compacted, appends + /// the new compaction window (summary + restated kept suffix) to the + /// store. Returns the `CompactionResult` for the embedder to report. + /// + /// `override_system_prompt`, when non-null, is the compaction system + /// prompt for this run; when null it falls back to + /// `config.compaction.compaction_prompt`. With neither set, this errors + /// (`error.NoCompactionPrompt`). + pub fn compact( + self: *Agent, + override_system_prompt: ?[]const u8, + extra_instructions: ?[]const u8, + ) !CompactionResult { + const system_prompt = override_system_prompt orelse + self._config.compaction.compaction_prompt orelse + return error.NoCompactionPrompt; + const res = try self._compactInPlace(system_prompt, extra_instructions); + // `rewriteWithSummary` rewound the high-water mark to the summary; + // flush appends the new compaction window (summary + restated suffix). + if (res.compacted) self.flushPersist(); + return res; + } + + /// Rewrite `conv.messages` to `[all system messages..., summary, + /// kept-suffix...]`. The summarized conversation prefix (everything + /// before `prefix_end` that isn't a system message) is dropped; system + /// messages survive in order; a `.CompactionSummary` user message is + /// inserted; the kept suffix (`messages[prefix_end..]`) is preserved. + fn rewriteWithSummary( + self: *Agent, + conv: *conversation.Conversation, + prefix_end: usize, + summary: []const u8, + summary_size: u64, + ) !void { + const alloc = self._allocator; + const old = conv.messages.items; + + var rebuilt: std.ArrayList(conversation.Message) = .empty; + errdefer { + for (rebuilt.items) |*m| m.deinit(alloc); + rebuilt.deinit(alloc); + } + + // 1. All system messages from the summarized prefix survive, in + // order. (System messages in the kept suffix come along with it + // below, so only scan the prefix here.) + for (old[0..prefix_end]) |*m| { + if (m.role != .system) continue; + try rebuilt.append(alloc, try cloneMessage(alloc, m.*)); + } + + // 2. The compaction summary, alone in a user message. + { + const tb = try conversation.textualBlockFromSlice(alloc, summary); + var content: std.ArrayList(conversation.ContentBlock) = .empty; + errdefer { + for (content.items) |*b| b.deinit(alloc); + content.deinit(alloc); + } + try content.append(alloc, .{ .CompactionSummary = .{ .text = tb } }); + try rebuilt.append(alloc, .{ .role = .user, .content = content }); + } + + // 3. The kept verbatim suffix, with usage recomputed so the + // restated window reads like a fresh conversation anchored at + // the summary. + // + // The post-compaction window is `[summary(user), kept_user, + // kept_assistant, ...]`. `usage.input` is *cumulative* (the whole + // prior prompt). We walk forward maintaining `running_input` — + // the synthetic cumulative prompt size as of "just before the + // next assistant output" — seeded with the summary's size: + // + // - non-assistant message → it occupies context; add its size + // (word-count heuristic, the same `messageTokenEstimate` the + // splitter uses for usage-less messages). + // - assistant message → its synthetic prompt is `running_input`. + // The whole prompt total collapses into `input` (the rewrite + // busts any provider prefix cache, so cache_read/cache_write + // are zeroed). `output`/`reasoning` are copied verbatim (real + // generation). Then `running_input += output` for the next + // turn. + // + // Assistants that had no usage stay null (we can't invent an + // output we never measured; the splitter already tolerates null). + var running_input: u64 = summary_size; + for (old[prefix_end..]) |*m| { + var cloned = try cloneMessage(alloc, m.*); + if (cloned.role == .assistant) { + if (m.usage) |u| { + cloned.usage = .{ + .input = running_input, + .output = u.output, + .cache_read = 0, + .cache_write = 0, + .reasoning = u.reasoning, + }; + running_input += u.output; + } + } else { + running_input += compaction_mod.messageTokenEstimate(m.*, null); + } + try rebuilt.append(alloc, cloned); + } + + // Swap in the rebuilt list and free the old one. + for (conv.messages.items) |*m| m.deinit(alloc); + conv.messages.deinit(alloc); + conv.messages = rebuilt; + + // The rewrite invalidated the old message indices. Rewind the + // persistence high-water mark to the inserted summary so the next + // `flushPersist` re-appends the new compaction window (summary + + // restated kept suffix) as fresh entries. + self._persisted_through = + conversation.latestCompactionIndex(conv.messages.items) orelse 0; + } + + /// Run a single compaction provider call against a throwaway + /// conversation. Returns the assistant's summary text (caller owns). + /// + /// Model selection: try `config.compaction.model` if set; on failure, + /// fall back to the active chat model. Compaction runs with an empty + /// tool registry and a single user message (the request body); no tools + /// are exposed and no session logging occurs. + fn runCompactionRequest( + self: *Agent, + system_prompt: []const u8, + body: []const u8, + extra_instructions: ?[]const u8, + ) !CompactionSummary { + const alloc = self._allocator; + + // Assemble the effective compaction system prompt (+ extra + // instructions for a `/compact $ARGUMENTS` run). + var sys_text: []const u8 = system_prompt; + var sys_owned: ?[]u8 = null; + defer if (sys_owned) |s| alloc.free(s); + if (extra_instructions) |extra| { + if (extra.len > 0) { + const combined = try std.fmt.allocPrint( + alloc, + "{s}\n\n## Additional instructions for this compaction run\n\n{s}", + .{ system_prompt, extra }, + ); + sys_owned = combined; + sys_text = combined; + } + } + + var empty_registry = ToolRegistry.init(alloc); + defer empty_registry.deinit(); + + // Try the configured compaction model first, then fall back to the + // active chat model on any failure. + if (self._config.compaction.model) |comp_provider| { + const cfg: config_mod.Config = .{ + .provider = comp_provider, + .compaction = self._config.compaction, + }; + if (self.runSingleCompactionTurn(&cfg, &empty_registry, sys_text, body)) |summary| { + return summary; + } else |err| { + std.log.warn("compaction model failed ({t}); falling back to active model", .{err}); + } + } + + const cfg: config_mod.Config = .{ + .provider = self._config.provider, + .compaction = self._config.compaction, + }; + return self.runSingleCompactionTurn(&cfg, &empty_registry, sys_text, body); + } + + /// A generated compaction summary plus the token size to attribute to + /// it when it becomes the new conversation anchor. `size` is the + /// provider-reported `output` token count for the summary turn (the + /// real generated length), falling back to the word-count heuristic + /// when the provider emitted no usage. `text` is caller-owned. + const CompactionSummary = struct { + text: []u8, + size: u64, + }; + + /// One provider call for compaction. Builds a throwaway conversation + /// (system prompt + one user message), streams a single turn through a + /// capturing receiver, and returns the assembled assistant text plus + /// the summary's token size (for usage anchoring on the rewrite). + fn runSingleCompactionTurn( + self: *Agent, + cfg: *const config_mod.Config, + registry: *const ToolRegistry, + system_prompt: []const u8, + body: []const u8, + ) !CompactionSummary { + const alloc = self._allocator; + var conv = conversation.Conversation.init(alloc); + defer conv.deinit(); + try conv.addSystemMessage(system_prompt); + try addUserText(&conv, body); + + // Drive one provider response to completion, ignoring every event. + // Compaction doesn't need incremental output — the assembled message + // is read off the conversation below — so we just pump the pull + // stream until it commits the assistant message. + var queue = stream_mod.EventQueue.init(alloc); + defer queue.deinit(); + var ps = try self._open_stream_fn(alloc, self._io, cfg, registry, &conv, null); + defer ps.deinit(); + while (true) { + const status = try ps.produce(&queue); + // Drain (and discard) any events to bound the queue/arena. + while (queue.pop()) |_| {} + if (status == .response_complete) break; + } + + // The provider appended an assistant message; gather its text. + const last = conv.messages.items[conv.messages.items.len - 1]; + if (last.role != .assistant) return error.CompactionNoResponse; + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(alloc); + for (last.content.items) |block| { + if (block == .Text) try out.appendSlice(alloc, block.Text.items); + } + if (out.items.len == 0) return error.CompactionEmptySummary; + + // Summary size: prefer the provider's reported output token count + // for this turn; otherwise word-count the assembled summary text + // (the same fallback the splitter uses for usage-less messages). + const size: u64 = if (last.usage) |u| + u.output + else + compaction_mod.messageTokenEstimate(last, null); + + return .{ .text = try out.toOwnedSlice(alloc), .size = size }; + } + + /// Dispatch every ToolUse block in `assistant_msg`. Groups by owning + /// registration; one OS thread per group; results assembled in the + /// original call order. + fn dispatchToolCalls( + self: *Agent, + assistant_msg: conversation.Message, + ) !void { + const conv = &self.conversation; + // Build the flat call list (in original order) and group calls + // by owning registration. + var calls: std.array_list.Managed(FlatCall) = .init(self._allocator); + defer calls.deinit(); + + for (assistant_msg.content.items) |block| { + if (block != .ToolUse) continue; + const tu = block.ToolUse; + if (!isValidToolInput(tu.input.items)) { + try calls.append(.{ + .tool_use_id = tu.id, + .tool_name = tu.name, + .input = tu.input.items, + .entry = null, + .result = try invalidInputResult(self._allocator, tu.input.items), + .err = null, + .is_error = true, + }); + continue; + } + const entry = self._registry.lookup(tu.name) orelse { + // Unknown tool: don't abort. Synthesize an error result so + // the model can correct, and so this ToolUse still gets its + // matching ToolResult (providers reject a follow-up request + // otherwise). + try calls.append(.{ + .tool_use_id = tu.id, + .tool_name = tu.name, + .input = tu.input.items, + .entry = null, + .result = try toolErrorResult(self._allocator, tu.name, error.UnknownTool), + .err = null, + .is_error = true, + }); + continue; + }; + try calls.append(.{ + .tool_use_id = tu.id, + .tool_name = tu.name, + .input = tu.input.items, + .entry = entry.entry, + .result = null, + .err = null, + }); + } + std.debug.assert(calls.items.len > 0); + + // Partition into groups. A group's `kind` determines how it + // runs; the `member_indices` are positions into `calls` (the + // original call order) so we can write back results without + // re-ordering. + var groups: std.array_list.Managed(Group) = .init(self._allocator); + defer { + for (groups.items) |*g| g.deinit(self._allocator); + groups.deinit(); + } + try buildGroups(self._allocator, calls.items, &groups); + + // Spawn one concurrent task per group via `std.Io.Group`. + // Single-tool groups run the tool's vtable; source groups run + // the source's `invoke_batch`. We use `concurrent` rather than + // `async` because tool work may block on I/O — under a + // single-threaded `Io` `async` would deadlock; `concurrent` + // forces real concurrency (or `error.ConcurrencyUnavailable`). + var task_group: Io.Group = .init; + // `cancel` is idempotent with `await`; if anything below this + // point errors before we successfully `await`, this releases + // the group's resources. + defer task_group.cancel(self._io); + errdefer { + for (calls.items) |*c| { + if (c.result) |r| r.deinit(self._allocator); + } + } + + // Try real concurrency first. If the `Io` implementation can't + // provide it (`error.ConcurrencyUnavailable`), fall back to running + // every group sequentially on this thread — tool batches are small + // (rarely more than a handful of calls) so the serial path is a fine + // safety net rather than a hard failure. + var ran_concurrently = true; + for (groups.items) |*g| { + task_group.concurrent(self._io, runGroup, .{ self, g, calls.items }) catch |e| { + if (e == error.ConcurrencyUnavailable) { + ran_concurrently = false; + break; + } + return e; + }; + } + if (ran_concurrently) { + // `error.Canceled` here means cancellation propagated into this + // dispatch from above; surface it like any other error. + try task_group.await(self._io); + } else { + // Cancel any tasks that were spawned before the failure, then + // run all groups serially. Only entry-bearing calls are touched + // by `runGroup`; the pre-seeded error results (unknown tool, + // invalid input) have `entry == null` and must be left intact. + task_group.cancel(self._io); + for (calls.items) |*c| { + if (c.entry == null) continue; + if (c.result) |r| r.deinit(self._allocator); + c.result = null; + c.err = null; + } + for (groups.items) |*g| runGroup(self, g, calls.items); + } + + // Pre-pass: resolve worker-reported errors. A hard host failure + // (cancellation, OOM) aborts the whole turn. Every other failure is + // converted into a model-visible error `ToolResult` so the model can + // recover and so each `ToolUse` keeps its matching `ToolResult` + // (providers reject the next request otherwise). + for (calls.items) |*c| { + const e = c.err orelse continue; + if (classifyToolError(e) == .hard_fail) return e; + // Replace any partial result with a synthesized error result. + if (c.result) |r| { + r.deinit(self._allocator); + c.result = null; + } + c.result = try toolErrorResult(self._allocator, c.tool_name, e); + c.err = null; + c.is_error = true; + } + + // Assemble ToolResult blocks in original call order. + var content: std.ArrayList(conversation.ContentBlock) = .empty; + errdefer { + for (content.items) |*b| b.deinit(self._allocator); + content.deinit(self._allocator); + } + try content.ensureTotalCapacity(self._allocator, calls.items.len); + + for (calls.items) |*c| { + const result_parts = c.result orelse { + // Internal invariant: every call should now have a result + // (success, synthesized error, or pre-seeded error). + return error.MissingToolResult; + }; + c.result = null; // ownership transferred below + defer result_parts.deinit(self._allocator); + + const id_copy = try self._allocator.dupe(u8, c.tool_use_id); + errdefer self._allocator.free(id_copy); + + var stored: std.ArrayList(conversation.ResultPartStored) = .empty; + errdefer { + for (stored.items) |*p| p.deinit(self._allocator); + stored.deinit(self._allocator); + } + try stored.ensureTotalCapacity(self._allocator, result_parts.items.len); + for (result_parts.items) |part| { + switch (part) { + .text => |t| { + var buf: conversation.TextualBlock = .empty; + errdefer buf.deinit(self._allocator); + try buf.appendSlice(self._allocator, t); + stored.appendAssumeCapacity(.{ .text = buf }); + }, + .media => |m| { + // libpanto owns the heavy lifting: detect the type + // (when the tool gave no hint), resize large + // rasters, then base64-encode for storage. Tools + // hand over raw bytes only. + const processed = image_mod.process(self._allocator, m.data, m.media_type) catch |e| { + // Media processing failure: keep the turn alive by + // dropping the attachment and noting it as text, + // rather than aborting. `UnknownMediaType` gets a + // friendly note; other failures name the error. + var note: conversation.TextualBlock = .empty; + errdefer note.deinit(self._allocator); + if (e == error.UnknownMediaType) { + try note.appendSlice(self._allocator, "[unrecognized binary attachment dropped]"); + } else { + const txt = try std.fmt.allocPrint( + self._allocator, + "[media attachment dropped: {s}]", + .{@errorName(e)}, + ); + defer self._allocator.free(txt); + try note.appendSlice(self._allocator, txt); + } + stored.appendAssumeCapacity(.{ .text = note }); + continue; + }; + defer self._allocator.free(processed.data); + + const mt = try self._allocator.dupe(u8, processed.media_type); + errdefer self._allocator.free(mt); + + const enc = std.base64.standard.Encoder; + var buf: conversation.TextualBlock = .empty; + errdefer buf.deinit(self._allocator); + try buf.resize(self._allocator, enc.calcSize(processed.data.len)); + _ = enc.encode(buf.items, processed.data); + + stored.appendAssumeCapacity(.{ .media = .{ .media_type = mt, .data = buf } }); + }, + } + } + + content.appendAssumeCapacity(.{ .ToolResult = .{ + .tool_use_id = id_copy, + .parts = stored, + .is_error = c.is_error, + } }); + } + + try conv.messages.append(self._allocator, .{ + .role = .user, + .content = content, + }); + } +}; + +/// A resumable pull handle over one agent turn. +/// +/// `next()` pulls one `Event` at a time, driving the agent loop +/// incrementally: open a provider response, stream its events, dispatch any +/// tool calls between responses, and repeat until the model stops calling +/// tools. The whole loop's state lives here (not on a stack frame), so the +/// turn can suspend and resume between events. +/// +/// Contract (see `stream.zig`): +/// - an `Event` value is streaming progress, including `turn_complete`; +/// - `null` means exhausted (already past `turn_complete`), never before; +/// - an error is a genuine failure (network/parse/provider). +/// +/// Event payloads borrow from the stream's decode state or the conversation +/// and are valid only until the next `next()` call. +pub const Stream = struct { + // Internal state (underscore-prefixed by convention: not part of the + // public surface, even though Zig fields are always reachable). The only + // intended-public field is `state`, the transparent turn state machine. + _agent: *Agent, + _queue: stream_mod.EventQueue, + state: State, + /// The active provider response, when in `.streaming`. + _response: ?provider_mod.ProviderStream = null, + /// First message index of this turn (the boundary `cancel` rolls back to). + _start: usize, + /// A terminal error to surface once any already-queued events (e.g. + /// `provider_retry` notices pushed before the failing attempt) have been + /// drained. `next()` yields the queue first, then this error. + _pending_error: ?anyerror = null, + /// Count of mid-stream retries attempted for the CURRENT response. Unlike + /// `openWithRetries` (which loops internally with a local counter), a + /// mid-stream failure unwinds back to `.turn_start` and re-opens, so the + /// attempt count must live on the stream to survive across re-opens. + /// Reset to 0 each time a response completes. A persistently-broken + /// stream would otherwise re-open forever without ever exhausting + /// `retry.max_attempts`. + _stream_retries: usize = 0, + /// Owned copy of the provider's diagnostic message for the in-flight + /// mid-stream retry notice. Borrowed by the queued `provider_retry` + /// event, so it must outlive that event: freed on the next mid-stream + /// failure and on `deinit`. + _retry_message: ?[]u8 = null, + + pub const State = enum { + /// Open the next provider response (with retries). + turn_start, + /// Pump the active provider response into events. + streaming, + /// A provider response completed; decide tools-vs-done. + after_response, + /// Tool dispatch has been announced to callers; run the blocking + /// dispatch on the next pull so `tool_dispatch_start` is observable + /// before long-running tools complete. + dispatching_tools, + /// The turn reached its terminal `turn_complete`. + done, + /// A failure already propagated; `next()` is poisoned. + failed, + }; + + fn init(agent: *Agent) Stream { + return .{ + ._agent = agent, + ._queue = stream_mod.EventQueue.init(agent._allocator), + .state = .turn_start, + ._start = agent.conversation.messages.items.len, + }; + } + + pub fn deinit(self: *Stream) void { + // Persist whatever the turn committed, on every exit path — including + // dropping the stream mid-turn after some messages were committed. + // `next()` already flushes at every boundary, so this is usually a + // no-op; it catches a stream dropped without a final `next()`. + self._agent.flushPersist(); + if (self._response) |ps| ps.deinit(); + if (self._retry_message) |m| self._agent._allocator.free(m); + self._queue.deinit(); + self._agent._allocator.destroy(self); + } + + /// Abort this turn from the embedder side (for example, Escape in a TUI). + /// Provider/tool work that is currently inside `next()` cannot be unwound + /// until that call returns, but once control reaches the embedder this + /// drops every assistant/tool-result message produced by this stream before + /// `deinit()` has a chance to persist them. The already-persisted user + /// prompt (written by `Agent.run`) is intentionally kept, leaving a valid + /// conversation prefix with no dangling ToolUse blocks. + pub fn cancel(self: *Stream) void { + if (self._response) |ps| { + ps.deinit(); + self._response = null; + } + const conv = &self._agent.conversation; + while (conv.messages.items.len > self._start) { + var msg = conv.messages.pop().?; + msg.deinit(conv.allocator); + } + // Rolled the conversation back; clamp the persistence mark to match. + self._agent.flushPersist(); + self.state = .done; + } + + /// Reset a `.failed` stream back to `.turn_start` so the caller can resume + /// `next()` after changing the agent config (e.g. an embedder that catches + /// a terminal `ProviderBadRequest`, rewrites the provider config, and + /// wants to retry the SAME turn without re-appending the user message). + /// + /// Safe only at the OPEN boundary: a failed open never mutates the + /// conversation, so the already-committed user prompt at `_start` stays + /// intact and the re-open re-reads the (now-swapped) config snapshot. + /// Returns `error.StreamNotFailed` if the stream is not in `.failed`. + pub fn reopen(self: *Stream) !void { + if (self.state != .failed) return error.StreamNotFailed; + if (self._response) |ps| { + ps.deinit(); + self._response = null; + } + self._pending_error = null; + self.state = .turn_start; + } + + /// Pull the next event, or null past the terminal. See the contract + /// above. + pub fn next(self: *Stream) !?Event { + // Bring persistence current on every return path (events, terminal, + // errors). This is the interruptibility guarantee: after any `next()` + // the store reflects all committed, coherent messages, so the caller + // can `break` and immediately `compact()` or resume with a new turn. + defer self._agent.flushPersist(); + // Always drain queued events first; they borrow decode/conversation + // state valid until this call returns. + if (self._queue.pop()) |ev| return ev; + // Queue drained: a deferred terminal error surfaces now. + if (self._pending_error) |err| { + self._pending_error = null; + self.state = .failed; + return err; + } + + while (true) { + switch (self.state) { + .done => return null, + .failed => return error.StreamPoisoned, + .turn_start => { + // Re-read the config snapshot at each response boundary + // so a mid-conversation swap takes effect here, never + // mid-stream. + const cfg = self._agent._config; + const ps = self._agent.openWithRetries(cfg, &self._queue) catch |err| { + // Surface the failure after any queued retry notices + // (pushed before each backoff) are drained. + if (self._queue.pop()) |ev| { + self._pending_error = err; + return ev; + } + self.state = .failed; + return err; + }; + self._response = ps; + self.state = .streaming; + if (self._queue.pop()) |ev| return ev; // retry notices + }, + .streaming => { + const ps = self._response.?; + const status = ps.produce(&self._queue) catch |err| { + // Mid-stream failure. The conversation was not + // mutated (commit happens only at response + // completion), so retry by re-opening from scratch, + // exactly as the prior push loop replayed a failed + // streamStep. Non-retryable errors propagate. + // + // Capture the provider's diagnostic message (e.g. an + // Anthropic `overloaded_error`) BEFORE deinit frees + // it, dup'ing it so it outlives the response. Owned + // here and freed after the retry notice is queued. + if (self._retry_message) |old| { + self._agent._allocator.free(old); + self._retry_message = null; + } + if (ps.lastError()) |m| { + self._retry_message = self._agent._allocator.dupe(u8, m) catch null; + } + ps.deinit(); + self._response = null; + if (!provider_mod.isRetryableProviderError(err)) { + self.state = .failed; + return err; + } + // Out of attempts: hard-fail with the last error + // instead of re-opening forever. `_stream_retries` + // counts mid-stream retries already made for this + // response; the initial attempt is the open itself, + // so the Nth retry is attempt N+1. + const cfg = self._agent._config; + self._stream_retries += 1; + if (self._stream_retries >= cfg.retry.max_attempts) { + self.state = .failed; + return err; + } + self.state = .turn_start; + // Emit a retry notice so consumers see the stall. + const delay_ms = self._agent.backoffDelayMs(cfg.retry, self._stream_retries, null); + self._queue.push(.{ .provider_retry = .{ + .attempt = self._stream_retries, + .max_attempts = cfg.retry.max_attempts, + .delay_ms = delay_ms, + .err = err, + .message = self._retry_message, + } }) catch |e| { + self.state = .failed; + return e; + }; + if (delay_ms > 0) { + const ms: i64 = @intCast(@min(delay_ms, std.math.maxInt(i64))); + self._agent._io.sleep(.fromMilliseconds(ms), .real) catch |e| { + self.state = .failed; + return e; + }; + } + if (self._queue.pop()) |ev| return ev; + continue; + }; + if (self._queue.pop()) |ev| return ev; + if (status == .response_complete) { + ps.deinit(); + self._response = null; + // A response completed: reset the mid-stream retry + // budget so a later turn starts fresh. + self._stream_retries = 0; + self.state = .after_response; + } + // else `.more`: loop and pump again. + }, + .after_response => { + const conv = &self._agent.conversation; + const last = conv.messages.items[conv.messages.items.len - 1]; + std.debug.assert(last.role == .assistant); + + // Defense-in-depth: a provider that silently committed an + // empty assistant message means the turn made no + // observable progress. Surface it instead of looping. + if (last.content.items.len == 0) { + self.state = .failed; + return error.EmptyAssistantResponse; + } + + if (!Agent.hasToolUseBlock(last)) { + self.state = .done; + return .turn_complete; + } + + // Announce tool dispatch and return immediately. The + // dispatch itself can block for a long time (especially + // with parallel tools); yielding this boundary event first + // lets UIs/renderers show all tool calls as running instead + // of appearing frozen until the slowest tool completes. + const count = toolUseCount(last); + self._queue.push(.{ .tool_dispatch_start = .{ .count = count } }) catch |e| { + self.state = .failed; + return e; + }; + self.state = .dispatching_tools; + if (self._queue.pop()) |ev| return ev; + }, + .dispatching_tools => { + const conv = &self._agent.conversation; + const last = conv.messages.items[conv.messages.items.len - 1]; + std.debug.assert(last.role == .assistant); + std.debug.assert(Agent.hasToolUseBlock(last)); + + self._agent.dispatchToolCalls(last) catch |err| { + self.state = .failed; + return err; + }; + const result_msg = conv.messages.items[conv.messages.items.len - 1]; + self._queue.push(.{ .tool_dispatch_complete = .{ .message = result_msg } }) catch |e| { + self.state = .failed; + return e; + }; + self.state = .turn_start; + if (self._queue.pop()) |ev| return ev; + }, + } + } + } +}; + +fn toolUseCount(msg: conversation.Message) usize { + var n: usize = 0; + for (msg.content.items) |block| { + if (block == .ToolUse) n += 1; + } + return n; +} + +/// One ToolUse, as flattened into the agent's dispatch list. `result` +/// and `err` are filled in by the worker; exactly one is non-null on +/// successful task completion. +const FlatCall = struct { + tool_use_id: []const u8, // borrowed from assistant_msg + tool_name: []const u8, // borrowed from assistant_msg + input: []const u8, // borrowed from assistant_msg + entry: ?Entry, + + /// Owned result parts from `Tool.invoke` or `ToolSource.invoke_batch`. + /// Allocated with the agent's allocator. Transferred into a + /// ToolResultBlock on success. + result: ?tool_mod.ResultParts, + + /// If non-null, the worker reported a failure for this call. After + /// dispatch it is classified: host failures abort the turn, everything + /// else is converted into an error `ToolResult`. + err: ?anyerror, + + /// True when `result` already holds a synthesized error result (unknown + /// tool, invalid input). Worker-reported `err`s are folded into this + /// during assembly. + is_error: bool = false, +}; + +/// One dispatch group. Either a single Tool invocation, or a batch of +/// calls headed to one ToolSource. +const Group = union(enum) { + single: SingleGroup, + source: SourceGroup, + + pub const SingleGroup = struct { + tool: Tool, + /// Index into the flat calls array. + call_index: usize, + }; + + pub const SourceGroup = struct { + source: *ToolSource, + /// Indices into the flat calls array. Owned by the group. + member_indices: []usize, + }; + + fn deinit(self: *Group, allocator: Allocator) void { + switch (self.*) { + .single => {}, + .source => |sg| allocator.free(sg.member_indices), + } + } +}; + +/// Partition the flat call list into groups. Order of groups is +/// arbitrary; order within a `source` group preserves the original +/// call order so that batch results can be written back positionally. +fn buildGroups( + allocator: Allocator, + calls: []const FlatCall, + out: *std.array_list.Managed(Group), +) !void { + // Map from source pointer to the index of its group in `out`. + // Buffers per source, accumulated then frozen into slices. + var pending: std.AutoHashMap(*ToolSource, std.array_list.Managed(usize)) = + .init(allocator); + defer { + var it = pending.valueIterator(); + while (it.next()) |l| l.deinit(); + pending.deinit(); + } + + for (calls, 0..) |c, i| { + const ent = c.entry orelse continue; + switch (ent) { + .single => |t| try out.append(.{ .single = .{ .tool = t, .call_index = i } }), + .source => |sr| { + const gop = try pending.getOrPut(sr.source); + if (!gop.found_existing) { + gop.value_ptr.* = std.array_list.Managed(usize).init(allocator); + } + try gop.value_ptr.append(i); + }, + } + } + + // Freeze each pending list into a source-group entry. We move + // ownership of the indices into `Group.source.member_indices`. + var pit = pending.iterator(); + while (pit.next()) |entry| { + const src = entry.key_ptr.*; + const indices = try entry.value_ptr.toOwnedSlice(); + try out.append(.{ .source = .{ .source = src, .member_indices = indices } }); + } +} + +/// Worker entry point. Runs one group to completion, populating +/// `calls[i].result` or `calls[i].err` for each member call. +/// +/// Return type is `void`, which coerces to `Io.Cancelable!void` as +/// required by `Group.concurrent`. Tool errors are reported via +/// `FlatCall.err`, not by returning from this function. +fn runGroup(agent: *Agent, group: *Group, calls: []FlatCall) void { + switch (group.*) { + .single => |sg| { + const i = sg.call_index; + const c = &calls[i]; + const out = sg.tool.vtable.invoke(sg.tool.ctx, c.input, agent._allocator) catch |e| { + c.err = e; + return; + }; + c.result = out; + }, + .source => |sg| runSourceGroup(agent, sg, calls), + } +} + +fn runSourceGroup(agent: *Agent, sg: Group.SourceGroup, calls: []FlatCall) void { + const n = sg.member_indices.len; + + const batch_calls = agent._allocator.alloc(tool_source_mod.Call, n) catch |e| { + for (sg.member_indices) |i| calls[i].err = e; + return; + }; + defer agent._allocator.free(batch_calls); + + const batch_results = agent._allocator.alloc(tool_source_mod.CallResult, n) catch |e| { + for (sg.member_indices) |i| calls[i].err = e; + return; + }; + defer agent._allocator.free(batch_results); + + for (sg.member_indices, 0..) |idx, j| { + batch_calls[j] = .{ + .tool_name = calls[idx].tool_name, + .input = calls[idx].input, + }; + batch_results[j] = .{ .err = error.SourceDroppedCall }; + } + + sg.source.vtable.invoke_batch( + sg.source.ctx, + batch_calls, + batch_results, + agent._allocator, + ) catch |e| { + // Whole-batch failure: free any partial successes the source + // already wrote, then mark every member as failed. + for (batch_results) |r| switch (r) { + .ok => |b| b.deinit(agent._allocator), + .err => {}, + }; + for (sg.member_indices) |i| calls[i].err = e; + return; + }; + + // Per-call success/error. + for (sg.member_indices, 0..) |i, j| { + switch (batch_results[j]) { + .ok => |b| calls[i].result = b, + .err => |e| calls[i].err = e, + } + } +} + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const testing = std.testing; + +/// Test helper: submit `text` and drive the whole turn to completion via the +/// pull `Stream`, discarding every event (the agent tests assert on +/// conversation/store state, not on the event stream). Mirrors the old +/// `submitUserMessage` + `runStep`: it returns the same terminal error a +/// turn would raise. +fn drainTurn(agent: *Agent, text: []const u8) !void { + var s = try runUserText(agent, text); + defer s.deinit(); + while (try s.next()) |_| {} +} + +/// Test helper: open a turn from a single user `.Text` block, mirroring the +/// old text-only `run(.{ .text = ... })`. The block is adopted by the agent's +/// conversation. +fn runUserText(agent: *Agent, text: []const u8) !*Stream { + var blocks = [_]conversation.ContentBlock{ + .{ .Text = try conversation.textualBlockFromSlice(agent._allocator, text) }, + }; + return agent.run(.{ .blocks = &blocks }); +} + +/// Test helper: the items of a ToolResultBlock's first text part. +fn trText(tr: conversation.ToolResultBlock) []const u8 { + for (tr.parts.items) |p| { + if (p == .text) return p.text.items; + } + return ""; +} + +/// Test harness for the injectable `_open_stream_fn` seam. +/// +/// `provider_mod.OpenStreamFn` carries no user context (it mirrors the real +/// free function exactly), so the stub parks its state in a module-level +/// pointer that `stubOpenStream` reads. The Zig test runner executes tests +/// serially in one process, so a single global slot is safe; each test sets +/// it via `install` before driving the agent. +var stub_active: ?*StubProvider = null; + +const StubProvider = struct { + allocator: Allocator, + scripted: []const ScriptedTurn, + next: usize = 0, + /// Number of leading stream opens that should fail with + /// `error.ContextOverflow` before any scripted turn is served. Used to + /// drive the auto-compaction path. Decremented on each overflow. + overflow_calls: usize = 0, + /// A queue of provider errors to return, in order, before any scripted + /// turn is served. Each entry is consumed on one open. Used to drive the + /// transient-retry path. + scripted_errors: []const ScriptedError = &.{}, + error_idx: usize = 0, + /// A queue of MID-STREAM errors. Unlike `scripted_errors` (which fail at + /// open time), each entry here lets the open succeed but makes the + /// returned response fail on its first `produce`, exercising the + /// `.streaming` retry path. Consumed in order, one per open. + stream_errors: []const StreamFailure = &.{}, + stream_error_idx: usize = 0, + /// Count of opens observed (failed + succeeded). Lets tests assert the + /// exact number of attempts. + calls_made: usize = 0, + + const ScriptedError = struct { + err: anyerror, + status_code: ?u16 = null, + retry_after_ms: ?u64 = null, + }; + + const StreamFailure = struct { + err: anyerror, + /// Optional provider diagnostic surfaced via `ProviderStream.lastError`. + message: ?[]const u8 = null, + }; + + const ScriptedTurn = struct { + blocks: []const TestBlock, + /// Optional provider usage to stamp on the committed assistant + /// message (mirrors a real provider's terminal usage). + usage: ?conversation.Usage = null, + }; + + const TestBlock = union(enum) { + Text: []const u8, + ToolUse: struct { + id: []const u8, + name: []const u8, + input: []const u8, + }, + }; + + /// Point the global seam at this stub and return the function to assign + /// to `agent._open_stream_fn`. Call once per test, after constructing the + /// stub on the stack. + fn install(self: *StubProvider) provider_mod.OpenStreamFn { + stub_active = self; + return stubOpenStream; + } +}; + +/// A canned resumable response: on the first `produce` it commits the +/// scripted assistant message to the conversation and pushes the terminal +/// `message_complete`, then reports `.response_complete`. It does not emit +/// per-block events (the agent tests assert on conversation state, not the +/// event stream), which is sufficient for driving the agent loop. +const StubResponse = struct { + allocator: Allocator, + conv: *conversation.Conversation, + turn: StubProvider.ScriptedTurn, + done: bool = false, + /// If set, the first `produce` returns this error instead of committing + /// (simulates a mid-stream provider failure after a successful open). + produce_error: ?anyerror = null, + /// Optional provider diagnostic surfaced via `lastError` after the + /// mid-stream failure (borrowed; static test strings). + error_message: ?[]const u8 = null, + + fn create( + allocator: Allocator, + conv: *conversation.Conversation, + turn: StubProvider.ScriptedTurn, + ) !provider_mod.ProviderStream { + const self = try allocator.create(StubResponse); + self.* = .{ .allocator = allocator, .conv = conv, .turn = turn }; + return .{ .ptr = self, .vtable = &vtable }; + } + + fn createFailing( + allocator: Allocator, + conv: *conversation.Conversation, + err: anyerror, + message: ?[]const u8, + ) !provider_mod.ProviderStream { + const self = try allocator.create(StubResponse); + self.* = .{ + .allocator = allocator, + .conv = conv, + .turn = .{ .blocks = &.{} }, + .produce_error = err, + .error_message = message, + }; + return .{ .ptr = self, .vtable = &vtable }; + } + + const vtable: provider_mod.ProviderStream.VTable = .{ + .produce = produceVT, + .deinit = deinitVT, + .last_error = lastErrorVT, + }; + + fn lastErrorVT(ptr: *anyopaque) ?[]const u8 { + const self: *StubResponse = @ptrCast(@alignCast(ptr)); + return self.error_message; + } + + fn produceVT(ptr: *anyopaque, out: *stream_mod.EventQueue) anyerror!provider_mod.ProviderStream.ProduceStatus { + const self: *StubResponse = @ptrCast(@alignCast(ptr)); + if (self.produce_error) |err| return err; + if (self.done) return .response_complete; + self.done = true; + + var blocks: std.ArrayList(conversation.ContentBlock) = .empty; + errdefer { + for (blocks.items) |*b| b.deinit(self.allocator); + blocks.deinit(self.allocator); + } + for (self.turn.blocks) |tb| { + switch (tb) { + .Text => |s| try blocks.append(self.allocator, .{ + .Text = try conversation.textualBlockFromSlice(self.allocator, s), + }), + .ToolUse => |tu| { + const id = try self.allocator.dupe(u8, tu.id); + errdefer self.allocator.free(id); + const name = try self.allocator.dupe(u8, tu.name); + errdefer self.allocator.free(name); + var input_buf: conversation.TextualBlock = .empty; + errdefer input_buf.deinit(self.allocator); + try input_buf.appendSlice(self.allocator, tu.input); + try blocks.append(self.allocator, .{ .ToolUse = .{ + .id = id, + .name = name, + .input = input_buf, + } }); + }, + } + } + const moved = try blocks.toOwnedSlice(self.allocator); + defer self.allocator.free(moved); + try self.conv.addAssistantMessage(moved, self.turn.usage); + + const msg = self.conv.messages.items[self.conv.messages.items.len - 1]; + try out.push(.{ .message_complete = .{ .message = msg, .usage = self.turn.usage } }); + return .response_complete; + } + + fn deinitVT(ptr: *anyopaque) void { + const self: *StubResponse = @ptrCast(@alignCast(ptr)); + self.allocator.destroy(self); + } +}; + +fn stubOpenStream( + allocator: Allocator, + _: Io, + _: *const config_mod.Config, + _: *const ToolRegistry, + conv: *conversation.Conversation, + diag: ?*provider_mod.ProviderDiagnostic, +) anyerror!provider_mod.ProviderStream { + const self = stub_active orelse return error.NoStubInstalled; + self.calls_made += 1; + if (self.error_idx < self.scripted_errors.len) { + const e = self.scripted_errors[self.error_idx]; + self.error_idx += 1; + if (diag) |d| { + d.status_code = e.status_code; + d.retry_after_ms = e.retry_after_ms; + } + return e.err; + } + if (self.stream_error_idx < self.stream_errors.len) { + const f = self.stream_errors[self.stream_error_idx]; + self.stream_error_idx += 1; + return StubResponse.createFailing(allocator, conv, f.err, f.message); + } + if (self.overflow_calls > 0) { + self.overflow_calls -= 1; + return error.ContextOverflow; + } + if (self.next >= self.scripted.len) return error.NoMoreScriptedTurns; + const turn = self.scripted[self.next]; + self.next += 1; + return StubResponse.create(allocator, conv, turn); +} + +/// Build a stack registry + active `Config` snapshot for tests that drive +/// the agent. Post-R1 the registry no longer lives on `Config` — it lives +/// on the `Agent`. The harness still owns a registry so tests can pre-stage +/// tools and copy them onto the agent (`seed`) after `init`. The caller +/// owns both and must keep them alive for the agent's lifetime. +const TestHarness = struct { + registry: ToolRegistry, + config: config_mod.Config, + + fn init(allocator: Allocator) TestHarness { + return .{ .registry = ToolRegistry.init(allocator), .config = undefined }; + } + + /// Finalize the config snapshot. Must be called after `init` and before + /// constructing the agent, once the harness has a stable address. + fn activate(self: *TestHarness) void { + self.config = .{ + .provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m" } }, + }; + } + + /// Move the tools pre-staged on the harness registry onto a freshly + /// `init`ed agent's own registry. Post-R1 the agent owns its tool set, + /// so tests stage tools on the harness then transplant them here. The + /// agent's empty registry is freed and replaced; the harness registry + /// is left empty (its `deinit` becomes a no-op free). + fn seedInto(self: *TestHarness, agent: *Agent) void { + agent._registry.deinit(); + agent._registry = self.registry; + self.registry = ToolRegistry.init(self.registry.allocator); + } + + fn deinit(self: *TestHarness) void { + self.registry.deinit(); + } +}; + +const EchoTool = struct { + prefix_owned: []u8, + name_owned: []u8, + + fn create(allocator: Allocator, name: []const u8, prefix: []const u8) !Tool { + const self = try allocator.create(EchoTool); + errdefer allocator.destroy(self); + self.name_owned = try allocator.dupe(u8, name); + errdefer allocator.free(self.name_owned); + self.prefix_owned = try allocator.dupe(u8, prefix); + return .{ + .decl = .{ + .name = self.name_owned, + .description = "echo", + .schema_json = "{}", + }, + .ctx = self, + .vtable = &vt, + }; + } + + const vt: Tool.VTable = .{ .invoke = invoke, .deinit = deinit }; + + fn invoke(ctx: *anyopaque, input: []const u8, allocator: Allocator) anyerror!tool_mod.ResultParts { + const self: *EchoTool = @ptrCast(@alignCast(ctx)); + const msg = try std.fmt.allocPrint(allocator, "{s}{s}", .{ self.prefix_owned, input }); + return tool_mod.ResultParts.fromTextOwned(allocator, msg); + } + + fn deinit(ctx: *anyopaque, allocator: Allocator) void { + const self: *EchoTool = @ptrCast(@alignCast(ctx)); + allocator.free(self.name_owned); + allocator.free(self.prefix_owned); + allocator.destroy(self); + } +}; + +const BarrierTool = struct { + name_owned: []u8, + barrier: *Barrier, + + const Barrier = struct { + target: u32, + arrived: std.atomic.Value(u32) = .init(0), + thread_ids: [4]std.atomic.Value(u64) = .{ + .init(0), .init(0), .init(0), .init(0), + }, + }; + + fn create(allocator: Allocator, name: []const u8, barrier: *Barrier) !Tool { + const self = try allocator.create(BarrierTool); + errdefer allocator.destroy(self); + self.name_owned = try allocator.dupe(u8, name); + self.barrier = barrier; + return .{ + .decl = .{ + .name = self.name_owned, + .description = "barrier", + .schema_json = "{}", + }, + .ctx = self, + .vtable = &vt, + }; + } + + const vt: Tool.VTable = .{ .invoke = invoke, .deinit = deinit }; + + fn invoke(ctx: *anyopaque, _: []const u8, allocator: Allocator) anyerror!tool_mod.ResultParts { + const self: *BarrierTool = @ptrCast(@alignCast(ctx)); + const arrived = self.barrier.arrived.fetchAdd(1, .acq_rel); + if (arrived < self.barrier.thread_ids.len) { + self.barrier.thread_ids[arrived].store(std.Thread.getCurrentId(), .release); + } + + var i: usize = 0; + while (self.barrier.arrived.load(.acquire) < self.barrier.target) : (i += 1) { + if (i > 50_000) return error.BarrierTimeout; + std.Thread.yield() catch {}; + } + return tool_mod.ResultParts.fromText(allocator, "done"); + } + + fn deinit(ctx: *anyopaque, allocator: Allocator) void { + const self: *BarrierTool = @ptrCast(@alignCast(ctx)); + allocator.free(self.name_owned); + allocator.destroy(self); + } +}; + +const FailingTool = struct { + name_owned: []u8, + + fn create(allocator: Allocator, name: []const u8) !Tool { + const self = try allocator.create(FailingTool); + errdefer allocator.destroy(self); + self.name_owned = try allocator.dupe(u8, name); + return .{ + .decl = .{ + .name = self.name_owned, + .description = "fails", + .schema_json = "{}", + }, + .ctx = self, + .vtable = &vt, + }; + } + + const vt: Tool.VTable = .{ .invoke = invoke, .deinit = deinit }; + + fn invoke(_: *anyopaque, _: []const u8, _: Allocator) anyerror!tool_mod.ResultParts { + return error.ToolExploded; + } + + fn deinit(ctx: *anyopaque, allocator: Allocator) void { + const self: *FailingTool = @ptrCast(@alignCast(ctx)); + allocator.free(self.name_owned); + allocator.destroy(self); + } +}; + +/// A tool that returns a hard host failure (`error.Canceled`), which must +/// abort the whole turn rather than degrade into a tool result. +const HardFailTool = struct { + name_owned: []u8, + + fn create(allocator: Allocator, name: []const u8) !Tool { + const self = try allocator.create(HardFailTool); + errdefer allocator.destroy(self); + self.name_owned = try allocator.dupe(u8, name); + return .{ + .decl = .{ .name = self.name_owned, .description = "hard fail", .schema_json = "{}" }, + .ctx = self, + .vtable = &vt, + }; + } + + const vt: Tool.VTable = .{ .invoke = invoke, .deinit = deinit }; + + fn invoke(_: *anyopaque, _: []const u8, _: Allocator) anyerror!tool_mod.ResultParts { + return error.Canceled; + } + + fn deinit(ctx: *anyopaque, allocator: Allocator) void { + const self: *HardFailTool = @ptrCast(@alignCast(ctx)); + allocator.free(self.name_owned); + allocator.destroy(self); + } +}; + +/// An in-memory `SessionStore` test double: records every appended +/// `StoredMessage` (role + provider/model stamp) so tests can assert the +/// agent persisted the right turn without touching disk. Honors the store +/// ownership contract by freeing each consumed message after recording its +/// salient fields. +const CapturingStore = struct { + allocator: Allocator, + roles: std.ArrayList(conversation.MessageRole) = .empty, + base_urls: std.ArrayList([]const u8) = .empty, + + fn init(allocator: Allocator) CapturingStore { + return .{ .allocator = allocator }; + } + + fn deinit(self: *CapturingStore) void { + for (self.base_urls.items) |s| self.allocator.free(s); + self.base_urls.deinit(self.allocator); + self.roles.deinit(self.allocator); + } + + fn createVT(ctx: *anyopaque) session_store_mod.Session { + const self: *CapturingStore = @ptrCast(@alignCast(ctx)); + const a = self.allocator; + const info: session_store_mod.SessionInfo = .{ + .id = a.dupe(u8, "cap") catch "cap", + .created = a.dupe(u8, "") catch "", + .modified = a.dupe(u8, "") catch "", + .message_count = 0, + .last_user_message = a.dupe(u8, "") catch "", + .api_style = .openai_chat, + .base_url = a.dupe(u8, "") catch "", + .model = a.dupe(u8, "") catch "", + .reasoning = .default, + }; + return .{ .info = info, .store = self.store() }; + } + + fn listVT(ctx: *anyopaque) anyerror![]session_store_mod.SessionInfo { + const self: *CapturingStore = @ptrCast(@alignCast(ctx)); + return self.allocator.alloc(session_store_mod.SessionInfo, 0); + } + fn freeSessionInfosVT(ctx: *anyopaque, infos: []session_store_mod.SessionInfo) void { + const self: *CapturingStore = @ptrCast(@alignCast(ctx)); + for (infos) |i| i.deinit(self.allocator); + self.allocator.free(infos); + } + fn resolveVT(_: *anyopaque, _: []const u8) anyerror!?session_store_mod.Session { + return null; + } + fn latestVT(_: *anyopaque) anyerror!?session_store_mod.Session { + return null; + } + fn loadVT(_: *anyopaque, _: []const u8) anyerror!?conversation.Conversation { + return null; + } + + fn appendMessagesVT( + ctx: *anyopaque, + _: []const u8, + messages: []session_store_mod.PersistentMessage, + ) anyerror!void { + const self: *CapturingStore = @ptrCast(@alignCast(ctx)); + for (messages) |m| { + try self.roles.append(self.allocator, m.message.role); + try self.base_urls.append(self.allocator, try self.allocator.dupe(u8, m.identity.base_url)); + } + } + + const vtable: session_store_mod.SessionStore.VTable = .{ + .create = createVT, + .list = listVT, + .freeSessionInfos = freeSessionInfosVT, + .resolve = resolveVT, + .latest = latestVT, + .load = loadVT, + .appendMessages = appendMessagesVT, + }; + + fn store(self: *CapturingStore) session_store_mod.SessionStore { + return .{ .ptr = self, .vtable = &vtable }; + } +}; + +test "agent persists user, assistant, and tool-result messages of a turn" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "tc_1", .name = "echo", .input = "hello" } }, + } }, + .{ .blocks = &.{ + .{ .Text = "ok" }, + } }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try EchoTool.create(allocator, "echo", "ECHO:")); + h.activate(); + + var cap = CapturingStore.init(allocator); + defer cap.deinit(); + const agent = try Agent.init(allocator, io, &h.config, cap.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + try drainTurn(agent, "call a tool"); + + // Persisted, in order: user prompt, assistant(ToolUse), user(ToolResult), + // assistant(text). + try testing.expectEqual(@as(usize, 4), cap.roles.items.len); + try testing.expectEqual(conversation.MessageRole.user, cap.roles.items[0]); + try testing.expectEqual(conversation.MessageRole.assistant, cap.roles.items[1]); + try testing.expectEqual(conversation.MessageRole.user, cap.roles.items[2]); + try testing.expectEqual(conversation.MessageRole.assistant, cap.roles.items[3]); + + // The wire identity (base_url from the active config) rode through on + // every entry. + for (cap.base_urls.items) |b| { + try testing.expectEqualStrings("u", b); + } +} + +test "interruption: persistence stays current and excludes a dangling tool call" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "tc_1", .name = "echo", .input = "hello" } }, + } }, + .{ .blocks = &.{ + .{ .Text = "ok" }, + } }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try EchoTool.create(allocator, "echo", "ECHO:")); + h.activate(); + + var cap = CapturingStore.init(allocator); + defer cap.deinit(); + const agent = try Agent.init(allocator, io, &h.config, cap.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + var s = try runUserText(agent, "call a tool"); + + // `run` persisted the user prompt up front, and nothing else yet. + try testing.expectEqual(@as(usize, 1), cap.roles.items.len); + try testing.expectEqual(conversation.MessageRole.user, cap.roles.items[0]); + + // Pull until the assistant's tool-call message completes, then break — the + // embedder's interruption point (e.g. to handle the tool calls itself). + while (try s.next()) |ev| { + if (ev == .message_complete) break; + } + + // The tool-call message is live in the conversation... + const msgs = agent.conversation.messages.items; + try testing.expectEqual(conversation.MessageRole.assistant, msgs[msgs.len - 1].role); + try testing.expect(Agent.hasToolUseBlock(msgs[msgs.len - 1])); + // ...but it is a dangling tool call (no results), so the store still holds + // only the coherent prefix: the interruption guarantee. + try testing.expectEqual(@as(usize, 1), cap.roles.items.len); + + // Dropping the stream mid-turn must not persist the dangling call either. + s.deinit(); + try testing.expectEqual(@as(usize, 1), cap.roles.items.len); + + // The conversation is coherent enough to resume from: feeding the tool + // result as a fresh user turn resolves the dangling call, and now both the + // assistant tool-call and the user result become persistable. + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "42") }); + var s2 = try agent.run(.{ .blocks = &.{ + .{ .ToolResult = .{ + .tool_use_id = try allocator.dupe(u8, "tc_1"), + .parts = parts, + .is_error = false, + } }, + } }); + defer s2.deinit(); + // user prompt, assistant(ToolUse), user(ToolResult) are all persisted once + // the dangling call is resolved by the new user turn. + try testing.expectEqual(@as(usize, 3), cap.roles.items.len); + try testing.expectEqual(conversation.MessageRole.user, cap.roles.items[0]); + try testing.expectEqual(conversation.MessageRole.assistant, cap.roles.items[1]); + try testing.expectEqual(conversation.MessageRole.user, cap.roles.items[2]); +} + +test "agent runs a turn against NullStore without persisting or erroring" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "hi" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + try drainTurn(agent, "hello"); + + // Nothing crashed; the conversation has the user + assistant messages. + try testing.expectEqual(@as(usize, 2), agent.conversation.messages.items.len); +} + +test "override mutators rewrite the conversation tail's tool blocks" { + const allocator = testing.allocator; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + + const conv = &agent.conversation; + // Committed assistant message carrying one ToolUse. + var a_content: std.ArrayList(conversation.ContentBlock) = .empty; + try a_content.append(allocator, .{ .ToolUse = .{ + .id = try allocator.dupe(u8, "tc_1"), + .name = try allocator.dupe(u8, "read"), + .input = try conversation.textualBlockFromSlice(allocator, "{\"path\":\"orig\"}"), + } }); + try conv.messages.append(allocator, .{ .role = .assistant, .content = a_content }); + + // Input override rewrites the tail's matching ToolUse; unknown ids miss. + try testing.expect(try agent.overrideToolUseInput("tc_1", "{\"path\":\"new\"}")); + try testing.expect(!try agent.overrideToolUseInput("tc_missing", "{}")); + const tu = conv.messages.items[0].content.items[0].ToolUse; + try testing.expectEqualStrings("{\"path\":\"new\"}", tu.input.items); + + // Tool-result user message: a text part followed by a media part. + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "original output") }); + try parts.append(allocator, .{ .media = .{ + .media_type = try allocator.dupe(u8, "image/png"), + .data = try conversation.textualBlockFromSlice(allocator, "aW1n"), + } }); + var u_content: std.ArrayList(conversation.ContentBlock) = .empty; + try u_content.append(allocator, .{ .ToolResult = .{ + .tool_use_id = try allocator.dupe(u8, "tc_1"), + .parts = parts, + } }); + try conv.messages.append(allocator, .{ .role = .user, .content = u_content }); + + // The ToolUse override only targets the tail — now a user message. + try testing.expect(!try agent.overrideToolUseInput("tc_1", "{}")); + + // Output override replaces the text, keeps the media part. + try testing.expect(try agent.overrideToolResultOutput("tc_1", "replaced output")); + try testing.expect(!try agent.overrideToolResultOutput("tc_other", "x")); + const tr = conv.messages.items[1].content.items[0].ToolResult; + try testing.expectEqual(@as(usize, 2), tr.parts.items.len); + try testing.expectEqualStrings("replaced output", tr.parts.items[0].text.items); + try testing.expectEqualStrings("image/png", tr.parts.items[1].media.media_type); +} + +/// A configurable ToolSource for testing the grouped-dispatch path. +/// Stores every batch it receives so tests can assert "calls X and Y +/// arrived in the same batch on the same thread". +const TestSource = struct { + name_owned: []u8, + decls: []tool_source_mod.ToolDecl, + decl_strings: std.array_list.Managed([]u8), + /// Sequence of (thread_id, [tool_name; n]) per batch received. + /// Only mutated inside `invoke_batch`. Because libpanto guarantees + /// at most one outstanding `invoke_batch` per source at any time + /// (one batch per turn per source), no synchronization is needed. + batches: std.array_list.Managed(Batch), + allocator: Allocator, + + const Batch = struct { + thread_id: u64, + names: std.array_list.Managed([]u8), + }; + + fn create( + allocator: Allocator, + source_name: []const u8, + tool_names: []const []const u8, + ) !ToolSource { + const self = try allocator.create(TestSource); + errdefer allocator.destroy(self); + + var strings = std.array_list.Managed([]u8).init(allocator); + errdefer { + for (strings.items) |s| allocator.free(s); + strings.deinit(); + } + + const name_owned = try allocator.dupe(u8, source_name); + try strings.append(name_owned); + + const decls = try allocator.alloc(tool_source_mod.ToolDecl, tool_names.len); + errdefer allocator.free(decls); + for (tool_names, 0..) |tn, i| { + const n = try allocator.dupe(u8, tn); + try strings.append(n); + const d = try allocator.dupe(u8, "test src tool"); + try strings.append(d); + const s = try allocator.dupe(u8, "{}"); + try strings.append(s); + decls[i] = .{ .name = n, .description = d, .schema_json = s }; + } + + self.* = .{ + .name_owned = name_owned, + .decls = decls, + .decl_strings = strings, + .batches = std.array_list.Managed(Batch).init(allocator), + .allocator = allocator, + }; + + return ToolSource{ + .name = self.name_owned, + .tools = self.decls, + .ctx = self, + .vtable = &vt, + }; + } + + const vt: ToolSource.VTable = .{ + .invoke_batch = invokeBatch, + .deinit = deinitSrc, + }; + + fn invokeBatch( + ctx: *anyopaque, + calls: []const tool_source_mod.Call, + results: []tool_source_mod.CallResult, + allocator: Allocator, + ) anyerror!void { + const self: *TestSource = @ptrCast(@alignCast(ctx)); + var batch: Batch = .{ + .thread_id = std.Thread.getCurrentId(), + .names = std.array_list.Managed([]u8).init(self.allocator), + }; + for (calls) |c| { + const copy = try self.allocator.dupe(u8, c.tool_name); + try batch.names.append(copy); + } + try self.batches.append(batch); + + for (calls, 0..) |c, i| { + const msg = std.fmt.allocPrint( + allocator, + "{s}->{s}", + .{ c.tool_name, c.input }, + ) catch |e| { + results[i] = .{ .err = e }; + continue; + }; + results[i] = .{ + .ok = tool_mod.ResultParts.fromTextOwned(allocator, msg) catch |e| { + results[i] = .{ .err = e }; + continue; + }, + }; + } + } + + fn deinitSrc(ctx: *anyopaque, _: Allocator) void { + const self: *TestSource = @ptrCast(@alignCast(ctx)); + for (self.decl_strings.items) |s| self.allocator.free(s); + self.decl_strings.deinit(); + for (self.batches.items) |*b| { + for (b.names.items) |n| self.allocator.free(n); + b.names.deinit(); + } + self.batches.deinit(); + self.allocator.free(self.decls); + self.allocator.destroy(self); + } +}; + +/// A source that always fails the whole batch by returning an error +/// from invoke_batch (rather than recording per-call errors). Used to +/// verify libpanto's whole-batch-failure path. +const FailingSource = struct { + name_owned: []u8, + decls: []tool_source_mod.ToolDecl, + decl_strings: std.array_list.Managed([]u8), + allocator: Allocator, + + fn create(allocator: Allocator, source_name: []const u8, tool_names: []const []const u8) !ToolSource { + const self = try allocator.create(FailingSource); + errdefer allocator.destroy(self); + + var strings = std.array_list.Managed([]u8).init(allocator); + errdefer { + for (strings.items) |s| allocator.free(s); + strings.deinit(); + } + + const name_owned = try allocator.dupe(u8, source_name); + try strings.append(name_owned); + + const decls = try allocator.alloc(tool_source_mod.ToolDecl, tool_names.len); + errdefer allocator.free(decls); + for (tool_names, 0..) |tn, i| { + const n = try allocator.dupe(u8, tn); + try strings.append(n); + const d = try allocator.dupe(u8, "fails"); + try strings.append(d); + const s = try allocator.dupe(u8, "{}"); + try strings.append(s); + decls[i] = .{ .name = n, .description = d, .schema_json = s }; + } + + self.* = .{ + .name_owned = name_owned, + .decls = decls, + .decl_strings = strings, + .allocator = allocator, + }; + return ToolSource{ .name = self.name_owned, .tools = self.decls, .ctx = self, .vtable = &vt }; + } + + const vt: ToolSource.VTable = .{ .invoke_batch = invokeBatch, .deinit = deinitSrc }; + + fn invokeBatch( + _: *anyopaque, + _: []const tool_source_mod.Call, + _: []tool_source_mod.CallResult, + _: Allocator, + ) anyerror!void { + return error.SourceExploded; + } + + fn deinitSrc(ctx: *anyopaque, _: Allocator) void { + const self: *FailingSource = @ptrCast(@alignCast(ctx)); + for (self.decl_strings.items) |s| self.allocator.free(s); + self.decl_strings.deinit(); + self.allocator.free(self.decls); + self.allocator.destroy(self); + } +}; + +/// A source that succeeds the first member call and fails the rest with a +/// per-call error (returning void from `invoke_batch`). Exercises the +/// per-call error path distinct from a whole-batch failure. +const PartialSource = struct { + name_owned: []u8, + decls: []tool_source_mod.ToolDecl, + decl_strings: std.array_list.Managed([]u8), + allocator: Allocator, + + fn create(allocator: Allocator, source_name: []const u8, tool_names: []const []const u8) !ToolSource { + const self = try allocator.create(PartialSource); + errdefer allocator.destroy(self); + var strings = std.array_list.Managed([]u8).init(allocator); + errdefer { + for (strings.items) |s| allocator.free(s); + strings.deinit(); + } + const name_owned = try allocator.dupe(u8, source_name); + try strings.append(name_owned); + const decls = try allocator.alloc(tool_source_mod.ToolDecl, tool_names.len); + errdefer allocator.free(decls); + for (tool_names, 0..) |tn, i| { + const n = try allocator.dupe(u8, tn); + try strings.append(n); + const d = try allocator.dupe(u8, "partial"); + try strings.append(d); + const s = try allocator.dupe(u8, "{}"); + try strings.append(s); + decls[i] = .{ .name = n, .description = d, .schema_json = s }; + } + self.* = .{ .name_owned = name_owned, .decls = decls, .decl_strings = strings, .allocator = allocator }; + return ToolSource{ .name = self.name_owned, .tools = self.decls, .ctx = self, .vtable = &vt }; + } + + const vt: ToolSource.VTable = .{ .invoke_batch = invokeBatch, .deinit = deinitSrc }; + + fn invokeBatch( + _: *anyopaque, + calls: []const tool_source_mod.Call, + results: []tool_source_mod.CallResult, + allocator: Allocator, + ) anyerror!void { + for (calls, 0..) |_, j| { + if (j == 0) { + results[j] = .{ .ok = try tool_mod.ResultParts.fromText(allocator, "ok") }; + } else { + results[j] = .{ .err = error.PerCallBoom }; + } + } + } + + fn deinitSrc(ctx: *anyopaque, _: Allocator) void { + const self: *PartialSource = @ptrCast(@alignCast(ctx)); + for (self.decl_strings.items) |s| self.allocator.free(s); + self.decl_strings.deinit(); + self.allocator.free(self.decls); + self.allocator.destroy(self); + } +}; + +test "registry register and lookup" { + var h = TestHarness.init(testing.allocator); + defer h.deinit(); + try h.registry.register(try EchoTool.create(testing.allocator, "echo", "ECHO:")); + try testing.expectEqual(@as(usize, 1), h.registry.count()); + try testing.expect(h.registry.lookup("echo") != null); +} + +test "duplicate register returns error" { + var h = TestHarness.init(testing.allocator); + defer h.deinit(); + try h.registry.register(try EchoTool.create(testing.allocator, "echo", "A:")); + + var dup = try EchoTool.create(testing.allocator, "echo", "B:"); + try testing.expectError(error.DuplicateTool, h.registry.register(dup)); + dup.vtable.deinit(dup.ctx, testing.allocator); +} + +test "runStep dispatches a tool call and loops to a final text turn" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "tc_1", .name = "echo", .input = "hello" } }, + } }, + .{ .blocks = &.{ + .{ .Text = "ok" }, + } }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try EchoTool.create(allocator, "echo", "ECHO:")); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try drainTurn(agent, "call a tool"); + + try testing.expectEqual(@as(usize, 4), conv.messages.items.len); + + try testing.expectEqual(conversation.MessageRole.assistant, conv.messages.items[1].role); + try testing.expectEqual(@as(usize, 1), conv.messages.items[1].content.items.len); + try testing.expectEqualStrings("tc_1", conv.messages.items[1].content.items[0].ToolUse.id); + + try testing.expectEqual(conversation.MessageRole.user, conv.messages.items[2].role); + try testing.expectEqual(@as(usize, 1), conv.messages.items[2].content.items.len); + const tr = conv.messages.items[2].content.items[0].ToolResult; + try testing.expectEqualStrings("tc_1", tr.tool_use_id); + try testing.expectEqualStrings("ECHO:hello", trText(tr)); + + try testing.expectEqual(conversation.MessageRole.assistant, conv.messages.items[3].role); + try testing.expectEqualStrings("ok", conv.messages.items[3].content.items[0].Text.items); +} + +test "Stream emits tool_dispatch_start before running tools" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "tc_1", .name = "echo", .input = "hello" } }, + } }, + .{ .blocks = &.{.{ .Text = "ok" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try EchoTool.create(allocator, "echo", "ECHO:")); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + var s = try runUserText(agent, "call a tool"); + defer s.deinit(); + + const first = (try s.next()).?; + try testing.expect(first == .message_complete); + try testing.expectEqual(@as(usize, 2), agent.conversation.messages.items.len); + + const start = (try s.next()).?; + try testing.expect(start == .tool_dispatch_start); + try testing.expectEqual(@as(usize, 1), start.tool_dispatch_start.count); + // The ToolResult user message must not exist yet; otherwise callers do + // not get a chance to render the tool as running before dispatch blocks. + try testing.expectEqual(@as(usize, 2), agent.conversation.messages.items.len); + + const complete = (try s.next()).?; + try testing.expect(complete == .tool_dispatch_complete); + try testing.expectEqual(@as(usize, 3), agent.conversation.messages.items.len); +} + +test "runStep dispatches multiple tool calls in parallel" { + const allocator = testing.allocator; + + var barrier: BarrierTool.Barrier = .{ .target = 3 }; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "a", .name = "barrierA", .input = "" } }, + .{ .ToolUse = .{ .id = "b", .name = "barrierB", .input = "" } }, + .{ .ToolUse = .{ .id = "c", .name = "barrierC", .input = "" } }, + } }, + .{ .blocks = &.{ + .{ .Text = "done" }, + } }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try BarrierTool.create(allocator, "barrierA", &barrier)); + try h.registry.register(try BarrierTool.create(allocator, "barrierB", &barrier)); + try h.registry.register(try BarrierTool.create(allocator, "barrierC", &barrier)); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try drainTurn(agent, "go"); + + const tr_msg = conv.messages.items[2]; + try testing.expectEqual(@as(usize, 3), tr_msg.content.items.len); + try testing.expectEqualStrings("a", tr_msg.content.items[0].ToolResult.tool_use_id); + try testing.expectEqualStrings("b", tr_msg.content.items[1].ToolResult.tool_use_id); + try testing.expectEqualStrings("c", tr_msg.content.items[2].ToolResult.tool_use_id); + + const t0 = barrier.thread_ids[0].load(.acquire); + const t1 = barrier.thread_ids[1].load(.acquire); + const t2 = barrier.thread_ids[2].load(.acquire); + try testing.expect(t0 != 0 and t1 != 0 and t2 != 0); + try testing.expect(t0 != t1 and t1 != t2 and t0 != t2); +} + +test "runStep: native tool handler error becomes an error result and the model gets another turn" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "x", .name = "boom", .input = "" } }, + } }, + .{ .blocks = &.{.{ .Text = "i will recover" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try FailingTool.create(allocator, "boom")); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try drainTurn(agent, "break it"); + + // user, assistant(tool_use), user(tool_result), assistant(text) + try testing.expectEqual(@as(usize, 4), conv.messages.items.len); + const tr = conv.messages.items[2].content.items[0].ToolResult; + try testing.expectEqualStrings("x", tr.tool_use_id); + try testing.expect(tr.is_error); + try testing.expect(std.mem.indexOf(u8, trText(tr), "ToolExploded") != null); +} + +test "runStep: unknown tool becomes an error tool result and the loop continues" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "z", .name = "ghost", .input = "" } }, + } }, + .{ .blocks = &.{.{ .Text = "ok, that tool does not exist" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try drainTurn(agent, "call a ghost"); + + // messages: user, assistant(tool_use), user(tool_result), assistant(text) + try testing.expectEqual(@as(usize, 4), conv.messages.items.len); + const tr_msg = conv.messages.items[2]; + try testing.expectEqual(conversation.MessageRole.user, tr_msg.role); + const tr = tr_msg.content.items[0].ToolResult; + try testing.expectEqualStrings("z", tr.tool_use_id); + try testing.expect(tr.is_error); + try testing.expect(std.mem.indexOf(u8, trText(tr), "UnknownTool") != null); +} + +test "runStep with no tool calls returns after one provider step" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "hi" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try drainTurn(agent, "hello"); + + try testing.expectEqual(@as(usize, 2), conv.messages.items.len); + try testing.expectEqualStrings("hi", conv.messages.items[1].content.items[0].Text.items); +} + +test "runStep surfaces EmptyAssistantResponse when provider commits an empty message" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + try testing.expectError(error.EmptyAssistantResponse, drainTurn(agent, "hi")); +} + +// ------------ ToolSource tests ------------ + +test "runStep delivers all source-backed calls in one batch on one thread" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "a", .name = "lua_x", .input = "1" } }, + .{ .ToolUse = .{ .id = "b", .name = "lua_y", .input = "2" } }, + .{ .ToolUse = .{ .id = "c", .name = "lua_x", .input = "3" } }, + } }, + .{ .blocks = &.{.{ .Text = "done" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.registerSource(try TestSource.create(allocator, "panto-lua", &.{ "lua_x", "lua_y" })); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try drainTurn(agent, "go"); + + // Locate the source and inspect its observed batches. + const view = agent._registry.lookup("lua_x") orelse return error.NotFound; + const src_ptr = view.entry.source.source; + const test_src: *TestSource = @ptrCast(@alignCast(src_ptr.ctx)); + + try testing.expectEqual(@as(usize, 1), test_src.batches.items.len); + const b = test_src.batches.items[0]; + try testing.expectEqual(@as(usize, 3), b.names.items.len); + try testing.expectEqualStrings("lua_x", b.names.items[0]); + try testing.expectEqualStrings("lua_y", b.names.items[1]); + try testing.expectEqualStrings("lua_x", b.names.items[2]); + + // ToolResults arrived in the original call order. + const tr_msg = conv.messages.items[2]; + try testing.expectEqual(@as(usize, 3), tr_msg.content.items.len); + try testing.expectEqualStrings("a", tr_msg.content.items[0].ToolResult.tool_use_id); + try testing.expectEqualStrings("lua_x->1", trText(tr_msg.content.items[0].ToolResult)); + try testing.expectEqualStrings("b", tr_msg.content.items[1].ToolResult.tool_use_id); + try testing.expectEqualStrings("lua_y->2", trText(tr_msg.content.items[1].ToolResult)); + try testing.expectEqualStrings("c", tr_msg.content.items[2].ToolResult.tool_use_id); + try testing.expectEqualStrings("lua_x->3", trText(tr_msg.content.items[2].ToolResult)); +} + +test "runStep: distinct sources run on distinct threads in parallel" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "a", .name = "src_a_t", .input = "" } }, + .{ .ToolUse = .{ .id = "b", .name = "src_b_t", .input = "" } }, + } }, + .{ .blocks = &.{.{ .Text = "done" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.registerSource(try TestSource.create(allocator, "src_a", &.{"src_a_t"})); + try h.registry.registerSource(try TestSource.create(allocator, "src_b", &.{"src_b_t"})); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + try drainTurn(agent, "go"); + + const view_a = agent._registry.lookup("src_a_t") orelse return error.NotFound; + const view_b = agent._registry.lookup("src_b_t") orelse return error.NotFound; + const sa: *TestSource = @ptrCast(@alignCast(view_a.entry.source.source.ctx)); + const sb: *TestSource = @ptrCast(@alignCast(view_b.entry.source.source.ctx)); + + try testing.expectEqual(@as(usize, 1), sa.batches.items.len); + try testing.expectEqual(@as(usize, 1), sb.batches.items.len); + // The two sources ran on distinct OS threads. + try testing.expect(sa.batches.items[0].thread_id != sb.batches.items[0].thread_id); +} + +test "runStep: source whole-batch error becomes per-call error results and continues" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "a", .name = "fa", .input = "" } }, + .{ .ToolUse = .{ .id = "b", .name = "fb", .input = "" } }, + } }, + .{ .blocks = &.{.{ .Text = "recovered" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.registerSource(try FailingSource.create(allocator, "fs", &.{ "fa", "fb" })); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try drainTurn(agent, "kaboom"); + + // user, assistant(tool_use x2), user(tool_result x2), assistant(text) + try testing.expectEqual(@as(usize, 4), conv.messages.items.len); + const tr_msg = conv.messages.items[2]; + try testing.expectEqual(@as(usize, 2), tr_msg.content.items.len); + // Every member of the failed batch produced an error result, in order. + const tr_a = tr_msg.content.items[0].ToolResult; + const tr_b = tr_msg.content.items[1].ToolResult; + try testing.expectEqualStrings("a", tr_a.tool_use_id); + try testing.expectEqualStrings("b", tr_b.tool_use_id); + try testing.expect(tr_a.is_error); + try testing.expect(tr_b.is_error); + try testing.expect(std.mem.indexOf(u8, trText(tr_a), "SourceExploded") != null); +} + +test "runStep: mixed single Tools and source-backed tools coexist in one turn" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "a", .name = "single", .input = "X" } }, + .{ .ToolUse = .{ .id = "b", .name = "src_t1", .input = "Y" } }, + .{ .ToolUse = .{ .id = "c", .name = "src_t2", .input = "Z" } }, + } }, + .{ .blocks = &.{.{ .Text = "done" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try EchoTool.create(allocator, "single", "S:")); + try h.registry.registerSource(try TestSource.create(allocator, "src", &.{ "src_t1", "src_t2" })); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try drainTurn(agent, "go"); + + const tr_msg = conv.messages.items[2]; + try testing.expectEqual(@as(usize, 3), tr_msg.content.items.len); + try testing.expectEqualStrings("S:X", trText(tr_msg.content.items[0].ToolResult)); + try testing.expectEqualStrings("src_t1->Y", trText(tr_msg.content.items[1].ToolResult)); + try testing.expectEqualStrings("src_t2->Z", trText(tr_msg.content.items[2].ToolResult)); +} + +test "setConfig swaps provider between turns; agent tool set persists" { + // Post-R1 the tool set lives on the `Agent`, not on `Config`. Swapping + // the config pointer (`setConfig`) changes provider/model at the next + // turn boundary but leaves the agent's registered tools intact: a turn + // after the swap still resolves a tool registered before it. + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .ToolUse = .{ .id = "2", .name = "late", .input = "B" } }} }, + .{ .blocks = &.{.{ .Text = "done" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + + const cfg_a: config_mod.Config = .{ + .provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "a", .model = "m" } }, + }; + const cfg_b: config_mod.Config = .{ + .provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "b", .model = "m" } }, + }; + + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &cfg_a, ns.store().create(), null); + defer agent.deinit(); + try agent.registerTool(try EchoTool.create(allocator, "late", "B:")); + agent._open_stream_fn = stub.install(); + + // The tool is visible regardless of which config is active. + try testing.expect(agent._registry.lookup("late") != null); + agent.setConfig(&cfg_b); + try testing.expect(agent._registry.lookup("late") != null); + + // A real turn after the swap still resolves `late`, then loops to the + // final text turn. + const conv = &agent.conversation; + try drainTurn(agent, "go"); + + const tr = conv.messages.items[2].content.items[0].ToolResult; + try testing.expectEqualStrings("2", tr.tool_use_id); + try testing.expectEqualStrings("B:B", trText(tr)); +} + +test "compact: summarizes prefix, keeps suffix, system survives" { + const allocator = testing.allocator; + + // The stub returns a single text turn — used as the summary text. + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "SUMMARY OF EARLIER" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + // keep_verbatim sized so only the last (short) turn fits: q2+a2 are + // 3 words each => ceil(3*1.3)=4 tokens each => 8 total <= 10, while + // adding the longer first turn exceeds it. + h.config.compaction = .{ .keep_verbatim = 10 }; + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + try conv.addSystemMessage("you are helpful"); + try addUserText(conv, "first question here with several words"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "first answer with several words") }, + }, null); + try addUserText(conv, "second recent question"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "second recent answer") }, + }, null); + + const res = try agent._compactInPlace("Summarize the conversation.", null); + try testing.expect(res.compacted); + + // Expected rebuilt: [system, compaction summary(user), user q2, asst a2] + try testing.expectEqual(@as(usize, 4), conv.messages.items.len); + try testing.expectEqual(conversation.MessageRole.system, conv.messages.items[0].role); + try testing.expectEqualStrings( + "you are helpful", + conv.messages.items[0].content.items[0].System.text.items, + ); + try testing.expectEqual(conversation.MessageRole.user, conv.messages.items[1].role); + try testing.expectEqualStrings( + "SUMMARY OF EARLIER", + conv.messages.items[1].content.items[0].CompactionSummary.text.items, + ); + try testing.expectEqualStrings( + "second recent question", + conv.messages.items[2].content.items[0].Text.items, + ); + try testing.expectEqualStrings( + "second recent answer", + conv.messages.items[3].content.items[0].Text.items, + ); +} + +test "interruption: resuming next() after a break dispatches the pending tools" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "tc_1", .name = "echo", .input = "hello" } }, + } }, + .{ .blocks = &.{ + .{ .Text = "ok" }, + } }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try EchoTool.create(allocator, "echo", "ECHO:")); + h.activate(); + + var cap = CapturingStore.init(allocator); + defer cap.deinit(); + const agent = try Agent.init(allocator, io, &h.config, cap.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + var s = try runUserText(agent, "call a tool"); + defer s.deinit(); + + // Break right after the assistant's tool-call message completes — the + // stream is paused, NOT closed. The tool has not been dispatched. + while (try s.next()) |ev| { + if (ev == .message_complete) break; + } + try testing.expectEqual(@as(usize, 1), cap.roles.items.len); // only the prompt + + // Resuming `next()` on the same stream picks up where it left off and runs + // the agent's registered-tool machinery: no new user turn required. + var saw_dispatch = false; + while (try s.next()) |ev| { + if (ev == .tool_dispatch_complete) saw_dispatch = true; + } + try testing.expect(saw_dispatch); + + // The full turn is now persisted: prompt, assistant(ToolUse), + // user(ToolResult), assistant(text). + try testing.expectEqual(@as(usize, 4), cap.roles.items.len); + try testing.expectEqual(conversation.MessageRole.user, cap.roles.items[2]); +} + +test "compact: tolerates a trailing dangling tool call (interrupted turn)" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "SUMMARY OF EARLIER" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + h.config.compaction = .{ .keep_verbatim = 10 }; + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + try conv.addSystemMessage("you are helpful"); + try addUserText(conv, "first question here with several words"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "first answer with several words") }, + }, null); + // A fresh turn whose assistant message ends in a tool call with no results + // yet — exactly the state a break right after `message_complete` leaves. + try addUserText(conv, "second recent question"); + try conv.addAssistantMessage(&.{ + .{ .ToolUse = .{ + .id = try allocator.dupe(u8, "tc_1"), + .name = try allocator.dupe(u8, "echo"), + .input = try conversation.textualBlockFromSlice(allocator, "hello"), + } }, + }, null); + + // Compaction must not choke on the dangling tail: it summarizes the older + // turn and keeps the interrupted turn (incl. the dangling call) verbatim. + const res = try agent._compactInPlace("Summarize the conversation.", null); + try testing.expect(res.compacted); + + // The dangling tool call survives as the tail, intact and resumable. + const last = conv.messages.items[conv.messages.items.len - 1]; + try testing.expectEqual(conversation.MessageRole.assistant, last.role); + try testing.expect(Agent.hasToolUseBlock(last)); + try testing.expectEqualStrings("tc_1", last.content.items[0].ToolUse.id); +} + +test "compact: restated suffix usage reconstructs a fresh cumulative chain" { + const allocator = testing.allocator; + + // The compaction turn reports a known output token count (the summary + // size used as the new anchor). + const scripted = [_]StubProvider.ScriptedTurn{ + .{ + .blocks = &.{.{ .Text = "SUMMARY" }}, + .usage = .{ .input = 9999, .output = 100 }, // input is ignored; only output anchors + }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + // Budget keeps the last two turns verbatim (deltas 8 + 6 = 14) but + // summarizes the prefix. + h.config.compaction = .{ .keep_verbatim = 20 }; + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + // Prefix turn (will be summarized). Cumulative footprint = 500+40+10+50 + // = 600. Its real usage (with cache buckets) is irrelevant + // post-compaction. + try addUserText(conv, "first question here with several words"); + try conv.addAssistantMessage( + &.{.{ .Text = try conversation.textualBlockFromSlice(allocator, "first answer with words") }}, + .{ .input = 500, .output = 50, .cache_read = 40, .cache_write = 10 }, + ); + // Kept turn 1: cumulative 608 (delta 8 over prefix). Reasoning is a + // subset of output. + try addUserText(conv, "kept question"); // 2 words => ceil(2*1.3)=3 tokens + try conv.addAssistantMessage( + &.{.{ .Text = try conversation.textualBlockFromSlice(allocator, "kept answer") }}, + .{ .input = 600, .output = 8, .reasoning = 5 }, + ); + // Kept turn 2: cumulative 614 (delta 6). Cache buckets present to prove + // they collapse into `input` on restatement. + try addUserText(conv, "two more words here"); // 4 words => ceil(4*1.3)=6 + try conv.addAssistantMessage( + &.{.{ .Text = try conversation.textualBlockFromSlice(allocator, "final answer") }}, + .{ .input = 600, .output = 4, .cache_read = 8, .cache_write = 2 }, + ); + + const res = try agent._compactInPlace("Summarize.", null); + try testing.expect(res.compacted); + + // Rebuilt: [summary(user), u1, a1, u2, a2]. + try testing.expectEqual(@as(usize, 5), conv.messages.items.len); + const asst1 = conv.messages.items[2]; + const asst2 = conv.messages.items[4]; + try testing.expect(conv.messages.items[1].usage == null); // kept user + + // First restated assistant: input = summary_size(100) + + // user_size("kept question" => 3) = 103. Cache zeroed; output/reasoning + // verbatim. + const us1 = asst1.usage.?; + try testing.expectEqual(@as(u64, 103), us1.input); + try testing.expectEqual(@as(u64, 0), us1.cache_read); + try testing.expectEqual(@as(u64, 0), us1.cache_write); + try testing.expectEqual(@as(u64, 8), us1.output); + try testing.expectEqual(@as(u64, 5), us1.reasoning); + + // Second restated assistant: input = prev.input(103) + prev.output(8) + + // user_size("two more words here" => 6) = 117. Original cache buckets + // (8+2) collapse away; only the synthetic cumulative total survives in + // `input`. + const us2 = asst2.usage.?; + try testing.expectEqual(@as(u64, 117), us2.input); + try testing.expectEqual(@as(u64, 0), us2.cache_read); + try testing.expectEqual(@as(u64, 0), us2.cache_write); + try testing.expectEqual(@as(u64, 4), us2.output); +} + +test "compact: no-op when conversation already fits the budget" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "should not be used" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + h.config.compaction = .{ .keep_verbatim = 1_000_000 }; + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + try conv.addSystemMessage("sys"); + try addUserText(conv, "hi"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "hello") }, + }, null); + + const res = try agent._compactInPlace("Summarize.", null); + try testing.expect(!res.compacted); + try testing.expectEqual(@as(usize, 3), conv.messages.items.len); + // Stub was never consumed. + try testing.expectEqual(@as(usize, 0), stub.next); +} + +test "compact: extra instructions are appended to the system prompt" { + const allocator = testing.allocator; + + // Capture the system prompt the stub sees by scripting a turn and + // inspecting the throwaway conversation isn't directly possible via the + // current stub; instead we just assert compaction succeeds with extra + // instructions present (smoke test of the append path). + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "S" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + h.config.compaction = .{ .keep_verbatim = 1 }; + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + try addUserText(conv, "question one two three"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer one two three") }, + }, null); + try addUserText(conv, "question two"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer two") }, + }, null); + + const res = try agent._compactInPlace("Base prompt.", "keep bug #3 details"); + try testing.expect(res.compacted); +} + +test "runStep: auto-compacts on context overflow and retries once" { + const allocator = testing.allocator; + + // First stream call overflows; then the compaction request returns a + // summary; then the retried main request returns a final text turn. + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "COMPACTED SUMMARY" }} }, // compaction call + .{ .blocks = &.{.{ .Text = "final answer" }} }, // retried main call + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .overflow_calls = 1, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + h.config.compaction = .{ .keep_verbatim = 10, .compaction_prompt = "Summarize the conversation." }; + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + try conv.addSystemMessage("you are helpful"); + try addUserText(conv, "first question with several words here"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "first answer with several words") }, + }, null); + + try drainTurn(agent, "second recent question"); + + try testing.expect(agent._auto_compacted); + // After compaction + retry: [system, summary, user q2, assistant final]. + const msgs = conv.messages.items; + try testing.expectEqual(conversation.MessageRole.system, msgs[0].role); + try testing.expectEqualStrings( + "COMPACTED SUMMARY", + msgs[1].content.items[0].CompactionSummary.text.items, + ); + try testing.expectEqualStrings("second recent question", msgs[2].content.items[0].Text.items); + try testing.expectEqualStrings("final answer", msgs[msgs.len - 1].content.items[0].Text.items); +} + +test "runStep: context overflow without compaction prompt propagates" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "unused" }} }, + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .overflow_calls = 1, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + // No compaction_system_prompt set -> overflow propagates. + + try testing.expectError(error.ContextOverflow, drainTurn(agent, "hi")); +} + +// ----------------------------------------------------------------------------- +// Phase 6: provider retry + tool-error holistic tests +// ----------------------------------------------------------------------------- + +/// Records the `provider_retry` events a turn emits, so tests can assert +/// retry scheduling without a live provider. +const RetryRecorder = struct { + infos: std.ArrayList(provider_mod.ProviderRetryInfo) = .empty, + allocator: Allocator, + + /// Append a copy of `info`, dup'ing its borrowed `message` so assertions + /// stay valid after the source `Stream` is deinit'd. + fn append(self: *RetryRecorder, info: provider_mod.ProviderRetryInfo) !void { + var owned = info; + if (info.message) |m| owned.message = try self.allocator.dupe(u8, m); + try self.infos.append(self.allocator, owned); + } + + fn deinit(self: *RetryRecorder) void { + for (self.infos.items) |info| { + if (info.message) |m| self.allocator.free(m); + } + self.infos.deinit(self.allocator); + } +}; + +/// Drive a whole turn via the pull `Stream`, recording every +/// `provider_retry` event into `rr` and discarding the rest. Returns the +/// same terminal error the turn would raise. +fn drainTurnRecording(agent: *Agent, text: []const u8, rr: *RetryRecorder) !void { + var s = try runUserText(agent, text); + defer s.deinit(); + while (try s.next()) |ev| { + if (ev == .provider_retry) try rr.append(ev.provider_retry); + } +} + +/// Build an agent + harness with near-zero backoff so retry tests don't +/// actually sleep. Caller owns the harness and must keep it alive. +fn fastRetryHarness(h: *TestHarness) void { + h.activate(); + // Make sleeps negligible and deterministic (no jitter). + h.config.retry = .{ + .max_attempts = 4, + .initial_delay_ms = 0, + .max_delay_ms = 0, + .multiplier = 2.0, + .jitter = false, + }; +} + +test "runStep: provider 429 retries then succeeds without duplicate messages" { + const allocator = testing.allocator; + + const errs = [_]StubProvider.ScriptedError{ + .{ .err = error.ProviderRateLimited, .status_code = 429 }, + .{ .err = error.ProviderRateLimited, .status_code = 429 }, + }; + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "finally" }} }, + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .scripted_errors = &errs, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + fastRetryHarness(&h); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + var rr = RetryRecorder{ .allocator = allocator }; + defer rr.deinit(); + try drainTurnRecording(agent, "hi", &rr); + + // Two failures + one success. + try testing.expectEqual(@as(usize, 3), stub.calls_made); + // No duplicate assistant messages: user + single assistant. + try testing.expectEqual(@as(usize, 2), conv.messages.items.len); + try testing.expectEqual(conversation.MessageRole.assistant, conv.messages.items[1].role); + // Two retry notifications, delivered before each delayed retry. + try testing.expectEqual(@as(usize, 2), rr.infos.items.len); + try testing.expectEqual(@as(?u16, 429), rr.infos.items[0].status_code); + try testing.expectEqual(@as(usize, 1), rr.infos.items[0].attempt); + try testing.expectEqual(@as(usize, 2), rr.infos.items[1].attempt); +} + +test "runStep: provider 500 retries with backoff notification" { + const allocator = testing.allocator; + + const errs = [_]StubProvider.ScriptedError{ + .{ .err = error.ProviderServerError, .status_code = 500 }, + }; + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "ok" }} }, + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .scripted_errors = &errs, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + fastRetryHarness(&h); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + var rr = RetryRecorder{ .allocator = allocator }; + defer rr.deinit(); + try drainTurnRecording(agent, "hi", &rr); + + try testing.expectEqual(@as(usize, 2), stub.calls_made); + try testing.expectEqual(@as(usize, 1), rr.infos.items.len); + try testing.expectEqual(error.ProviderServerError, rr.infos.items[0].err); + try testing.expectEqual(@as(usize, 4), rr.infos.items[0].max_attempts); + try testing.expect(!rr.infos.items[0].compaction); +} + +test "runStep: provider auth failure does not retry" { + const allocator = testing.allocator; + + const errs = [_]StubProvider.ScriptedError{ + .{ .err = error.ProviderAuthFailed, .status_code = 401 }, + }; + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "unreachable" }} }, + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .scripted_errors = &errs, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + fastRetryHarness(&h); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + var rr = RetryRecorder{ .allocator = allocator }; + defer rr.deinit(); + try testing.expectError(error.ProviderAuthFailed, drainTurnRecording(agent, "hi", &rr)); + + // Exactly one attempt, no retry notification. + try testing.expectEqual(@as(usize, 1), stub.calls_made); + try testing.expectEqual(@as(usize, 0), rr.infos.items.len); +} + +test "runStep: retries exhaust and hard-fail after max_attempts" { + const allocator = testing.allocator; + + const errs = [_]StubProvider.ScriptedError{ + .{ .err = error.ProviderUnavailable, .status_code = 503 }, + .{ .err = error.ProviderUnavailable, .status_code = 503 }, + .{ .err = error.ProviderUnavailable, .status_code = 503 }, + .{ .err = error.ProviderUnavailable, .status_code = 503 }, + }; + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "unreachable" }} }, + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .scripted_errors = &errs, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + fastRetryHarness(&h); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + var rr = RetryRecorder{ .allocator = allocator }; + defer rr.deinit(); + try testing.expectError(error.ProviderUnavailable, drainTurnRecording(agent, "hi", &rr)); + + // 4 attempts total (max_attempts), 3 retry notifications. + try testing.expectEqual(@as(usize, 4), stub.calls_made); + try testing.expectEqual(@as(usize, 3), rr.infos.items.len); +} + +test "runStep: mid-stream failure retries then succeeds" { + const allocator = testing.allocator; + + // The open succeeds every time; the FIRST two responses fail during + // `produce` (mid-stream), the third is a normal scripted turn. + const stream_errs = [_]StubProvider.StreamFailure{ + .{ .err = error.ProviderOverloaded, .message = "overloaded_error: Overloaded" }, + .{ .err = error.ProviderOverloaded, .message = "overloaded_error: Overloaded" }, + }; + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "finally" }} }, + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .stream_errors = &stream_errs, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + fastRetryHarness(&h); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + var rr = RetryRecorder{ .allocator = allocator }; + defer rr.deinit(); + try drainTurnRecording(agent, "hi", &rr); + + // Three opens: two that fail mid-stream + one that succeeds. + try testing.expectEqual(@as(usize, 3), stub.calls_made); + // No duplicate assistant messages: the mid-stream failures committed + // nothing, so we end with user + a single assistant. + try testing.expectEqual(@as(usize, 2), conv.messages.items.len); + try testing.expectEqual(conversation.MessageRole.assistant, conv.messages.items[1].role); + // Two retry notifications, with a monotonically increasing attempt count + // (the bug was that this stayed pinned at 1 forever). + try testing.expectEqual(@as(usize, 2), rr.infos.items.len); + try testing.expectEqual(@as(usize, 1), rr.infos.items[0].attempt); + try testing.expectEqual(@as(usize, 2), rr.infos.items[1].attempt); + // The provider's diagnostic is surfaced on the retry notice (was the + // whole point: the user should see "overloaded", not a bare error name). + try testing.expectEqualStrings("overloaded_error: Overloaded", rr.infos.items[0].message.?); +} + +test "runStep: mid-stream failures exhaust and hard-fail after max_attempts" { + const allocator = testing.allocator; + + // Every open succeeds but every response fails mid-stream. Without the + // attempt counter this would re-open forever; it must hard-fail after + // `max_attempts` opens. + const stream_errs = [_]StubProvider.StreamFailure{ + .{ .err = error.ProviderStreamMalformed }, + .{ .err = error.ProviderStreamMalformed }, + .{ .err = error.ProviderStreamMalformed }, + .{ .err = error.ProviderStreamMalformed }, + }; + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "unreachable" }} }, + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .stream_errors = &stream_errs, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + fastRetryHarness(&h); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + var rr = RetryRecorder{ .allocator = allocator }; + defer rr.deinit(); + try testing.expectError(error.ProviderStreamMalformed, drainTurnRecording(agent, "hi", &rr)); + + // 4 opens total (max_attempts), 3 retry notifications — then hard-fail + // instead of looping endlessly. + try testing.expectEqual(@as(usize, 4), stub.calls_made); + try testing.expectEqual(@as(usize, 3), rr.infos.items.len); +} + +test "runStep: Retry-After is honored and reported" { + const allocator = testing.allocator; + + const errs = [_]StubProvider.ScriptedError{ + .{ .err = error.ProviderRateLimited, .status_code = 429, .retry_after_ms = 7000 }, + }; + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "ok" }} }, + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .scripted_errors = &errs, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + // Cap below the Retry-After to verify the policy cap applies. + h.config.retry = .{ .initial_delay_ms = 0, .max_delay_ms = 1, .jitter = false }; + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + var rr = RetryRecorder{ .allocator = allocator }; + defer rr.deinit(); + try drainTurnRecording(agent, "hi", &rr); + + try testing.expectEqual(@as(usize, 1), rr.infos.items.len); + // Reported Retry-After is the raw provider value... + try testing.expectEqual(@as(?u64, 7000), rr.infos.items[0].retry_after_ms); + // ...but the actual delay is capped by policy.max_delay_ms. + try testing.expectEqual(@as(u64, 1), rr.infos.items[0].delay_ms); +} + +test "runStep: cancellation from a tool still hard-fails" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "x", .name = "hard", .input = "" } }, + } }, + .{ .blocks = &.{.{ .Text = "unreachable" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.register(try HardFailTool.create(allocator, "hard")); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try testing.expectError(error.Canceled, drainTurn(agent, "go")); + // Turn aborts: no tool result appended (user + assistant only). + try testing.expectEqual(@as(usize, 2), conv.messages.items.len); +} + +test "runStep: source per-call error produces a per-call error result and continues" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{ + .{ .ToolUse = .{ .id = "a", .name = "pa", .input = "" } }, + .{ .ToolUse = .{ .id = "b", .name = "pb", .input = "" } }, + } }, + .{ .blocks = &.{.{ .Text = "moving on" }} }, + }; + var stub = StubProvider{ .allocator = allocator, .scripted = &scripted }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + try h.registry.registerSource(try PartialSource.create(allocator, "ps", &.{ "pa", "pb" })); + h.activate(); + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + + try drainTurn(agent, "go"); + + try testing.expectEqual(@as(usize, 4), conv.messages.items.len); + const tr_msg = conv.messages.items[2]; + const tr_a = tr_msg.content.items[0].ToolResult; // first call succeeded + const tr_b = tr_msg.content.items[1].ToolResult; // second failed + try testing.expect(!tr_a.is_error); + try testing.expectEqualStrings("ok", trText(tr_a)); + try testing.expect(tr_b.is_error); + try testing.expect(std.mem.indexOf(u8, trText(tr_b), "PerCallBoom") != null); +} + +test "runStep: context-overflow compaction fires a compaction retry notification" { + const allocator = testing.allocator; + + const scripted = [_]StubProvider.ScriptedTurn{ + .{ .blocks = &.{.{ .Text = "COMPACTED SUMMARY" }} }, // compaction call + .{ .blocks = &.{.{ .Text = "final answer" }} }, // retried main call + }; + var stub = StubProvider{ + .allocator = allocator, + .scripted = &scripted, + .overflow_calls = 1, + }; + var threaded: std.Io.Threaded = .init(allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + var h = TestHarness.init(allocator); + defer h.deinit(); + h.activate(); + h.config.compaction = .{ .keep_verbatim = 10, .compaction_prompt = "Summarize the conversation." }; + var ns = null_store_mod.NullStore.init(allocator); + const agent = try Agent.init(allocator, io, &h.config, ns.store().create(), null); + defer agent.deinit(); + h.seedInto(agent); + agent._open_stream_fn = stub.install(); + + const conv = &agent.conversation; + try conv.addSystemMessage("you are helpful"); + try addUserText(conv, "first question with several words here"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "first answer with several words") }, + }, null); + + var rr = RetryRecorder{ .allocator = allocator }; + defer rr.deinit(); + try drainTurnRecording(agent, "second recent question", &rr); + + try testing.expect(agent._auto_compacted); + // Exactly one notification, flagged as a compaction retry with no delay. + try testing.expectEqual(@as(usize, 1), rr.infos.items.len); + try testing.expect(rr.infos.items[0].compaction); + try testing.expectEqual(@as(u64, 0), rr.infos.items[0].delay_ms); + try testing.expectEqual(error.ContextOverflow, rr.infos.items[0].err); +} diff --git a/src/anthropic_messages_json.zig b/src/anthropic_messages_json.zig new file mode 100644 index 0000000..5085149 --- /dev/null +++ b/src/anthropic_messages_json.zig @@ -0,0 +1,1619 @@ +//! Anthropic Messages API JSON serialization and event parsing. +//! +//! Two responsibilities: +//! 1. Serialize a `Conversation` into a `/v1/messages` request body. +//! Anthropic differs from OpenAI in several ways — see `serializeRequest`. +//! 2. Parse one streaming SSE event payload (the JSON object after `data: `) +//! into a strongly-typed `StreamEvent` that the provider can consume. +//! +//! Wire format reference: +//! https://platform.claude.com/docs/en/build-with-claude/streaming +//! https://platform.claude.com/docs/en/build-with-claude/extended-thinking + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Writer = std.Io.Writer; +const conversation = @import("conversation.zig"); +const config_mod = @import("config.zig"); +const tool_registry_mod = @import("tool_registry.zig"); +const writeRawJson = @import("provider.zig").writeRawJson; + +// ----------------------------------------------------------------------------- +// Request serialization +// ----------------------------------------------------------------------------- + +/// Serialize a Conversation into a `/v1/messages` request body. +/// +/// Differences from OpenAI Chat Completions: +/// - System messages are extracted and concatenated into a top-level +/// `system` string. They do not appear in the `messages` array. +/// - `content` is always an array of typed blocks, never a bare string. +/// - `max_tokens` is required. +/// +/// Caller owns the returned slice. +pub fn serializeRequest( + allocator: Allocator, + cfg: *const config_mod.AnthropicMessagesConfig, + conv: *const conversation.Conversation, + tools: *const tool_registry_mod.ToolRegistry, +) ![]u8 { + var aw: Writer.Allocating = .init(allocator); + errdefer aw.deinit(); + + var s: std.json.Stringify = .{ .writer = &aw.writer }; + + try s.beginObject(); + + try s.objectField("model"); + try s.write(cfg.model); + + try s.objectField("max_tokens"); + try s.write(cfg.max_tokens); + + try s.objectField("stream"); + try s.write(true); + + // Extended thinking configuration. + // `.disabled` — omit the field entirely. + // `.enabled` — manual budget: { type, budget_tokens, display }. + // `.adaptive` — adaptive mode: { type, display } + output_config.effort. + switch (cfg.thinking) { + .disabled => {}, + .enabled => { + // Resolve budget: explicit value or fall back to max_tokens - 1. + // Clamp so budget is always strictly less than max_tokens (required + // by Anthropic when not using interleaved thinking). + const raw_budget: u32 = cfg.thinking_budget_tokens orelse (cfg.max_tokens -| 1); + const budget: u32 = if (raw_budget >= cfg.max_tokens) cfg.max_tokens -| 1 else raw_budget; + try s.objectField("thinking"); + try s.beginObject(); + try s.objectField("type"); + try s.write("enabled"); + try s.objectField("budget_tokens"); + try s.write(budget); + try s.objectField("display"); + try s.write("summarized"); + try s.endObject(); + }, + .adaptive => { + try s.objectField("thinking"); + try s.beginObject(); + try s.objectField("type"); + try s.write("adaptive"); + try s.objectField("display"); + try s.write("summarized"); + try s.endObject(); + try s.objectField("output_config"); + try s.beginObject(); + try s.objectField("effort"); + try s.write(@tagName(cfg.effort)); + try s.endObject(); + }, + } + + // Build and emit the concatenated system prompt, if any. + var system_buf: std.ArrayList(u8) = .empty; + defer system_buf.deinit(allocator); + try collectSystemPrompt(conv, &system_buf, allocator); + if (system_buf.items.len > 0) { + try s.objectField("system"); + try s.write(system_buf.items); + } + + if (tools.count() > 0) { + try s.objectField("tools"); + try s.beginArray(); + var it = tools.toolsForLLM(); + while (it.next()) |t| { + try s.beginObject(); + try s.objectField("name"); + // `t.decl.name` is already wire-encoded by `toolsForLLM`. + try s.write(t.decl.name); + try s.objectField("description"); + try s.write(t.decl.description); + try s.objectField("input_schema"); + try writeRawJson(&s, t.decl.schema_json); + try s.endObject(); + } + try s.endArray(); + } + + // Emit messages (everything that isn't .system). If the conversation + // has been compacted, only the latest compaction summary and the + // messages after it are active; the superseded prefix is dropped. + // + // Prompt caching: we replicate Anthropic's "automatic caching" by + // placing one `cache_control` breakpoint on the last cacheable block of + // the request. Anthropic's native API offers a single top-level + // `cache_control` field for this, but Anthropic-style proxies + // (Bedrock, Vertex, OpenRouter) reject that field outright (400). A + // per-block breakpoint produces the same caching behavior and is the + // broadly-supported form, so we mark the block directly. As the + // append-only conversation grows, this breakpoint advances each request + // and stays well inside the 20-block lookback window, so every turn + // reads the prior turn's write and writes only its own new suffix. + const window = conversation.activeMessageWindow(conv.messages.items); + const cache_at: ?CacheMark = if (cfg.prompt_cache) cacheableTailBlock(window) else null; + try s.objectField("messages"); + try s.beginArray(); + for (window, 0..) |msg, mi| { + if (msg.role == .system) continue; + const mark: ?usize = if (cache_at) |c| (if (c.message == mi) c.block else null) else null; + try writeMessage(&s, msg, mark, cfg); + } + try s.endArray(); + + try s.endObject(); + + return try aw.toOwnedSlice(); +} + +/// Build the top-level Anthropic `system` string. Applies the shared +/// append/replace derivation (see `conversation.effectiveSystemBlocks`), +/// strips trailing newlines from each surviving block, and joins them with +/// a horizontal rule (`\n\n---\n\n`). Anthropic's wire format requires a +/// single string, so this rule is its concession to that constraint. +fn collectSystemPrompt( + conv: *const conversation.Conversation, + out: *std.ArrayList(u8), + allocator: Allocator, +) !void { + var blocks = try conversation.effectiveSystemBlocks(allocator, conv.messages.items); + defer blocks.deinit(allocator); + + const sep = "\n\n---\n\n"; + var first = true; + for (blocks.items) |text| { + const trimmed = std.mem.trimEnd(u8, text, "\n"); + if (!first) try out.appendSlice(allocator, sep); + try out.appendSlice(allocator, trimmed); + first = false; + } +} + +/// Location of the prompt-cache breakpoint: the block to stamp with +/// `cache_control`. Indices are into the active message window and into +/// that message's `content` array. +const CacheMark = struct { message: usize, block: usize }; + +/// Returns whether a content block can carry a `cache_control` marker and +/// is actually emitted on the wire. Thinking blocks can't be cached +/// directly, and unsigned thinking blocks are dropped entirely +/// (`writeBlock` returns early), so neither is a valid breakpoint target. +/// System blocks are filtered before emission. +fn isCacheableBlock(block: conversation.ContentBlock) bool { + return switch (block) { + .Text, .ToolUse, .ToolResult, .CompactionSummary => true, + .Thinking, .System => false, + }; +} + +/// Find the last cacheable, actually-emitted block in the active window: +/// the spot to place the single cache breakpoint. Scans messages from the +/// end, skipping system messages and any trailing non-cacheable blocks +/// (e.g. a thinking block as the final assistant block). Returns null if +/// nothing cacheable is present (then no breakpoint is emitted). +fn cacheableTailBlock(window: []const conversation.Message) ?CacheMark { + var mi = window.len; + while (mi > 0) { + mi -= 1; + const msg = window[mi]; + if (msg.role == .system) continue; + var bi = msg.content.items.len; + while (bi > 0) { + bi -= 1; + if (isCacheableBlock(msg.content.items[bi])) { + return .{ .message = mi, .block = bi }; + } + } + } + return null; +} + +fn writeMessage( + s: *std.json.Stringify, + msg: conversation.Message, + cache_block: ?usize, + cfg: *const config_mod.AnthropicMessagesConfig, +) !void { + try s.beginObject(); + + try s.objectField("role"); + try s.write(@tagName(msg.role)); + + try s.objectField("content"); + try s.beginArray(); + for (msg.content.items, 0..) |block, bi| { + const mark = if (cache_block) |cb| cb == bi else false; + try writeBlock(s, block, mark, cfg, msg.identity); + } + try s.endArray(); + + try s.endObject(); +} + +/// Emit a `"cache_control": {"type": "ephemeral"}` field. The caller must +/// be positioned inside an open block object, after its other fields. +fn writeCacheControl(s: *std.json.Stringify) !void { + try s.objectField("cache_control"); + try s.beginObject(); + try s.objectField("type"); + try s.write("ephemeral"); + try s.endObject(); +} + +fn writeBlock( + s: *std.json.Stringify, + block: conversation.ContentBlock, + mark_cache: bool, + cfg: *const config_mod.AnthropicMessagesConfig, + msg_identity: ?config_mod.WireIdentity, +) !void { + switch (block) { + .Text => |tb| { + try s.beginObject(); + try s.objectField("type"); + try s.write("text"); + try s.objectField("text"); + try s.write(tb.items); + if (mark_cache) try writeCacheControl(s); + try s.endObject(); + }, + .Thinking => |tb| { + // Anthropic requires the signature field to round-trip a thinking + // block. If we don't have one (e.g. block was synthesized from a + // non-Anthropic provider), or if the signature came from a + // different provider/model than the current request, skip the + // block: Anthropic will reject unsigned thinking on incoming + // messages, and opaque signatures are not portable across + // provider/model boundaries. + const sig = tb.signature orelse return; + if (!conversation.thinkingSignatureMatches(tb, msg_identity, .anthropic_messages, cfg.base_url, cfg.model)) return; + try s.beginObject(); + try s.objectField("type"); + try s.write("thinking"); + try s.objectField("thinking"); + try s.write(tb.text.items); + try s.objectField("signature"); + try s.write(sig); + try s.endObject(); + }, + .ToolUse => |tu| { + try s.beginObject(); + try s.objectField("type"); + try s.write("tool_use"); + try s.objectField("id"); + try s.write(tu.id); + try s.objectField("name"); + // Replayed assistant tool_use: encode the stored dotted name. + var name_buf: [tool_registry_mod.max_wire_name_len]u8 = undefined; + try s.write(tool_registry_mod.encodeName(&name_buf, tu.name)); + try s.objectField("input"); + // Anthropic expects `input` as a nested JSON object, not a + // string. The block's input bytes were assembled from + // `input_json_delta` fragments — splice them in verbatim. + // Treat empty as an empty object. + const input_bytes = tu.input.items; + if (input_bytes.len == 0) { + try s.beginObject(); + try s.endObject(); + } else { + try writeRawJson(s, input_bytes); + } + if (mark_cache) try writeCacheControl(s); + try s.endObject(); + }, + .ToolResult => |tr| { + try s.beginObject(); + try s.objectField("type"); + try s.write("tool_result"); + try s.objectField("tool_use_id"); + try s.write(tr.tool_use_id); + // Anthropic supports an `is_error` marker on tool results; + // emit it only when set (omitting it defaults to a success + // result on the wire). + if (tr.is_error) { + try s.objectField("is_error"); + try s.write(true); + } + // Anthropic accepts `content` as an array of typed blocks: + // text, image (base64 source), and document (PDF). + try s.objectField("content"); + try s.beginArray(); + for (tr.parts.items) |part| { + switch (part) { + .text => |tb| { + try s.beginObject(); + try s.objectField("type"); + try s.write("text"); + try s.objectField("text"); + try s.write(tb.items); + try s.endObject(); + }, + .media => |m| { + const is_pdf = std.mem.eql(u8, m.media_type, "application/pdf"); + try s.beginObject(); + try s.objectField("type"); + try s.write(if (is_pdf) "document" else "image"); + try s.objectField("source"); + try s.beginObject(); + try s.objectField("type"); + try s.write("base64"); + try s.objectField("media_type"); + try s.write(m.media_type); + try s.objectField("data"); + try s.write(m.data.items); + try s.endObject(); + try s.endObject(); + }, + } + } + try s.endArray(); + if (mark_cache) try writeCacheControl(s); + try s.endObject(); + }, + // System blocks never reach here: `serializeRequest` filters out + // `.system` messages before emitting the `messages` array, and the + // system text is hoisted into the top-level `system` string by + // `collectSystemPrompt`. Present only to keep the switch exhaustive. + .System => {}, + // A compaction summary is the synthetic seed text standing in for + // a compacted prefix; the model reads it as ordinary user text. + .CompactionSummary => |cs| { + try s.beginObject(); + try s.objectField("type"); + try s.write("text"); + try s.objectField("text"); + try s.write(cs.text.items); + if (mark_cache) try writeCacheControl(s); + try s.endObject(); + }, + } +} + +// ----------------------------------------------------------------------------- +// Streaming event parsing +// ----------------------------------------------------------------------------- + +/// Parsed shape of one SSE event payload. The `tag` field selects which of +/// the payload variants is populated. +/// +/// String slices are borrowed from the underlying `std.json.Parsed`. The +/// caller must keep `parsed` alive (or copy the slices) until the event is +/// fully consumed. +pub const StreamEventTag = enum { + message_start, + content_block_start, + content_block_delta, + content_block_stop, + message_delta, + message_stop, + ping, + @"error", + unknown, +}; + +pub const ContentBlockKind = enum { text, thinking, tool_use, unknown }; + +/// Partial token-count snapshot emitted in `message_start` and +/// `message_delta`. Field semantics follow Anthropic's wire format: +/// +/// - `input_tokens`: prompt tokens billed at the base rate. +/// - `cache_creation_input_tokens`: prompt tokens written to a new +/// cache entry (1.25× base). +/// - `cache_read_input_tokens`: prompt tokens served from cache +/// (0.1× base). +/// - `output_tokens`: response tokens billed at the output rate. +/// +/// `message_start.usage` carries the initial input-side counts (output +/// will be 0 or absent); `message_delta.usage` carries the final +/// `output_tokens` and may repeat the input-side counts. The provider +/// merges them — "missing" means "unchanged," not "reset to zero." +pub const StreamUsage = struct { + input_tokens: ?u64 = null, + output_tokens: ?u64 = null, + cache_creation_input_tokens: ?u64 = null, + cache_read_input_tokens: ?u64 = null, +}; + +pub const StreamEvent = union(StreamEventTag) { + message_start: struct { + usage: StreamUsage = .{}, + }, + content_block_start: struct { + index: usize, + kind: ContentBlockKind, + // For tool_use blocks (phase 3+): + tool_id: ?[]const u8 = null, + tool_name: ?[]const u8 = null, + }, + content_block_delta: struct { + index: usize, + text_delta: ?[]const u8 = null, + thinking_delta: ?[]const u8 = null, + signature_delta: ?[]const u8 = null, + input_json_delta: ?[]const u8 = null, + }, + content_block_stop: struct { + index: usize, + }, + message_delta: struct { + stop_reason: ?[]const u8 = null, + usage: StreamUsage = .{}, + }, + message_stop: void, + ping: void, + @"error": struct { + kind: ?[]const u8 = null, + message: ?[]const u8 = null, + }, + unknown: void, +}; + +pub const ParsedStreamEvent = struct { + parsed: std.json.Parsed(std.json.Value), + event: StreamEvent, + + pub fn deinit(self: *ParsedStreamEvent) void { + self.parsed.deinit(); + } +}; + +pub fn parseStreamEvent(allocator: Allocator, payload: []const u8) !ParsedStreamEvent { + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, payload, .{}); + errdefer parsed.deinit(); + + const root = parsed.value; + if (root != .object) return .{ .parsed = parsed, .event = .unknown }; + + const type_v = root.object.get("type") orelse return .{ .parsed = parsed, .event = .unknown }; + if (type_v != .string) return .{ .parsed = parsed, .event = .unknown }; + + const ty = type_v.string; + if (std.mem.eql(u8, ty, "message_start")) { + var usage: StreamUsage = .{}; + if (root.object.get("message")) |m| { + if (m == .object) { + if (m.object.get("usage")) |u| { + if (u == .object) usage = parseStreamUsage(u.object); + } + } + } + return .{ .parsed = parsed, .event = .{ .message_start = .{ .usage = usage } } }; + } + + if (std.mem.eql(u8, ty, "content_block_start")) { + const idx = readIndex(root) orelse 0; + var kind: ContentBlockKind = .unknown; + var tool_id: ?[]const u8 = null; + var tool_name: ?[]const u8 = null; + if (root.object.get("content_block")) |cb| { + if (cb == .object) { + if (cb.object.get("type")) |t| { + if (t == .string) { + if (std.mem.eql(u8, t.string, "text")) { + kind = .text; + } else if (std.mem.eql(u8, t.string, "thinking")) { + kind = .thinking; + } else if (std.mem.eql(u8, t.string, "tool_use")) { + kind = .tool_use; + } + } + } + if (cb.object.get("id")) |i| { + if (i == .string) tool_id = i.string; + } + if (cb.object.get("name")) |n| { + if (n == .string) tool_name = n.string; + } + } + } + return .{ .parsed = parsed, .event = .{ .content_block_start = .{ + .index = idx, + .kind = kind, + .tool_id = tool_id, + .tool_name = tool_name, + } } }; + } + + if (std.mem.eql(u8, ty, "content_block_delta")) { + const idx = readIndex(root) orelse 0; + var text_delta: ?[]const u8 = null; + var thinking_delta: ?[]const u8 = null; + var signature_delta: ?[]const u8 = null; + var input_json_delta: ?[]const u8 = null; + if (root.object.get("delta")) |d| { + if (d == .object) { + const dt = blk: { + if (d.object.get("type")) |t| { + if (t == .string) break :blk t.string; + } + break :blk ""; + }; + if (std.mem.eql(u8, dt, "text_delta")) { + if (d.object.get("text")) |v| if (v == .string) { + text_delta = v.string; + }; + } else if (std.mem.eql(u8, dt, "thinking_delta")) { + if (d.object.get("thinking")) |v| if (v == .string) { + thinking_delta = v.string; + }; + } else if (std.mem.eql(u8, dt, "signature_delta")) { + if (d.object.get("signature")) |v| if (v == .string) { + signature_delta = v.string; + }; + } else if (std.mem.eql(u8, dt, "input_json_delta")) { + if (d.object.get("partial_json")) |v| if (v == .string) { + input_json_delta = v.string; + }; + } + } + } + return .{ .parsed = parsed, .event = .{ .content_block_delta = .{ + .index = idx, + .text_delta = text_delta, + .thinking_delta = thinking_delta, + .signature_delta = signature_delta, + .input_json_delta = input_json_delta, + } } }; + } + + if (std.mem.eql(u8, ty, "content_block_stop")) { + const idx = readIndex(root) orelse 0; + return .{ .parsed = parsed, .event = .{ .content_block_stop = .{ .index = idx } } }; + } + + if (std.mem.eql(u8, ty, "message_delta")) { + var stop_reason: ?[]const u8 = null; + var usage: StreamUsage = .{}; + if (root.object.get("delta")) |d| { + if (d == .object) { + if (d.object.get("stop_reason")) |sr| { + if (sr == .string) stop_reason = sr.string; + } + } + } + // `usage` is on the message_delta event itself, not under `delta`. + if (root.object.get("usage")) |u| { + if (u == .object) usage = parseStreamUsage(u.object); + } + return .{ .parsed = parsed, .event = .{ .message_delta = .{ .stop_reason = stop_reason, .usage = usage } } }; + } + + if (std.mem.eql(u8, ty, "message_stop")) { + return .{ .parsed = parsed, .event = .message_stop }; + } + + if (std.mem.eql(u8, ty, "ping")) { + return .{ .parsed = parsed, .event = .ping }; + } + + if (std.mem.eql(u8, ty, "error")) { + var kind: ?[]const u8 = null; + var message: ?[]const u8 = null; + if (root.object.get("error")) |e| { + if (e == .object) { + if (e.object.get("type")) |t| if (t == .string) { + kind = t.string; + }; + if (e.object.get("message")) |m| if (m == .string) { + message = m.string; + }; + } + } + return .{ .parsed = parsed, .event = .{ .@"error" = .{ .kind = kind, .message = message } } }; + } + + return .{ .parsed = parsed, .event = .unknown }; +} + +fn parseStreamUsage(obj: std.json.ObjectMap) StreamUsage { + return .{ + .input_tokens = readOptionalU64(obj, "input_tokens"), + .output_tokens = readOptionalU64(obj, "output_tokens"), + .cache_creation_input_tokens = readOptionalU64(obj, "cache_creation_input_tokens"), + .cache_read_input_tokens = readOptionalU64(obj, "cache_read_input_tokens"), + }; +} + +fn readOptionalU64(obj: std.json.ObjectMap, name: []const u8) ?u64 { + const v = obj.get(name) orelse return null; + if (v != .integer) return null; + if (v.integer < 0) return null; + return @intCast(v.integer); +} + +fn readIndex(root: std.json.Value) ?usize { + const v = root.object.get("index") orelse return null; + if (v != .integer) return null; + if (v.integer < 0) return null; + return @intCast(v.integer); +} + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const testing = std.testing; + +fn testConfig(model: []const u8) config_mod.AnthropicMessagesConfig { + return .{ + .api_key = "k", + .base_url = "u", + .model = model, + .max_tokens = 1024, + }; +} + +/// Test helper: append a single-text user message. `addUserMessage` now +/// takes a block slice (symmetric with `addAssistantMessage`); this wraps +/// the common plain-text case the tests below use. +fn addUserText(conv: *conversation.Conversation, text: []const u8) !void { + const tb = try conversation.textualBlockFromSlice(conv.allocator, text); + var block: conversation.ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + +test "serializeRequest - system extracted into top-level field" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("You are helpful."); + try addUserText(&conv, "Hello!"); + + const cfg = testConfig("claude-sonnet-4-20250514"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const root = parsed.value.object; + try testing.expectEqualStrings("claude-sonnet-4-20250514", root.get("model").?.string); + try testing.expect(root.get("stream").?.bool); + try testing.expectEqual(@as(i64, 1024), root.get("max_tokens").?.integer); + try testing.expectEqualStrings("You are helpful.", root.get("system").?.string); + + // Only the user message appears in `messages`. + const msgs = root.get("messages").?.array.items; + try testing.expectEqual(@as(usize, 1), msgs.len); + try testing.expectEqualStrings("user", msgs[0].object.get("role").?.string); + + // Content is an array of typed blocks, never a bare string. + const content = msgs[0].object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 1), content.len); + try testing.expectEqualStrings("text", content[0].object.get("type").?.string); + try testing.expectEqualStrings("Hello!", content[0].object.get("text").?.string); +} + +test "serializeRequest - prompt_cache marks last block with cache_control" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "Hi"); + + var cfg = testConfig("claude-x"); + cfg.prompt_cache = true; + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + // No top-level field (would break Bedrock/Vertex/OpenRouter). + try testing.expect(parsed.value.object.get("cache_control") == null); + // The single user text block carries the breakpoint. + const block = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items[0].object; + try testing.expectEqualStrings("ephemeral", block.get("cache_control").?.object.get("type").?.string); +} + +test "serializeRequest - cache breakpoint lands on the final block only" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "first"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "reply") }, + }, null); + try addUserText(&conv, "second"); + + var cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const msgs = parsed.value.object.get("messages").?.array.items; + try testing.expectEqual(@as(usize, 3), msgs.len); + // Only the last message's only block is marked. + try testing.expect(msgs[0].object.get("content").?.array.items[0].object.get("cache_control") == null); + try testing.expect(msgs[1].object.get("content").?.array.items[0].object.get("cache_control") == null); + try testing.expect(msgs[2].object.get("content").?.array.items[0].object.get("cache_control") != null); +} + +test "serializeRequest - cache breakpoint skips a trailing thinking block" { + // A thinking block can't carry cache_control; the breakpoint must fall + // back to the prior cacheable block (here the assistant text). + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const sig = try allocator.dupe(u8, "EqQBCgIYAhIM1gbcDa9GJwZA"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") }, + .{ .Thinking = .{ + .text = try conversation.textualBlockFromSlice(allocator, "trailing thought"), + .signature = sig, + } }, + }, null); + try conversation.setThinkingOrigins(allocator, conv.messages.items[0].content.items, .anthropic_messages, "u", "claude-x"); + + var cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const content = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items; + // text block marked, thinking block not. + try testing.expectEqualStrings("text", content[0].object.get("type").?.string); + try testing.expect(content[0].object.get("cache_control") != null); + try testing.expectEqualStrings("thinking", content[1].object.get("type").?.string); + try testing.expect(content[1].object.get("cache_control") == null); +} + +test "serializeRequest - thinking signature replays from message identity when block origin is absent" { + // A reloaded thinking block need not carry its own signature_origin: the + // enclosing message's identity is the per-message source of truth, so a + // host can rely on it alone (no per-block identity copy required). + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addAssistantMessage(&.{ + .{ + .Thinking = .{ + .text = try conversation.textualBlockFromSlice(allocator, "reasoned"), + .signature = try allocator.dupe(u8, "EqQBCgIYAhIM1gbcDa9GJwZA"), + // No signature_origin on the block. + }, + }, + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") }, + }, null); + // Per-message identity matching the request. + conv.messages.items[0].identity = try conversation.dupeWireIdentity(allocator, .{ + .api_style = .anthropic_messages, + .base_url = "u", + .model = "claude-x", + }); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const content = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items; + try testing.expectEqualStrings("thinking", content[0].object.get("type").?.string); + try testing.expectEqualStrings("EqQBCgIYAhIM1gbcDa9GJwZA", content[0].object.get("signature").?.string); +} + +test "serializeRequest - thinking dropped when message identity targets a different model" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ + .text = try conversation.textualBlockFromSlice(allocator, "reasoned"), + .signature = try allocator.dupe(u8, "EqQBCgIYAhIM1gbcDa9GJwZA"), + } }, + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") }, + }, null); + // Identity points at a *different* model than the request. + conv.messages.items[0].identity = try conversation.dupeWireIdentity(allocator, .{ + .api_style = .anthropic_messages, + .base_url = "u", + .model = "some-other-model", + }); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const content = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items; + // Only the text block survives; the unportable thinking block is dropped. + try testing.expectEqual(@as(usize, 1), content.len); + try testing.expectEqualStrings("text", content[0].object.get("type").?.string); +} + +test "serializeRequest - prompt_cache disabled omits all cache_control" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "Hi"); + + var cfg = testConfig("claude-x"); + cfg.prompt_cache = false; + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + try testing.expect(parsed.value.object.get("cache_control") == null); + const block = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items[0].object; + try testing.expect(block.get("cache_control") == null); +} + +test "serializeRequest - multiple system messages joined with horizontal rule" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("Be terse."); + try conv.addSystemMessage("Be accurate."); + try addUserText(&conv, "Hi"); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + try testing.expectEqualStrings( + "Be terse.\n\n---\n\nBe accurate.", + parsed.value.object.get("system").?.string, + ); +} + +test "serializeRequest - replace-mode system block wipes prior system text" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("original seed"); + try conv.addSystemMessage("original append"); + try conv.replaceSystemMessage("fresh seed"); + try conv.addSystemMessage("fresh append"); + try addUserText(&conv, "Hi"); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + try testing.expectEqualStrings( + "fresh seed\n\n---\n\nfresh append", + parsed.value.object.get("system").?.string, + ); +} + +test "serializeRequest - trailing newlines stripped before the rule join" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("line one\n\n"); + try conv.addSystemMessage("line two\n"); + try addUserText(&conv, "Hi"); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + try testing.expectEqualStrings( + "line one\n\n---\n\nline two", + parsed.value.object.get("system").?.string, + ); +} + +test "serializeRequest - no system messages omits the system field" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "Hi"); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + try testing.expect(parsed.value.object.get("system") == null); +} + +test "serializeRequest - signed assistant Thinking blocks round-trip" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const sig = try allocator.dupe(u8, "EqQBCgIYAhIM1gbcDa9GJwZA"); + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ + .text = try conversation.textualBlockFromSlice(allocator, "let me think"), + .signature = sig, + } }, + .{ .Text = try conversation.textualBlockFromSlice(allocator, "the answer is 42") }, + }, null); + try conversation.setThinkingOrigins(allocator, conv.messages.items[0].content.items, .anthropic_messages, "u", "claude-x"); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msg = parsed.value.object.get("messages").?.array.items[0]; + try testing.expectEqualStrings("assistant", msg.object.get("role").?.string); + + const content = msg.object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 2), content.len); + + try testing.expectEqualStrings("thinking", content[0].object.get("type").?.string); + try testing.expectEqualStrings("let me think", content[0].object.get("thinking").?.string); + try testing.expectEqualStrings( + "EqQBCgIYAhIM1gbcDa9GJwZA", + content[0].object.get("signature").?.string, + ); + + try testing.expectEqualStrings("text", content[1].object.get("type").?.string); + try testing.expectEqualStrings("the answer is 42", content[1].object.get("text").?.string); +} + +test "serializeRequest - mismatched thinking origin drops the block" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const sig = try allocator.dupe(u8, "EqQBCgIYAhIM1gbcDa9GJwZA"); + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ + .text = try conversation.textualBlockFromSlice(allocator, "other provider thinking"), + .signature = sig, + } }, + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") }, + }, null); + try conversation.setThinkingOrigins(allocator, conv.messages.items[0].content.items, .openai_responses, "https://api.individual.githubcopilot.com", "gpt-5.4-mini"); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const content = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 1), content.len); + try testing.expectEqualStrings("text", content[0].object.get("type").?.string); +} + +test "serializeRequest - unsigned Thinking blocks are dropped" { + // Anthropic rejects thinking blocks without a valid signature on inbound + // messages. If a block lacks one (e.g. from a different provider or an + // interrupted stream), omit it rather than send a guaranteed-400 request. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ + .text = try conversation.textualBlockFromSlice(allocator, "unsigned thinking"), + .signature = null, + } }, + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") }, + }, null); + + const cfg = testConfig("claude-x"); + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const content = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 1), content.len); + try testing.expectEqualStrings("text", content[0].object.get("type").?.string); +} + +test "parseStreamEvent - message_start" { + const allocator = testing.allocator; + const payload = + \\{"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","content":[],"model":"claude","stop_reason":null}} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqual(StreamEventTag.message_start, @as(StreamEventTag, pe.event)); +} + +test "parseStreamEvent - content_block_start text" { + const allocator = testing.allocator; + const payload = + \\{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqual(StreamEventTag.content_block_start, @as(StreamEventTag, pe.event)); + try testing.expectEqual(@as(usize, 0), pe.event.content_block_start.index); + try testing.expectEqual(ContentBlockKind.text, pe.event.content_block_start.kind); +} + +test "parseStreamEvent - content_block_start thinking" { + const allocator = testing.allocator; + const payload = + \\{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqual(ContentBlockKind.thinking, pe.event.content_block_start.kind); +} + +test "parseStreamEvent - text_delta" { + const allocator = testing.allocator; + const payload = + \\{"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"Hello"}} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqual(@as(usize, 1), pe.event.content_block_delta.index); + try testing.expectEqualStrings("Hello", pe.event.content_block_delta.text_delta.?); + try testing.expect(pe.event.content_block_delta.thinking_delta == null); +} + +test "parseStreamEvent - thinking_delta" { + const allocator = testing.allocator; + const payload = + \\{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"step 1"}} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqualStrings("step 1", pe.event.content_block_delta.thinking_delta.?); +} + +test "parseStreamEvent - signature_delta" { + const allocator = testing.allocator; + const payload = + \\{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"abc123"}} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqualStrings("abc123", pe.event.content_block_delta.signature_delta.?); +} + +test "parseStreamEvent - content_block_stop" { + const allocator = testing.allocator; + const payload = + \\{"type":"content_block_stop","index":2} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqual(@as(usize, 2), pe.event.content_block_stop.index); +} + +test "parseStreamEvent - message_delta stop_reason" { + const allocator = testing.allocator; + const payload = + \\{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null}} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqualStrings("end_turn", pe.event.message_delta.stop_reason.?); +} + +test "parseStreamEvent - message_stop" { + const allocator = testing.allocator; + const payload = + \\{"type":"message_stop"} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqual(StreamEventTag.message_stop, @as(StreamEventTag, pe.event)); +} + +test "parseStreamEvent - ping" { + const allocator = testing.allocator; + const payload = "{\"type\":\"ping\"}"; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqual(StreamEventTag.ping, @as(StreamEventTag, pe.event)); +} + +test "parseStreamEvent - error" { + const allocator = testing.allocator; + const payload = + \\{"type":"error","error":{"type":"overloaded_error","message":"too busy"}} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqualStrings("overloaded_error", pe.event.@"error".kind.?); + try testing.expectEqualStrings("too busy", pe.event.@"error".message.?); +} + +test "parseStreamEvent - unknown type" { + const allocator = testing.allocator; + const payload = + \\{"type":"some_future_event","extra":"data"} + ; + var pe = try parseStreamEvent(allocator, payload); + defer pe.deinit(); + try testing.expectEqual(StreamEventTag.unknown, @as(StreamEventTag, pe.event)); +} + +// ----------------------------------------------------------------------------- +// Phase 3: tools serialization, ToolUse + ToolResult content blocks +// ----------------------------------------------------------------------------- + +const tool_mod = @import("tool.zig"); + +const StaticToolVT = struct { + fn invoke(_: *anyopaque, _: []const u8, _: Allocator) anyerror!tool_mod.ResultParts { + return error.NotImplementedInTest; + } + fn deinit_(_: *anyopaque, _: Allocator) void {} + const v: tool_mod.Tool.VTable = .{ .invoke = invoke, .deinit = deinit_ }; +}; +var static_tool_ctx_sentinel: u8 = 0; +fn makeStaticTool( + name: []const u8, + description: []const u8, + schema: []const u8, +) tool_mod.Tool { + return .{ + .decl = .{ + .name = name, + .description = description, + .schema_json = schema, + }, + .ctx = &static_tool_ctx_sentinel, + .vtable = &StaticToolVT.v, + }; +} + +test "serializeRequest - emits tools array when registry non-empty" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call something"); + + var tools = tool_registry_mod.ToolRegistry.init(allocator); + defer tools.deinit(); + try tools.register(makeStaticTool("echo", "Echo a message back.", + \\{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]} + )); + + const cfg = testConfig("claude-x"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const arr = parsed.value.object.get("tools").?.array.items; + try testing.expectEqual(@as(usize, 1), arr.len); + try testing.expectEqualStrings("echo", arr[0].object.get("name").?.string); + try testing.expectEqualStrings("Echo a message back.", arr[0].object.get("description").?.string); + + const schema = arr[0].object.get("input_schema").?.object; + try testing.expectEqualStrings("object", schema.get("type").?.string); + try testing.expect(schema.get("properties").? == .object); +} + +test "serializeRequest - dotted names are wire-encoded in tools and history" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + // A prior assistant tool_use replayed from history (dotted internally). + const id = try allocator.dupe(u8, "tu_1"); + const name = try allocator.dupe(u8, "std.write"); + var input: conversation.TextualBlock = .empty; + try input.appendSlice(allocator, "{}"); + try conv.addAssistantMessage(&.{ + .{ .ToolUse = .{ .id = id, .name = name, .input = input } }, + }, null); + + var tools = tool_registry_mod.ToolRegistry.init(allocator); + defer tools.deinit(); + try tools.register(makeStaticTool("std.read", "Read.", "{\"type\":\"object\"}")); + + const cfg = testConfig("claude-x"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + // Tool list: `std.read` -> `std__read`. + const tool_name = parsed.value.object.get("tools").?.array.items[0] + .object.get("name").?.string; + try testing.expectEqualStrings("std__read", tool_name); + + // Replayed history tool_use: `std.write` -> `std__write`. + const hist_name = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items[0].object.get("name").?.string; + try testing.expectEqualStrings("std__write", hist_name); +} + +test "serializeRequest - assistant ToolUse becomes tool_use content block" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id = try allocator.dupe(u8, "tu_1"); + const name = try allocator.dupe(u8, "echo"); + var input: conversation.TextualBlock = .empty; + try input.appendSlice(allocator, "{\"message\":\"hi\"}"); + + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "calling tool") }, + .{ .ToolUse = .{ .id = id, .name = name, .input = input } }, + }, null); + + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const cfg = testConfig("claude-x"); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const content = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 2), content.len); + try testing.expectEqualStrings("text", content[0].object.get("type").?.string); + + try testing.expectEqualStrings("tool_use", content[1].object.get("type").?.string); + try testing.expectEqualStrings("tu_1", content[1].object.get("id").?.string); + try testing.expectEqualStrings("echo", content[1].object.get("name").?.string); + // `input` is a nested JSON object, NOT a string. + const inp = content[1].object.get("input").?.object; + try testing.expectEqualStrings("hi", inp.get("message").?.string); +} + +test "serializeRequest - user ToolResult becomes tool_result content block" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id = try allocator.dupe(u8, "tu_1"); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "the answer is 42") }); + + var blocks: std.ArrayList(conversation.ContentBlock) = .empty; + try blocks.append(allocator, .{ .ToolResult = .{ .tool_use_id = id, .parts = parts } }); + try conv.messages.append(allocator, .{ .role = .user, .content = blocks }); + + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const cfg = testConfig("claude-x"); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msg = parsed.value.object.get("messages").?.array.items[0].object; + try testing.expectEqualStrings("user", msg.get("role").?.string); + + const arr = msg.get("content").?.array.items; + try testing.expectEqual(@as(usize, 1), arr.len); + try testing.expectEqualStrings("tool_result", arr[0].object.get("type").?.string); + try testing.expectEqualStrings("tu_1", arr[0].object.get("tool_use_id").?.string); + // A successful result omits the is_error marker entirely. + try testing.expect(arr[0].object.get("is_error") == null); + const tr_content = arr[0].object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 1), tr_content.len); + try testing.expectEqualStrings("text", tr_content[0].object.get("type").?.string); + try testing.expectEqualStrings("the answer is 42", tr_content[0].object.get("text").?.string); +} + +test "serializeRequest - error ToolResult emits is_error: true" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id = try allocator.dupe(u8, "tu_err"); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "file not found") }); + + var blocks: std.ArrayList(conversation.ContentBlock) = .empty; + try blocks.append(allocator, .{ .ToolResult = .{ .tool_use_id = id, .parts = parts, .is_error = true } }); + try conv.messages.append(allocator, .{ .role = .user, .content = blocks }); + + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const cfg = testConfig("claude-x"); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msg = parsed.value.object.get("messages").?.array.items[0].object; + const arr = msg.get("content").?.array.items; + try testing.expectEqualStrings("tool_result", arr[0].object.get("type").?.string); + try testing.expect(arr[0].object.get("is_error").?.bool); +} + +test "serializeRequest - tool result with image part emits image block" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id = try allocator.dupe(u8, "tu_img"); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "the file:") }); + try parts.append(allocator, .{ .media = .{ + .media_type = try allocator.dupe(u8, "image/png"), + .data = try conversation.textualBlockFromSlice(allocator, "iVBOR=="), + } }); + var blocks: std.ArrayList(conversation.ContentBlock) = .empty; + try blocks.append(allocator, .{ .ToolResult = .{ .tool_use_id = id, .parts = parts } }); + try conv.messages.append(allocator, .{ .role = .user, .content = blocks }); + + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const cfg = testConfig("claude-x"); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msg = parsed.value.object.get("messages").?.array.items[0].object; + const arr = msg.get("content").?.array.items[0].object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 2), arr.len); + try testing.expectEqualStrings("text", arr[0].object.get("type").?.string); + try testing.expectEqualStrings("image", arr[1].object.get("type").?.string); + const src = arr[1].object.get("source").?.object; + try testing.expectEqualStrings("base64", src.get("type").?.string); + try testing.expectEqualStrings("image/png", src.get("media_type").?.string); + try testing.expectEqualStrings("iVBOR==", src.get("data").?.string); +} + +test "serializeRequest - thinking disabled omits thinking and effort fields" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var cfg = testConfig("claude-x"); + cfg.thinking = .disabled; + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const root = parsed.value.object; + try testing.expect(root.get("thinking") == null); + try testing.expect(root.get("effort") == null); +} + +test "serializeRequest - thinking enabled explicit budget" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var cfg = testConfig("claude-x"); // max_tokens = 1024 + cfg.thinking = .enabled; + cfg.thinking_budget_tokens = 500; // within max_tokens + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const root = parsed.value.object; + const th = root.get("thinking").?.object; + try testing.expectEqualStrings("enabled", th.get("type").?.string); + try testing.expectEqual(@as(i64, 500), th.get("budget_tokens").?.integer); + try testing.expectEqualStrings("summarized", th.get("display").?.string); + try testing.expect(root.get("effort") == null); +} + +test "serializeRequest - thinking enabled null budget falls back to max_tokens - 1" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var cfg = testConfig("claude-x"); // max_tokens = 1024 + cfg.thinking = .enabled; + cfg.thinking_budget_tokens = null; + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const th = parsed.value.object.get("thinking").?.object; + try testing.expectEqual(@as(i64, 1023), th.get("budget_tokens").?.integer); +} + +test "serializeRequest - thinking enabled budget clamped when >= max_tokens" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var cfg = testConfig("claude-x"); // max_tokens = 1024 + cfg.thinking = .enabled; + cfg.thinking_budget_tokens = 2_000; // > max_tokens + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const th = parsed.value.object.get("thinking").?.object; + // Clamped to max_tokens - 1 = 1023 + try testing.expectEqual(@as(i64, 1023), th.get("budget_tokens").?.integer); +} + +test "serializeRequest - thinking adaptive default effort" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var cfg = testConfig("claude-x"); + cfg.thinking = .adaptive; + // effort defaults to .medium + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const root = parsed.value.object; + const th = root.get("thinking").?.object; + try testing.expectEqualStrings("adaptive", th.get("type").?.string); + try testing.expectEqualStrings("summarized", th.get("display").?.string); + try testing.expect(th.get("budget_tokens") == null); + try testing.expect(root.get("effort") == null); + const oc = root.get("output_config").?.object; + try testing.expectEqualStrings("medium", oc.get("effort").?.string); +} + +test "serializeRequest - thinking adaptive explicit effort levels" { + const allocator = testing.allocator; + const efforts = [_]struct { e: config_mod.Effort, name: []const u8 }{ + .{ .e = .low, .name = "low" }, + .{ .e = .medium, .name = "medium" }, + .{ .e = .high, .name = "high" }, + .{ .e = .xhigh, .name = "xhigh" }, + .{ .e = .max, .name = "max" }, + }; + for (efforts) |entry| { + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var cfg = testConfig("claude-x"); + cfg.thinking = .adaptive; + cfg.effort = entry.e; + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const oc = parsed.value.object.get("output_config").?.object; + try testing.expectEqualStrings(entry.name, oc.get("effort").?.string); + } +} + +test "serializeRequest - thinking adaptive ignores budget_tokens" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var cfg = testConfig("claude-x"); + cfg.thinking = .adaptive; + cfg.thinking_budget_tokens = 9_999; // should be ignored + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const th = parsed.value.object.get("thinking").?.object; + try testing.expect(th.get("budget_tokens") == null); +} + +test "serializeRequest - empty ToolUse input becomes {} not invalid JSON" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id = try allocator.dupe(u8, "tu_1"); + const name = try allocator.dupe(u8, "noargs"); + try conv.addAssistantMessage(&.{ + .{ .ToolUse = .{ .id = id, .name = name, .input = .empty } }, + }, null); + + var empty_tools = tool_registry_mod.ToolRegistry.init(allocator); + defer empty_tools.deinit(); + const cfg = testConfig("claude-x"); + const body = try serializeRequest(allocator, &cfg, &conv, &empty_tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const tu = parsed.value.object.get("messages").?.array.items[0] + .object.get("content").?.array.items[0].object; + try testing.expectEqualStrings("tool_use", tu.get("type").?.string); + try testing.expect(tu.get("input").? == .object); + try testing.expectEqual(@as(usize, 0), tu.get("input").?.object.count()); +} diff --git a/src/auth.zig b/src/auth.zig new file mode 100644 index 0000000..f70b031 --- /dev/null +++ b/src/auth.zig @@ -0,0 +1,954 @@ +//! Provider authentication: named auth sessions resolved into request-ready +//! credentials before a provider stream opens. +//! +//! A provider names the auth session it uses (`auth = "<name>"`); core +//! resolves that session into a `ResolvedCredential` (a bearer/api-key value, +//! an optional dynamic `base_url`, and optional auth-derived request headers). +//! This covers three families with one configuration shape: +//! +//! - `api_key` — static key from a literal or an environment variable. +//! - `oauth_device` — OAuth 2.0 device-authorization flow. Two dialects: +//! * `token` — standard device flow (GitHub Copilot). The poll endpoint +//! returns the OAuth token response directly. +//! * `codex` — OpenAI Codex device flow. The poll endpoint returns an +//! authorization code plus a server-generated PKCE verifier; core +//! exchanges those at `token_url` for `{access,refresh,id}_token`. +//! +//! This module owns the *mechanism* (config types, the persisted token shape, +//! and — added incrementally — the HTTP flows and refresh lifecycle). It is +//! UI- and filesystem-policy-agnostic: token storage takes a directory path, +//! and interactive device-code prompts are delivered through a caller-supplied +//! `Presenter`. That keeps libpanto reusable while the embedder owns token +//! storage policy and how a device code is shown to the user. + +const std = @import("std"); +const builtin = @import("builtin"); +const Io = std.Io; +const config_mod = @import("config.zig"); +const http = @import("http_helper.zig"); + +/// A single request header. Re-exported from `config.zig` so callers can +/// build auth-derived headers and provider `extra_headers` from one type. +pub const Header = config_mod.Header; + +// =========================================================================== +// Auth configuration (parsed from `[auth.<name>]`) +// =========================================================================== + +pub const AuthType = enum { api_key, oauth_device }; + +/// Device-flow completion shape. See the module doc. +pub const DeviceDialect = enum { token, codex }; + +/// Wire encoding for the `token` dialect's device-code / poll request bodies. +pub const TokenRequestFormat = enum { form, json }; + +/// Static API-key auth. `key` is the resolved credential — a literal, or the +/// result of `${env:VAR}`-style substitution performed by the embedder. An +/// absent/empty key means unresolved; providers using such a session are +/// omitted from the active config (export the key and they reappear). +pub const ApiKeyAuth = struct { + key: ?[]const u8 = null, +}; + +/// A secondary token exchange run after the durable OAuth token is obtained +/// (GitHub Copilot's `/copilot_internal/v2/token`). The exchange result — +/// not the OAuth token — becomes the request credential, and may also +/// override the provider `base_url`. +pub const ExchangeConfig = struct { + method: []const u8 = "GET", + url: []const u8, + /// Which stored token to send as the exchange request's bearer. + bearer: BearerSource = .oauth_access_token, + /// Dotted JSON path to the credential in the exchange response. + token_json_path: []const u8 = "token", + /// Dotted JSON path to the credential's unix-seconds expiry, if any. + expires_at_json_path: ?[]const u8 = null, + /// Dotted JSON path to a dynamic `base_url` in the exchange response. + base_url_json_path: ?[]const u8 = null, + + pub const BearerSource = enum { oauth_access_token }; +}; + +/// OAuth 2.0 device-authorization auth. +pub const OAuthDeviceAuth = struct { + dialect: DeviceDialect = .token, + client_id: []const u8, + /// Endpoint that issues the device/user code. + device_code_url: []const u8, + /// `token` dialect: poll-for-token and refresh endpoint. + /// `codex` dialect: authorization-code exchange and refresh endpoint. + token_url: []const u8, + /// `codex` dialect: endpoint polled for the authorization code. + device_poll_url: ?[]const u8 = null, + /// Browser URL shown to the user (codex). For the `token` dialect the + /// verification URI comes from the device-code response. + verification_url: ?[]const u8 = null, + scope: ?[]const u8 = null, + /// Request encoding for the `token` dialect device-code / poll bodies. + token_request_format: TokenRequestFormat = .form, + /// `codex` dialect: redirect URI sent in the code exchange. + redirect_uri: ?[]const u8 = null, + /// `codex` dialect: JWT claim (on the id_token) holding the account id, + /// e.g. `https://api.openai.com/auth`. When set, the resolver extracts an + /// account id and emits it as a header (see provider wiring). + account_id_jwt_claim: ?[]const u8 = null, + /// Optional post-login token exchange (Copilot). + exchange: ?ExchangeConfig = null, +}; + +/// One named auth session's configuration. The union tag mirrors the TOML +/// `type` field. +pub const AuthConfig = union(AuthType) { + api_key: ApiKeyAuth, + oauth_device: OAuthDeviceAuth, +}; + +// =========================================================================== +// Persisted token state (`<auth_dir>/<name>.json`) +// =========================================================================== + +/// Durable auth state for one session. Only the fields relevant to the auth +/// type are populated. Treat the on-disk file like a password. +pub const TokenSet = struct { + /// `"api_key"` | `"oauth_device"` — records which family wrote the file. + type: []const u8 = "oauth_device", + /// Durable OAuth access token (the `ghu_...` for Copilot; the JWT access + /// token for Codex). + access_token: ?[]const u8 = null, + refresh_token: ?[]const u8 = null, + id_token: ?[]const u8 = null, + /// Unix-seconds expiry of `access_token` (when known; OAuth `expires_in` + /// or a JWT `exp`). Null for tokens with no intrinsic expiry (Copilot's + /// durable `ghu_` token). + expires_at: ?i64 = null, + /// Account id derived from the id_token (Codex `chatgpt-account-id`). + account_id: ?[]const u8 = null, + /// Result of the secondary exchange (Copilot short-lived API token). + exchange: ?ExchangeToken = null, + + pub const ExchangeToken = struct { + token: []const u8, + /// Unix-seconds expiry of the exchanged token. + expires_at: ?i64 = null, + /// Dynamic base_url returned by the exchange, if any. + base_url: ?[]const u8 = null, + }; +}; + +// =========================================================================== +// Resolved credential (handed to the provider for one turn) +// =========================================================================== + +/// The request-ready output of resolving an auth session: the secret to place +/// in the provider's auth header, plus any dynamic base_url and auth-derived +/// headers. The embedder injects these into the active `ProviderConfig`. +pub const ResolvedCredential = struct { + /// Value for the provider auth header (the bearer / x-api-key value). + api_key: []const u8, + /// Overrides the provider's configured `base_url` when present (e.g. + /// Copilot's `endpoints.api`). + base_url_override: ?[]const u8 = null, + /// Auth-derived headers (e.g. `chatgpt-account-id`) to merge onto the + /// provider's request. + extra_headers: []const Header = &.{}, +}; + +// =========================================================================== +// Token storage (`<auth_dir>/<name>.json`) +// =========================================================================== + +/// A parsed `TokenSet` plus the arena owning its strings. Call `deinit`. +pub const ParsedTokenSet = std.json.Parsed(TokenSet); + +/// Owner-only file permissions for token files (POSIX 0o600). On Windows the +/// platform `Permissions` enum has no mode, so fall back to its default. +fn tokenFilePermissions() Io.File.Permissions { + if (builtin.os.tag == .windows) return .default_file; + return Io.File.Permissions.fromMode(0o600); +} + +/// Load and parse `<auth_dir>/<name>.json`. Returns null if the file does not +/// exist. The caller owns the result and must `deinit` it. +pub fn loadTokenSet( + allocator: std.mem.Allocator, + io: Io, + auth_dir: []const u8, + name: []const u8, +) !?ParsedTokenSet { + const fname = try std.fmt.allocPrint(allocator, "{s}.json", .{name}); + defer allocator.free(fname); + + var dir = Io.Dir.cwd().openDir(io, auth_dir, .{}) catch |err| switch (err) { + error.FileNotFound => return null, + else => return err, + }; + defer dir.close(io); + + const bytes = dir.readFileAlloc(io, fname, allocator, .limited(1 << 20)) catch |err| switch (err) { + error.FileNotFound => return null, + else => return err, + }; + defer allocator.free(bytes); + + return try std.json.parseFromSlice(TokenSet, allocator, bytes, .{ + .ignore_unknown_fields = true, + .allocate = .alloc_always, + }); +} + +/// Serialize and write `ts` to `<auth_dir>/<name>.json` with owner-only +/// permissions, creating `auth_dir` if needed. Null optional fields are +/// omitted to keep the file tidy. +pub fn saveTokenSet( + allocator: std.mem.Allocator, + io: Io, + auth_dir: []const u8, + name: []const u8, + ts: TokenSet, +) !void { + Io.Dir.cwd().createDirPath(io, auth_dir) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + + const json = try std.json.Stringify.valueAlloc(allocator, ts, .{ + .emit_null_optional_fields = false, + .whitespace = .indent_2, + }); + defer allocator.free(json); + + const fname = try std.fmt.allocPrint(allocator, "{s}.json", .{name}); + defer allocator.free(fname); + + var dir = try Io.Dir.cwd().openDir(io, auth_dir, .{}); + defer dir.close(io); + try dir.writeFile(io, .{ + .sub_path = fname, + .data = json, + .flags = .{ .permissions = tokenFilePermissions() }, + }); +} + +/// Delete `<auth_dir>/<name>.json`. Returns true if a file was removed, false +/// if it did not exist (idempotent logout). +pub fn deleteTokenSet( + allocator: std.mem.Allocator, + io: Io, + auth_dir: []const u8, + name: []const u8, +) !bool { + const fname = try std.fmt.allocPrint(allocator, "{s}.json", .{name}); + defer allocator.free(fname); + var dir = Io.Dir.cwd().openDir(io, auth_dir, .{}) catch |err| switch (err) { + error.FileNotFound => return false, + else => return err, + }; + defer dir.close(io); + dir.deleteFile(io, fname) catch |err| switch (err) { + error.FileNotFound => return false, + else => return err, + }; + return true; +} + +// =========================================================================== +// OAuth device flow +// =========================================================================== + +pub const AuthError = error{ + /// No persisted token and no interactive presenter to run a login. + AuthLoginRequired, + /// The OAuth endpoint returned an error (denied, expired, malformed). + AuthFlowFailed, + /// The device-flow poll exceeded its deadline. + AuthLoginTimeout, + /// The configured auth could not be resolved into a credential. + AuthUnresolved, +}; + +/// The device-code prompt shown to the user. The embedder renders this +/// (a verification URL + a short user code) however suits its UI. +pub const DeviceCodePrompt = struct { + verification_uri: []const u8, + user_code: []const u8, + /// Seconds until the code expires (0 if the endpoint didn't say). + expires_in: i64 = 0, +}; + +/// Caller-supplied UI hook for interactive device login. `onDeviceCode` is +/// called once with the prompt; `onStatus` (optional) receives progress text. +pub const Presenter = struct { + ptr: *anyopaque, + vtable: *const VTable, + + pub const VTable = struct { + on_device_code: *const fn (ptr: *anyopaque, prompt: DeviceCodePrompt) void, + on_status: ?*const fn (ptr: *anyopaque, msg: []const u8) void = null, + }; + + fn deviceCode(self: Presenter, prompt: DeviceCodePrompt) void { + self.vtable.on_device_code(self.ptr, prompt); + } + fn status(self: Presenter, msg: []const u8) void { + if (self.vtable.on_status) |f| f(self.ptr, msg); + } +}; + +/// The OAuth token-endpoint response, fields borrowed from an arena. +pub const OAuthTokens = struct { + access_token: []const u8, + refresh_token: ?[]const u8 = null, + id_token: ?[]const u8 = null, + /// Seconds-to-live from `expires_in`, if present. + expires_in: ?i64 = null, +}; + +/// Identifiers needed to poll a started device authorization. +const DeviceAuth = struct { + /// `token` dialect poll parameter. + device_code: ?[]const u8 = null, + /// `codex` dialect poll parameter. + device_auth_id: ?[]const u8 = null, + user_code: []const u8, + verification_uri: []const u8, + interval_secs: u32 = 5, + expires_in: i64 = 0, +}; + +/// `application/x-www-form-urlencoded` body from name/value pairs. Values are +/// percent-encoded (unreserved chars pass through). Allocated in `arena`. +fn formEncode(arena: std.mem.Allocator, pairs: []const [2][]const u8) ![]u8 { + var out: std.ArrayList(u8) = .empty; + for (pairs, 0..) |kv, i| { + if (i != 0) try out.append(arena, '&'); + try percentEncode(arena, &out, kv[0]); + try out.append(arena, '='); + try percentEncode(arena, &out, kv[1]); + } + return out.toOwnedSlice(arena); +} + +fn percentEncode(arena: std.mem.Allocator, out: *std.ArrayList(u8), s: []const u8) !void { + for (s) |c| { + const unreserved = (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or + (c >= '0' and c <= '9') or c == '-' or c == '.' or c == '_' or c == '~'; + if (unreserved) { + try out.append(arena, c); + } else { + try out.print(arena, "%{X:0>2}", .{c}); + } + } +} + +/// Build the device-code / token request body in the dialect's encoding. +fn encodeBody( + arena: std.mem.Allocator, + format: TokenRequestFormat, + pairs: []const [2][]const u8, +) !struct { body: []u8, content_type: []const u8 } { + switch (format) { + .form => return .{ .body = try formEncode(arena, pairs), .content_type = "application/x-www-form-urlencoded" }, + .json => { + var aw: std.Io.Writer.Allocating = .init(arena); + var s: std.json.Stringify = .{ .writer = &aw.writer }; + try s.beginObject(); + for (pairs) |kv| { + try s.objectField(kv[0]); + try s.write(kv[1]); + } + try s.endObject(); + return .{ .body = aw.written(), .content_type = "application/json" }; + }, + } +} + +/// Request a device/user code. `headers` are the client-identity headers +/// (the provider's `extra_headers`) sent on every auth HTTP call. Allocates +/// the result into `arena`. +fn requestDeviceAuth( + arena: std.mem.Allocator, + client: *std.http.Client, + oauth: OAuthDeviceAuth, + headers: []const Header, +) !DeviceAuth { + const enc = switch (oauth.dialect) { + // GitHub: `client_id` (+ optional scope), in the configured encoding. + .token => try encodeBody(arena, oauth.token_request_format, blk: { + if (oauth.scope) |sc| { + break :blk &[_][2][]const u8{ .{ "client_id", oauth.client_id }, .{ "scope", sc } }; + } else break :blk &[_][2][]const u8{.{ "client_id", oauth.client_id }}; + }), + // Codex: JSON `{client_id}`. + .codex => try encodeBody(arena, .json, &.{.{ "client_id", oauth.client_id }}), + }; + + const resp = try http.request(arena, client, .POST, oauth.device_code_url, .{ + .headers = headers, + .body = enc.body, + .content_type = enc.content_type, + }); + if (!resp.ok()) return AuthError.AuthFlowFailed; + + const parsed = std.json.parseFromSlice(std.json.Value, arena, resp.body, .{}) catch + return AuthError.AuthFlowFailed; + const v = parsed.value; + + const user_code = http.jsonStringAtPath(v, "user_code") orelse + http.jsonStringAtPath(v, "usercode") orelse return AuthError.AuthFlowFailed; + const interval: u32 = blk: { + if (http.jsonIntAtPath(v, "interval")) |iv| break :blk @intCast(@max(iv, 1)); + // Codex returns interval as a string. + if (http.jsonStringAtPath(v, "interval")) |is| { + if (std.fmt.parseInt(i64, is, 10) catch null) |iv| break :blk @intCast(@max(iv, 1)); + } + break :blk 5; + }; + const verification_uri = switch (oauth.dialect) { + .token => http.jsonStringAtPath(v, "verification_uri") orelse + http.jsonStringAtPath(v, "verification_uri_complete") orelse return AuthError.AuthFlowFailed, + .codex => oauth.verification_url orelse return AuthError.AuthFlowFailed, + }; + return .{ + .device_code = http.jsonStringAtPath(v, "device_code"), + .device_auth_id = http.jsonStringAtPath(v, "device_auth_id"), + .user_code = user_code, + .verification_uri = verification_uri, + .interval_secs = interval, + .expires_in = http.jsonIntAtPath(v, "expires_in") orelse 0, + }; +} + +const PollResult = union(enum) { + pending, + done: OAuthTokens, +}; + +/// Poll once for completion. Allocates any returned tokens into `arena`. +fn pollOnce( + arena: std.mem.Allocator, + client: *std.http.Client, + oauth: OAuthDeviceAuth, + da: DeviceAuth, + headers: []const Header, +) !PollResult { + switch (oauth.dialect) { + .token => { + const enc = try encodeBody(arena, oauth.token_request_format, &.{ + .{ "client_id", oauth.client_id }, + .{ "device_code", da.device_code orelse return AuthError.AuthFlowFailed }, + .{ "grant_type", "urn:ietf:params:oauth:grant-type:device_code" }, + }); + const resp = try http.request(arena, client, .POST, oauth.token_url, .{ + .headers = headers, + .body = enc.body, + .content_type = enc.content_type, + }); + const parsed = std.json.parseFromSlice(std.json.Value, arena, resp.body, .{}) catch + return AuthError.AuthFlowFailed; + const v = parsed.value; + if (http.jsonStringAtPath(v, "error")) |e| { + if (std.mem.eql(u8, e, "authorization_pending") or std.mem.eql(u8, e, "slow_down")) + return .pending; + return AuthError.AuthFlowFailed; + } + const tokens = parseOAuthTokens(v) orelse return AuthError.AuthFlowFailed; + return .{ .done = tokens }; + }, + .codex => { + const poll_url = oauth.device_poll_url orelse return AuthError.AuthFlowFailed; + const enc = try encodeBody(arena, .json, &.{ + .{ "device_auth_id", da.device_auth_id orelse return AuthError.AuthFlowFailed }, + .{ "user_code", da.user_code }, + }); + const resp = try http.request(arena, client, .POST, poll_url, .{ + .headers = headers, + .body = enc.body, + .content_type = enc.content_type, + }); + // 403/404 => still pending (reference behavior). + if (resp.status == 403 or resp.status == 404) return .pending; + if (!resp.ok()) return AuthError.AuthFlowFailed; + const parsed = std.json.parseFromSlice(std.json.Value, arena, resp.body, .{}) catch + return AuthError.AuthFlowFailed; + const code = http.jsonStringAtPath(parsed.value, "authorization_code") orelse return .pending; + const verifier = http.jsonStringAtPath(parsed.value, "code_verifier") orelse return AuthError.AuthFlowFailed; + // Exchange the authorization code + server-generated PKCE verifier. + const tokens = try exchangeCode(arena, client, oauth, code, verifier, headers); + return .{ .done = tokens }; + }, + } +} + +/// Codex authorization-code exchange at the token endpoint. +fn exchangeCode( + arena: std.mem.Allocator, + client: *std.http.Client, + oauth: OAuthDeviceAuth, + code: []const u8, + verifier: []const u8, + headers: []const Header, +) !OAuthTokens { + const redirect = oauth.redirect_uri orelse "https://auth.openai.com/deviceauth/callback"; + const enc = try encodeBody(arena, .form, &.{ + .{ "grant_type", "authorization_code" }, + .{ "client_id", oauth.client_id }, + .{ "code", code }, + .{ "code_verifier", verifier }, + .{ "redirect_uri", redirect }, + }); + const resp = try http.request(arena, client, .POST, oauth.token_url, .{ + .headers = headers, + .body = enc.body, + .content_type = enc.content_type, + }); + if (!resp.ok()) return AuthError.AuthFlowFailed; + const parsed = std.json.parseFromSlice(std.json.Value, arena, resp.body, .{}) catch + return AuthError.AuthFlowFailed; + return parseOAuthTokens(parsed.value) orelse AuthError.AuthFlowFailed; +} + +/// Refresh an access token. Allocates into `arena`. +pub fn refreshTokens( + arena: std.mem.Allocator, + client: *std.http.Client, + oauth: OAuthDeviceAuth, + refresh_token: []const u8, + headers: []const Header, +) !OAuthTokens { + const enc = try encodeBody(arena, .form, &.{ + .{ "grant_type", "refresh_token" }, + .{ "refresh_token", refresh_token }, + .{ "client_id", oauth.client_id }, + }); + const resp = try http.request(arena, client, .POST, oauth.token_url, .{ + .headers = headers, + .body = enc.body, + .content_type = enc.content_type, + }); + if (!resp.ok()) return AuthError.AuthFlowFailed; + const parsed = std.json.parseFromSlice(std.json.Value, arena, resp.body, .{}) catch + return AuthError.AuthFlowFailed; + return parseOAuthTokens(parsed.value) orelse AuthError.AuthFlowFailed; +} + +/// Pluck an `OAuthTokens` out of a parsed token response. Null if no +/// `access_token`. Borrows from the parsed value. +fn parseOAuthTokens(v: std.json.Value) ?OAuthTokens { + const access = http.jsonStringAtPath(v, "access_token") orelse return null; + return .{ + .access_token = access, + .refresh_token = http.jsonStringAtPath(v, "refresh_token"), + .id_token = http.jsonStringAtPath(v, "id_token"), + .expires_in = http.jsonIntAtPath(v, "expires_in"), + }; +} + +/// Run the full interactive device login: request a code, present it, then +/// poll until authorized (or the deadline passes). Allocates the resulting +/// tokens into `arena`. +pub fn login( + arena: std.mem.Allocator, + io: Io, + client: *std.http.Client, + oauth: OAuthDeviceAuth, + presenter: Presenter, + headers: []const Header, +) !OAuthTokens { + const da = try requestDeviceAuth(arena, client, oauth, headers); + presenter.deviceCode(.{ + .verification_uri = da.verification_uri, + .user_code = da.user_code, + .expires_in = da.expires_in, + }); + presenter.status("waiting for authorization…"); + + const start_ns = std.Io.Clock.now(.real, io).nanoseconds; + const deadline_ns = start_ns + 15 * std.time.ns_per_min; + while (true) { + io.sleep(.fromSeconds(@intCast(da.interval_secs)), .real) catch {}; + const result = try pollOnce(arena, client, oauth, da, headers); + switch (result) { + .pending => {}, + .done => |toks| return toks, + } + if (std.Io.Clock.now(.real, io).nanoseconds >= deadline_ns) return AuthError.AuthLoginTimeout; + } +} + +// =========================================================================== +// Secondary token exchange (Copilot) +// =========================================================================== + +/// Run the configured exchange (Copilot's `/v2/token`) using `bearer` as the +/// request bearer. Returns the exchanged token + optional expiry/base_url, +/// allocated into `arena`. +pub fn runExchange( + arena: std.mem.Allocator, + client: *std.http.Client, + exchange: ExchangeConfig, + bearer: []const u8, + headers: []const Header, +) !TokenSet.ExchangeToken { + const auth_value = try std.fmt.allocPrint(arena, "Bearer {s}", .{bearer}); + var hdrs: std.ArrayList(Header) = .empty; + try hdrs.append(arena, .{ .name = "authorization", .value = auth_value }); + for (headers) |h| try hdrs.append(arena, h); + + const method: http.Method = if (std.ascii.eqlIgnoreCase(exchange.method, "POST")) .POST else .GET; + const resp = try http.request(arena, client, method, exchange.url, .{ .headers = hdrs.items }); + if (!resp.ok()) return AuthError.AuthFlowFailed; + + const parsed = std.json.parseFromSlice(std.json.Value, arena, resp.body, .{}) catch + return AuthError.AuthFlowFailed; + const v = parsed.value; + const token = http.jsonStringAtPath(v, exchange.token_json_path) orelse return AuthError.AuthFlowFailed; + return .{ + .token = token, + .expires_at = if (exchange.expires_at_json_path) |p| http.jsonIntAtPath(v, p) else null, + .base_url = if (exchange.base_url_json_path) |p| http.jsonStringAtPath(v, p) else null, + }; +} + +// =========================================================================== +// JWT + credential building (pure) +// =========================================================================== + +/// Decode a JWT's payload (the middle segment) into a parsed JSON value. +/// Returns null if the token is malformed. Caller owns the result. +pub fn decodeJwtPayload(allocator: std.mem.Allocator, token: []const u8) ?std.json.Parsed(std.json.Value) { + var it = std.mem.splitScalar(u8, token, '.'); + _ = it.next() orelse return null; // header + const payload_b64 = it.next() orelse return null; + if (it.next() == null) return null; // require a signature segment + + const dec = std.base64.url_safe_no_pad.Decoder; + const n = dec.calcSizeForSlice(payload_b64) catch return null; + const buf = allocator.alloc(u8, n) catch return null; + defer allocator.free(buf); + dec.decode(buf, payload_b64) catch return null; + return std.json.parseFromSlice(std.json.Value, allocator, buf, .{}) catch null; +} + +/// The `exp` (unix-seconds expiry) claim of a JWT access/id token, or null. +pub fn parseJwtExp(allocator: std.mem.Allocator, token: []const u8) ?i64 { + var parsed = decodeJwtPayload(allocator, token) orelse return null; + defer parsed.deinit(); + return http.jsonIntAtPath(parsed.value, "exp"); +} + +/// Extract the ChatGPT account id from an id_token. `claim_path` is the JWT +/// claim holding the auth object (e.g. `https://api.openai.com/auth`); the +/// account id is its `chatgpt_account_id` field. Returns an owned copy. +pub fn extractAccountId( + allocator: std.mem.Allocator, + id_token: []const u8, + claim_path: []const u8, +) ?[]u8 { + var parsed = decodeJwtPayload(allocator, id_token) orelse return null; + defer parsed.deinit(); + // `claim_path` is a single literal claim key (e.g. + // `https://api.openai.com/auth`) that itself contains dots, so it is + // looked up directly rather than walked as a dotted path. + const claim = switch (parsed.value) { + .object => |o| o.get(claim_path) orelse return null, + else => return null, + }; + const account = switch (claim) { + .object => |o| o.get("chatgpt_account_id") orelse return null, + else => return null, + }; + return switch (account) { + .string => |s| allocator.dupe(u8, s) catch null, + else => null, + }; +} + +/// True when an access token is missing or within `margin` seconds of expiry. +pub fn needsRefresh(ts: TokenSet, now_unix: i64, margin: i64) bool { + if (ts.access_token == null) return false; // nothing to refresh (login path) + const exp = ts.expires_at orelse return false; // no intrinsic expiry (e.g. ghu_) + return exp - now_unix <= margin; +} + +/// True when an exchange is configured but the stored exchange token is +/// missing or within `margin` seconds of expiry. +pub fn needsExchange(oauth: OAuthDeviceAuth, ts: TokenSet, now_unix: i64, margin: i64) bool { + if (oauth.exchange == null) return false; + const ex = ts.exchange orelse return true; + const exp = ex.expires_at orelse return false; // no expiry => assume durable + return exp - now_unix <= margin; +} + +/// Build the request-ready credential from a (refreshed/exchanged) token set. +/// The exchanged token wins over the access token; a codex account id becomes +/// a `chatgpt-account-id` header. All strings are duped into `arena`. +pub fn buildCredential( + arena: std.mem.Allocator, + oauth: OAuthDeviceAuth, + ts: TokenSet, +) !ResolvedCredential { + var base_url_override: ?[]const u8 = null; + const secret: []const u8 = blk: { + if (ts.exchange) |ex| { + if (ex.base_url) |bu| base_url_override = try arena.dupe(u8, bu); + break :blk ex.token; + } + break :blk ts.access_token orelse return AuthError.AuthUnresolved; + }; + + var headers: std.ArrayList(Header) = .empty; + if (oauth.account_id_jwt_claim != null) { + if (ts.account_id) |aid| { + try headers.append(arena, .{ + .name = "chatgpt-account-id", + .value = try arena.dupe(u8, aid), + }); + } + } + + return .{ + .api_key = try arena.dupe(u8, secret), + .base_url_override = base_url_override, + .extra_headers = try headers.toOwnedSlice(arena), + }; +} + +/// Assemble a persisted `TokenSet` from a fresh OAuth token response. +/// `expires_at` is derived from `expires_in` (preferred) or the access +/// token's JWT `exp`. A codex account id is extracted when configured. All +/// strings are duped into `arena`. +pub fn tokensToTokenSet( + arena: std.mem.Allocator, + oauth: OAuthDeviceAuth, + tokens: OAuthTokens, + now_unix: i64, +) !TokenSet { + const access = try arena.dupe(u8, tokens.access_token); + const expires_at: ?i64 = blk: { + if (tokens.expires_in) |e| break :blk now_unix + e; + break :blk parseJwtExp(arena, access); + }; + var account_id: ?[]const u8 = null; + if (oauth.account_id_jwt_claim) |claim| { + if (tokens.id_token) |idt| { + account_id = extractAccountId(arena, idt, claim); + } + } + return .{ + .type = "oauth_device", + .access_token = access, + .refresh_token = if (tokens.refresh_token) |r| try arena.dupe(u8, r) else null, + .id_token = if (tokens.id_token) |i| try arena.dupe(u8, i) else null, + .expires_at = expires_at, + .account_id = account_id, + }; +} + +const t = std.testing; + +test "token storage: save, load, delete round-trip" { + const io = t.io; + var tmp = t.tmpDir(.{}); + defer tmp.cleanup(); + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const n = try tmp.dir.realPath(io, &path_buf); + const auth_dir = try std.fmt.allocPrint(t.allocator, "{s}/auth", .{path_buf[0..n]}); + defer t.allocator.free(auth_dir); + + // Absent => null. + try t.expect((try loadTokenSet(t.allocator, io, auth_dir, "ghost")) == null); + + const ts: TokenSet = .{ + .type = "oauth_device", + .access_token = "ghu_abc", + .refresh_token = "rt_xyz", + .expires_at = 1700000000, + .exchange = .{ .token = "tkn", .expires_at = 1700001800, .base_url = "https://api.x" }, + }; + try saveTokenSet(t.allocator, io, auth_dir, "github_copilot", ts); + + var loaded = (try loadTokenSet(t.allocator, io, auth_dir, "github_copilot")).?; + defer loaded.deinit(); + try t.expectEqualStrings("ghu_abc", loaded.value.access_token.?); + try t.expectEqualStrings("rt_xyz", loaded.value.refresh_token.?); + try t.expectEqual(@as(?i64, 1700000000), loaded.value.expires_at); + try t.expect(loaded.value.id_token == null); + try t.expectEqualStrings("tkn", loaded.value.exchange.?.token); + try t.expectEqualStrings("https://api.x", loaded.value.exchange.?.base_url.?); + + try t.expect(try deleteTokenSet(t.allocator, io, auth_dir, "github_copilot")); + try t.expect(!try deleteTokenSet(t.allocator, io, auth_dir, "github_copilot")); + try t.expect((try loadTokenSet(t.allocator, io, auth_dir, "github_copilot")) == null); +} + +test "AuthConfig: api_key variant" { + const a: AuthConfig = .{ .api_key = .{ .key = "sk-resolved" } }; + try t.expectEqual(AuthType.api_key, @as(AuthType, a)); + try t.expectEqualStrings("sk-resolved", a.api_key.key.?); +} + +test "AuthConfig: oauth_device defaults" { + const a: AuthConfig = .{ .oauth_device = .{ + .client_id = "Iv1.x", + .device_code_url = "https://github.com/login/device/code", + .token_url = "https://github.com/login/oauth/access_token", + } }; + try t.expectEqual(DeviceDialect.token, a.oauth_device.dialect); + try t.expectEqual(TokenRequestFormat.form, a.oauth_device.token_request_format); + try t.expect(a.oauth_device.exchange == null); +} + +test "TokenSet: defaults" { + const ts: TokenSet = .{}; + try t.expectEqualStrings("oauth_device", ts.type); + try t.expect(ts.access_token == null); + try t.expect(ts.exchange == null); +} + +/// Build a `header.payload.sig` JWT whose payload is `payload_json`, +/// base64url-no-pad encoded. Allocated in `arena`. +fn makeJwt(arena: std.mem.Allocator, payload_json: []const u8) ![]u8 { + const enc = std.base64.url_safe_no_pad.Encoder; + const header = "{\"alg\":\"none\"}"; + var hbuf: [64]u8 = undefined; + const h = enc.encode(&hbuf, header); + const pbuf = try arena.alloc(u8, enc.calcSize(payload_json.len)); + const p = enc.encode(pbuf, payload_json); + return std.fmt.allocPrint(arena, "{s}.{s}.sig", .{ h, p }); +} + +test "parseJwtExp + extractAccountId" { + var aa = std.heap.ArenaAllocator.init(t.allocator); + defer aa.deinit(); + const a = aa.allocator(); + const jwt = try makeJwt(a, + \\{"exp":1700000000,"https://api.openai.com/auth":{"chatgpt_account_id":"acct_123"}} + ); + try t.expectEqual(@as(?i64, 1700000000), parseJwtExp(a, jwt)); + const aid = extractAccountId(a, jwt, "https://api.openai.com/auth").?; + try t.expectEqualStrings("acct_123", aid); + // Wrong claim path => null. + try t.expect(extractAccountId(a, jwt, "nope") == null); + // Malformed token => null. + try t.expect(parseJwtExp(a, "not-a-jwt") == null); +} + +test "parseOAuthTokens: required access_token, optional rest" { + var parsed = try std.json.parseFromSlice(std.json.Value, t.allocator, + \\{"access_token":"at","refresh_token":"rt","id_token":"it","expires_in":1800} + , .{}); + defer parsed.deinit(); + const toks = parseOAuthTokens(parsed.value).?; + try t.expectEqualStrings("at", toks.access_token); + try t.expectEqualStrings("rt", toks.refresh_token.?); + try t.expectEqual(@as(?i64, 1800), toks.expires_in); + + var p2 = try std.json.parseFromSlice(std.json.Value, t.allocator, "{\"error\":\"x\"}", .{}); + defer p2.deinit(); + try t.expect(parseOAuthTokens(p2.value) == null); +} + +test "formEncode: percent-encodes reserved characters" { + var aa = std.heap.ArenaAllocator.init(t.allocator); + defer aa.deinit(); + const body = try formEncode(aa.allocator(), &.{ + .{ "grant_type", "urn:ietf:params:oauth:grant-type:device_code" }, + .{ "client_id", "Iv1.abc" }, + }); + try t.expectEqualStrings( + "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&client_id=Iv1.abc", + body, + ); +} + +test "needsRefresh / needsExchange predicates" { + const now: i64 = 1000; + // Durable token (no expiry) never needs refresh. + try t.expect(!needsRefresh(.{ .access_token = "x" }, now, 120)); + // Within margin => refresh. + try t.expect(needsRefresh(.{ .access_token = "x", .expires_at = 1100 }, now, 120)); + // Comfortably fresh => no. + try t.expect(!needsRefresh(.{ .access_token = "x", .expires_at = 5000 }, now, 120)); + + const oauth: OAuthDeviceAuth = .{ + .client_id = "c", + .device_code_url = "u", + .token_url = "u", + .exchange = .{ .url = "https://x" }, + }; + // Exchange configured but none stored => needs exchange. + try t.expect(needsExchange(oauth, .{ .access_token = "x" }, now, 120)); + // Stored exchange near expiry => needs exchange. + try t.expect(needsExchange(oauth, .{ .exchange = .{ .token = "t", .expires_at = 1050 } }, now, 120)); + // Fresh exchange => no. + try t.expect(!needsExchange(oauth, .{ .exchange = .{ .token = "t", .expires_at = 9000 } }, now, 120)); + // No exchange configured => never. + const no_ex: OAuthDeviceAuth = .{ .client_id = "c", .device_code_url = "u", .token_url = "u" }; + try t.expect(!needsExchange(no_ex, .{ .access_token = "x" }, now, 120)); +} + +test "buildCredential: copilot exchange token + dynamic base_url" { + var aa = std.heap.ArenaAllocator.init(t.allocator); + defer aa.deinit(); + const oauth: OAuthDeviceAuth = .{ + .client_id = "c", + .device_code_url = "u", + .token_url = "u", + .exchange = .{ .url = "https://x" }, + }; + const ts: TokenSet = .{ + .access_token = "ghu_durable", + .exchange = .{ .token = "tid", .base_url = "https://api.individual.githubcopilot.com" }, + }; + const cred = try buildCredential(aa.allocator(), oauth, ts); + try t.expectEqualStrings("tid", cred.api_key); + try t.expectEqualStrings("https://api.individual.githubcopilot.com", cred.base_url_override.?); + try t.expectEqual(@as(usize, 0), cred.extra_headers.len); +} + +test "buildCredential: codex access token + account-id header" { + var aa = std.heap.ArenaAllocator.init(t.allocator); + defer aa.deinit(); + const oauth: OAuthDeviceAuth = .{ + .dialect = .codex, + .client_id = "c", + .device_code_url = "u", + .token_url = "u", + .device_poll_url = "p", + .account_id_jwt_claim = "https://api.openai.com/auth", + }; + const ts: TokenSet = .{ .access_token = "jwt-access", .account_id = "acct_9" }; + const cred = try buildCredential(aa.allocator(), oauth, ts); + try t.expectEqualStrings("jwt-access", cred.api_key); + try t.expect(cred.base_url_override == null); + try t.expectEqual(@as(usize, 1), cred.extra_headers.len); + try t.expectEqualStrings("chatgpt-account-id", cred.extra_headers[0].name); + try t.expectEqualStrings("acct_9", cred.extra_headers[0].value); +} + +test "tokensToTokenSet: derives expires_at from expires_in and extracts account_id" { + var aa = std.heap.ArenaAllocator.init(t.allocator); + defer aa.deinit(); + const a = aa.allocator(); + const idt = try makeJwt(a, + \\{"https://api.openai.com/auth":{"chatgpt_account_id":"acct_77"}} + ); + const oauth: OAuthDeviceAuth = .{ + .dialect = .codex, + .client_id = "c", + .device_code_url = "u", + .token_url = "u", + .device_poll_url = "p", + .account_id_jwt_claim = "https://api.openai.com/auth", + }; + const ts = try tokensToTokenSet(a, oauth, .{ + .access_token = "at", + .refresh_token = "rt", + .id_token = idt, + .expires_in = 1800, + }, 1000); + try t.expectEqual(@as(?i64, 2800), ts.expires_at); + try t.expectEqualStrings("rt", ts.refresh_token.?); + try t.expectEqualStrings("acct_77", ts.account_id.?); +} diff --git a/src/cdeps/image_impl.c b/src/cdeps/image_impl.c new file mode 100644 index 0000000..6266630 --- /dev/null +++ b/src/cdeps/image_impl.c @@ -0,0 +1,25 @@ +// Single translation unit that compiles the implementations of the +// vendored single-header image libraries. Keeping the implementation +// macros confined to one .c file avoids duplicate-symbol errors. + +#define STB_IMAGE_IMPLEMENTATION +// We only decode raster formats we detect via magic bytes. Disable the +// formats we never feed in to shrink code size and attack surface. +#define STBI_NO_HDR +#define STBI_NO_LINEAR +#define STBI_NO_PSD +#define STBI_NO_PIC +#define STBI_NO_PNM +#define STBI_NO_TGA +#include "stb_image.h" + +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#include "stb_image_resize2.h" + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" + +// We feed jebp bytes from memory only; no file I/O needed. +#define JEBP_NO_STDIO +#define JEBP_IMPLEMENTATION +#include "jebp.h" diff --git a/src/cdeps/jebp.h b/src/cdeps/jebp.h new file mode 100644 index 0000000..dc95d06 --- /dev/null +++ b/src/cdeps/jebp.h @@ -0,0 +1,2457 @@ +/** + * JebP - Single header WebP decoder + */ + +/** + * LICENSE + ** + * MIT No Attribution + * + * Copyright 2022 Jasmine Minter + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Attribution is not required, but would be appreciated :) + +/** + * DOCUMENTATION + ** + * First and foremost, this project uses some custom types: + * `jebp_byte`/`jebp_ubyte` is a singular byte. + * `jebp_short`/jebp_ushort` is an integer of atleast 16-bits. + * `jebp_int`/`jebp_uint` is an integer of atleast 32-bits. + * + * This is a header only file. This means that it operates as a standard header + * and to generate the source file you define `JEBP_IMPLEMENTATION` in ONE file + * only. For example: + * ```c + * #define JEBP_IMPLEMENTATION + * #include "jebp.h" + * ``` + * + * The most basic API call in this library is: + * ```c + * err = jebp_decode(&image, size, data); + * ``` + * where: + * `jebp_image_t *image` is a pointer to an image structure to receive the + * decoded data. + * `size_t size` is the size of the WebP-encoded data buffer. + * `const void *data` is a pointer to the WebP encoded data buffer, `size` + * bytes large. + * `jebp_error_t err` is the result of the operation (OK or an error code). + * + * For reading from a provided file path, this API call can be used instead: + * ```c + * err = jebp_read(&image, path); + * ``` + * where: + * `const char *path` is the path of the file to be read. + * + * It is currently not possible to read from a `FILE *` object. + * If you only want to get the size of the image without a full read, these + * functions can be used instead: + * ```c + * err = jebp_decode_size(&image, size, data); + * err = jebp_read_size(&image, path); + * ``` + * + * The `jebp_image_t` structure has the following properties: + * `jebp_int width` is the width of the image. + * `jebp_int height` is the height of the image. + * `jebp_color_t *pixels` is a pointer to an array pixels. Each `jebp_color_t` + * structure contains four `jebp_ubyte` values for `r`, + * `g`, `b` and `a`. This allows the `pixels` pointer + * to be cast to `jebp_ubyte *` to get an RGBA pixel + * buffer. + * The allocated data in the image can be free'd with: + * ```c + * jebp_free_image(&image); + * ``` + * This function will also clear the structure, notably width and height will be + * set to 0. + * + * The `jebp_error_t` enumeration has the following values: + * `JEBP_OK` means the operation completed successfully. + * `JEBP_ERROR_INVAL` means one of the arguments provided is invalid, usually + * this refers to a NULL pointer. + * `JEBP_ERROR_INVDATA` means the WebP-encoded data is invalid or corrupted. + * `JEBP_ERROR_INVDATA_HEADER` is a suberror of `INVDATA` that indicates that + * the header bytes are invalid. This file is likely not a + * WebP file. + * `JEBP_ERROR_EOF` means the end of the file (or data buffer) was reached + * before the operation could successfully complete. + * `JEBP_ERROR_NOSUP` means there is a feature in the WebP stream that is not + * currently supported (see below). This can also represent + * new features, versions or RIFF-chunks that were not in + * the specification when writing. + * `JEBP_ERROR_NOSUP_CODEC` is a suberror of `NOSUP` that indicates that the + * RIFF chunk that is most likely for the codec is not + * recognized. Currently lossy images are not supported + * (see below) and lossless image support can be disabled + * (see `JEBP_NO_VP8L`). + * `JEBP_ERROR_NOSUP_PALETTE` is a suberror of `NOSUP` that indicates that the + * image has a color-index transform (in WebP terminology, + * this would be a paletted image). Color-indexing + * transforms are not currently supported (see below). Note + * that this error code might be removed after + * color-indexing transform support is added, this is only + * here for now to help detecting common issues. + * `JEBP_ERROR_NOMEM` means that a memory allocation failed, indicating that + * there is no more memory available. + * `JEBP_ERROR_IO` represents any generic I/O error, usually from + * file-reading. + * `JEBP_ERROR_UNKNOWN` means any unknown error. Currently this is only used + * when an unknown value is passed into + * `jebp_error_string`. + * To get a human-readable string of the error, the following function can be + * used: + * ```c + * const char *error = jebp_error_string(err); + * ``` + * + * This is not a feature-complete WebP decoder and has the following + * limitations: + * - Does not support decoding lossy files with VP8. + * - Does not support extended file-formats with VP8X. + * - Does not support VP8L lossless images with the color-indexing transform + * (palleted images). + * - Does not support VP8L images with more than 256 huffman groups. This is + * an arbitrary limit to prevent bad images from using too much memory. In + * theory, images requiring more groups should be very rare. This limit may + * be increased in the future. + * + * Features that will probably never be supported due to complexity or API + * constraints: + * - Decoding color profiles. + * - Decoding metadata. + * - Full color-indexing/palette support will be a bit of a mess, so don't + * expect full support of that coming anytime soon. Simple color-indexing + * support (more than 16 colors, skipping the need for bit-packing) is + * definitely alot more do-able. + * + * Along with `JEBP_IMPLEMENTATION` defined above, there are a few other macros + * that can be defined to change how JebP operates: + * `JEBP_NO_STDIO` will disable the file-reading API. + * `JEBP_NO_SIMD` will disable SIMD optimizations. These are currently + * not-used but the detection is there ready for further work. + * `JEBP_NO_VP8L` will disable VP8L (lossless) decoding support. Note that + * currently this will make all images fail since VP8L is the + * only supported codec right now. + * `JEBP_ALLOC` and `JEBP_FREE` can be defined to functions for a custom + * allocator. They either both have to be defined or neither + * defined. + * + * This single-header library requires C99 to be supported. Along with this it + * requires the following headers from the system to successfully compile. Some + * of these can be disabled with the above macros: + * `stddef.h` is used for the definition of the `size_t` type. + * `limits.h` is used for the `UINT_MAX` macro to check the size of `int`. If + * `int` is not 32-bits, `long` will be used for `jebp_int` + * instead. + * `string.h` is used for `memset` to clear out memory. + * `stdio.h` is used for I/O support and logging errors. If `JEBP_NO_STDIO` is + * defined and `JEBP_LOG_ERRORS` is not defined, this will not be + * included. + * `stdlib.h` is used for the default implementations of `JEBP_ALLOC` + * and `JEBP_FREE`, using `malloc` and `free` respectively. If + * those macros are already defined to something else, this will + * not be included. + * `emmintrin.h` and `arm_neon.h` is used for SIMD intrinsice. If + * `JEBP_NO_SIMD` is defined these will not be included. + * + * The following predefined macros are also used for compiler-feature, SIMD and + * endianness detection. These can be changed or modified before import to + * change JebP's detection logic: + * `__STDC_VERSION__` is used to detect if the compiler supports C99 and also + * checks for C11 support to use `_Noreturn`. + * `__has_attribute` and `__has_builtin` are used to detect the `noreturn` and + * `always_inline` attributes, along with the + * `__builtin_bswap32` builtin. Note that `__has_attribute` + * does not fallback to compiler-version checks since most + * compilers already support `__has_attribute`. + * `__GNUC__` and `__GNUC_MINOR__` are used to detect if the compiler is GCC + * (or GCC compatible) and what version of GCC it is. This, in + * turn, is used to polyfill `__has_builtin` on older compilers + * that may not support it. + * `__clang__` is used to detect the Clang compiler. This is only used to set + * the detected GCC version higher since Clang still marks itself + * as GCC 4.2 by default. No Clang version detection is done. + * `_MSC_VER` is used to detect the MSVC compiler. This is used to check + * support for `__declspec(noreturn)`, `__forceinline` and + * `_byteswap_ulong`. No MSVC version detection is done. + * `__LITTLE_ENDIAN__` is used to check if the architecture is little-endian. + * Note that this is only checked either if the + * architecture cannot be detected or, in special cases, + * where there is not enough information from the + * architecture or compiler to detect endianness. Also + * note that big-endian and other more-obscure endian + * types are not detected. Little-endian is the only + * endianness detected and is used for optimization in a + * few areas. If the architecture is not little-endian or + * cannot be detected as such, a naive solution is used + * instead. + * `__i386`, `__i386__` and `_M_IX86` are used to detect if this is being + * compiled for x86-32 (also known as x86, IA-32, or i386). If one of + * these are defined, it is also assumed that the architecture is + * little-endian. `_M_IX86` is usually present on MSVC, while + * the other two are usually present on most other compilers. + * `__SSE2__` and `_M_IX86_FP` are used to detect SSE2 support on x86-32. + * `_M_IX86`, which is usually present on MSVC, must equal 2 to + * indicate that the code is being compiled for a SSE2-compatible + * floating-point unit. `__SSE2__` is usually present on most other + * compilers. + * `__x86_64`, `__x86_64__` and `_M_X64` are used to detect if this is being + * compiled for x86-64 (also known as AMD64). If one of these are + * defined, it is also assumed that the architecture is little-endian + * and that SSE2 is supported (which is required for x86-64 support). + * `_M_X64` is usually present on MSVC, while the other two are + * usually present on most other compilers. + * `__arm`, `__arm__` and `_M_ARM` are used to detect if this is being + * compiled for AArch32 (also known as arm32 or armhf). If one of + * these are defined on Windows, it is also assumed that Neon is + * supported (which is required for Windows). `_M_ARM` is usually + * present on MSVC while the other two are usually present on most + * other compilers. + * `__ARM_NEON` is used to detect Neon support on AArch32. MSVC doesn't seem + * to support this and I can't find any info on detecting Neon + * support for MSVC. I have found mentions of Windows requiring + * Neon support but cannot find any concrete proof anywhere. + * `__aarch64`, `__aarch64__` and `_M_ARM64` are used to detect if this is + * being compiled for AArch64 (also known as arm64). If one of + * these are defined, it is also assumed that Neon is supported + * (which is required for AArch64 support). `_M_ARM64` is usually + * present on MSVC, while the other two are usually present on + * most other compilers. + * `__ARM_BIG_ENDIAN` is used to detect, on AArch/ARM architectures, if it is + * in big-endian mode. However, as mentioned above, there + * is no special code for big-endian and it's worth noting + * that this is just used to force-disable little-endian. + * If this is not present, it falls back to using + * `__LITTLE_ENDIAN__`. It is also worth noting that MSVC + * does not seem to provide a way to detect endianness. It + * may be that Windows requires little-endian but I can't + * find any concrete sources on this so currently + * little-endian detection is not supported on MSVC. + */ + +/** + * HEADER + */ +#ifndef JEBP__HEADER +#define JEBP__HEADER +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus +#include <limits.h> +#include <stddef.h> + +#if UINT_MAX >= 0xffffffff +#define JEBP__INT int +#else +#define JEBP__INT long +#endif +typedef signed char jebp_byte; +typedef unsigned char jebp_ubyte; +typedef short jebp_short; +typedef unsigned short jebp_ushort; +typedef JEBP__INT jebp_int; +typedef unsigned JEBP__INT jebp_uint; + +typedef enum jebp_error_t { + JEBP_OK, + JEBP_ERROR_INVAL, + JEBP_ERROR_INVDATA, + JEBP_ERROR_INVDATA_HEADER, + JEBP_ERROR_EOF, + JEBP_ERROR_NOSUP, + JEBP_ERROR_NOSUP_CODEC, + JEBP_ERROR_NOSUP_PALETTE, + JEBP_ERROR_NOMEM, + JEBP_ERROR_IO, + JEBP_ERROR_UNKNOWN, + JEBP_NB_ERRORS +} jebp_error_t; + +typedef struct jebp_color_t { + jebp_ubyte r; + jebp_ubyte g; + jebp_ubyte b; + jebp_ubyte a; +} jebp_color_t; + +typedef struct jebp_image_t { + jebp_int width; + jebp_int height; + jebp_color_t *pixels; +} jebp_image_t; + +const char *jebp_error_string(jebp_error_t err); +void jebp_free_image(jebp_image_t *image); +jebp_error_t jebp_decode_size(jebp_image_t *image, size_t size, + const void *data); +jebp_error_t jebp_decode(jebp_image_t *image, size_t size, const void *data); + +// I/O API +#ifndef JEBP_NO_STDIO +jebp_error_t jebp_read_size(jebp_image_t *image, const char *path); +jebp_error_t jebp_read(jebp_image_t *image, const char *path); +#endif // JEBP_NO_STDIO + +#ifdef __cplusplus +} +#endif // __cplusplus +#endif // JEBP__HEADER + +/** + * IMPLEMENTATION + */ +#ifdef JEBP_IMPLEMENTATION +#include <string.h> +#if !defined(JEBP_NO_STDIO) || defined(JEBP_LOG_ERRORS) +#include <stdio.h> +#endif +#if !defined(JEBP_ALLOC) && !defined(JEBP_FREE) +#include <stdlib.h> +#define JEBP_ALLOC malloc +#define JEBP_FREE free +#elif !defined(JEBP_ALLOC) || !defined(JEBP_FREE) +#error "Both JEBP_ALLOC and JEBP_FREE have to be defined" +#endif + +/** + * Predefined macro detection + */ +#ifdef __STDC_VERSION__ +#if __STDC_VERSION__ < 199901 +#error "Standard C99 support is required" +#endif +#else // __STDC_VERSION__ +#if defined(__GNUC__) +#warning "C version cannot be checked, compilation may fail" +#elif defined(_MSC_VER) +#pragma message( \ + "MSVC by default is C89 'with extensions', use /std:c11 to ensure there are no errors") +#endif +#endif // __STDC_VERSION__ +#if defined(__clang__) +// The default GNUC version provided by Clang is just short of what we need +#define JEBP__GNU_VERSION 403 +#elif defined(__GNUC__) +#define JEBP__GNU_VERSION ((__GNUC__ * 100) + __GNUC_MINOR__) +#else +#define JEBP__GNU_VERSION 0 +#endif // __GNUC__ + +#ifdef __has_attribute +#define JEBP__HAS_ATTRIBUTE __has_attribute +#else // __has_attribute +// We don't add GCC version checks since, unlike __has_builtin, __has_attribute +// has been out for so long that its more likely that the compiler supports it. +#define JEBP__HAS_ATTRIBUTE(attr) 0 +#endif // __has_attribute +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +#define JEBP__NORETURN _Noreturn +#elif JEBP__HAS_ATTRIBUTE(noreturn) +#define JEBP__NORETURN __attribute__((noreturn)) +#elif defined(_MSC_VER) +#define JEBP__NORETURN __declspec(noreturn) +#else +#define JEBP__NORETURN +#endif +#if JEBP__HAS_ATTRIBUTE(always_inline) +#define JEBP__ALWAYS_INLINE __attribute__((always_inline)) +#elif defined(_MSC_VER) +#define JEBP__ALWAYS_INLINE __forceinline +#else +#define JEBP__ALWAYS_INLINE +#endif +#define JEBP__INLINE static inline JEBP__ALWAYS_INLINE + +#ifdef __has_builtin +#define JEBP__HAS_BUILTIN __has_builtin +#else // __has_builtin +#define JEBP__HAS_BUILTIN(builtin) \ + JEBP__VERSION##builtin != 0 && JEBP__GNU_VERSION >= JEBP__VERSION##builtin +// I believe this was added earlier but GCC 4.3 is the first time it was +// mentioned in the changelog and manual. +#define JEBP__VERSION__builtin_bswap32 403 +#endif // __has_builtin +#if JEBP__HAS_BUILTIN(__builtin_bswap32) +#define JEBP__SWAP32(value) __builtin_bswap32(value) +#elif defined(_MSC_VER) +#define JEBP__SWAP32(value) _byteswap_ulong(value) +#endif + +// We don't do any SIMD runtime detection since that causes alot of +// heavily-documented issues that I won't go into here. Instead, if the compiler +// supports it (and requests it) we will use it. It helps that both x86-64 and +// AArch64 always support the SIMD from their 32-bit counterparts. +#if defined(__i386) || defined(__i386__) || defined(_M_IX86) +#define JEBP__ARCH_X86 +#if defined(__SSE2__) || _M_IX86_FP == 2 +#define JEBP__SIMD_SSE2 +#endif +#elif defined(__x86_64) || defined(__x86_64__) || defined(_M_X64) +#define JEBP__ARCH_X86 +#define JEBP__SIMD_SSE2 +#elif defined(__arm) || defined(__arm__) || defined(_M_ARM) +#define JEBP__ARCH_ARM +#if defined(__ARM_NEON) || defined(_MSC_VER) +// According to the following article, MSVC requires Neon support +// https://docs.microsoft.com/en-us/cpp/build/overview-of-arm-abi-conventions +#define JEBP__SIMD_NEON +#endif +#elif defined(__aarch64) || defined(__aarch64__) || defined(_M_ARM64) +#define JEBP__ARCH_ARM +#define JEBP__SIMD_NEON +#define JEBP__SIMD_NEON64 +#endif + +#if defined(JEBP__ARCH_X86) +// x86 is always little-endian +#define JEBP__LITTLE_ENDIAN +#elif defined(JEBP__ARCH_ARM) && defined(__ARM_BIG_ENDIAN) +// The ACLE big-endian define overrules everything else, including the defualt +// endianness detection +#elif defined(JEBP__ARCH_ARM) && (defined(__ARM_ACLE) || defined(_MSC_VER)) +// If ACLE is supported and big-endian is not defined, it must be little-endian +// According to the article linked above, MSVC only supports little-endian +#define JEBP__LITTLE_ENDIAN +#elif defined(__LITTLE_ENDIAN__) || __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define JEBP__LITTLE_ENDIAN +#endif + +#ifdef JEBP_NO_SIMD +#undef JEBP__SIMD_SSE2 +#undef JEBP__SIMD_NEON +#endif // JEBP_NO_SIMD +#ifdef JEBP__SIMD_SSE2 +#include <emmintrin.h> +#endif // JEBP__SIMD_SSE2 +#ifdef JEBP__SIMD_NEON +#include <arm_neon.h> +#endif // JEBP__SIMD_NEON + +/** + * Common utilities + */ +// TODO: Maybe we should have a logging flag and add custom logs with more +// information to each error (and maybe other stuff like allocations) +#define JEBP__MIN(a, b) ((a) < (b) ? (a) : (b)) +#define JEBP__MAX(a, b) ((a) > (b) ? (a) : (b)) +#define JEBP__ABS(a) ((a) < 0 ? -(a) : (a)) +#define JEBP__AVG(a, b) (((a) + (b)) / 2) +#define JEBP__CEIL_SHIFT(a, b) (((a) + (1 << (b)) - 1) >> (b)) +#define JEBP__CLAMP(x, min, max) JEBP__MIN(JEBP__MAX(x, min), max) +#define JEBP__CLAMP_UBYTE(x) JEBP__CLAMP(x, 0, 255) +#define JEBP__CLEAR(ptr, size) memset(ptr, 0, size) + +// A simple utility that updates an error pointer if it currently does not have +// an error +JEBP__INLINE jebp_error_t jebp__error(jebp_error_t *err, jebp_error_t error) { + if (*err == JEBP_OK) { + *err = error; + } + return *err; +} + +// Currently only used by VP8L +// TODO: after VP8(no-L) support is added, make it an error to remove both +// VP8 and VP8L +#ifndef JEBP_NO_VP8L +static jebp_error_t jebp__alloc_image(jebp_image_t *image) { + image->pixels = + JEBP_ALLOC(image->width * image->height * sizeof(jebp_color_t)); + if (image->pixels == NULL) { + return JEBP_ERROR_NOMEM; + } + return JEBP_OK; +} +#endif // JEBP_NO_VP8L + +/** + * Reader abstraction + */ +#define JEBP__BUFFER_SIZE 4096 + +typedef struct jebp__reader_t { + size_t nb_bytes; + const jebp_ubyte *bytes; +#ifndef JEBP_NO_STDIO + FILE *file; + void *buffer; +#endif // JEBP_NO_STDIO +} jebp__reader_t; + +static void jebp__init_memory(jebp__reader_t *reader, size_t size, + const void *data) { + reader->nb_bytes = size; + reader->bytes = data; +#ifndef JEBP_NO_STDIO + reader->file = NULL; +#endif // JEBP_NO_STDIO +} + +#ifndef JEBP_NO_STDIO +static jebp_error_t jebp__open_file(jebp__reader_t *reader, const char *path) { + reader->nb_bytes = 0; + reader->file = fopen(path, "rb"); + if (reader->file == NULL) { + return JEBP_ERROR_IO; + } + reader->buffer = JEBP_ALLOC(JEBP__BUFFER_SIZE); + if (reader->buffer == NULL) { + fclose(reader->file); + return JEBP_ERROR_NOMEM; + } + return JEBP_OK; +} + +static void jebp__close_file(jebp__reader_t *reader) { + JEBP_FREE(reader->buffer); + fclose(reader->file); +} +#endif // JEBP_NO_STDIO + +static jebp_error_t jebp__buffer_bytes(jebp__reader_t *reader) { + if (reader->nb_bytes > 0) { + return JEBP_OK; + } +#ifndef JEBP_NO_STDIO + if (reader->file != NULL) { + reader->nb_bytes = + fread(reader->buffer, 1, JEBP__BUFFER_SIZE, reader->file); + reader->bytes = reader->buffer; + if (ferror(reader->file)) { + return JEBP_ERROR_IO; + } + } +#endif // JEBP_NO_STDIO + if (reader->nb_bytes == 0) { + return JEBP_ERROR_EOF; + } + return JEBP_OK; +} + +// TODO: Most reads are only a few bytes so maybe I should optimize for that +static jebp_error_t jebp__read_bytes(jebp__reader_t *reader, size_t size, + void *data) { + jebp_error_t err; + jebp_ubyte *bytes = data; + while (size > 0) { + if ((err = jebp__buffer_bytes(reader)) != JEBP_OK) { + return err; + } + size_t nb_bytes = JEBP__MIN(size, reader->nb_bytes); + if (bytes != NULL) { + memcpy(bytes, reader->bytes, nb_bytes); + bytes += nb_bytes; + } + size -= nb_bytes; + reader->nb_bytes -= nb_bytes; + reader->bytes += nb_bytes; + } + return JEBP_OK; +} + +// 8-bit uint reading is currently only used by the bit-reader +#ifndef JEBP_NO_VP8L +static jebp_ubyte jebp__read_uint8(jebp__reader_t *reader, jebp_error_t *err) { + if (*err != JEBP_OK) { + return 0; + } + if ((*err = jebp__buffer_bytes(reader)) != JEBP_OK) { + return 0; + } + reader->nb_bytes -= 1; + return *(reader->bytes++); +} +#endif // JEBP_NO_VP8L + +static jebp_uint jebp__read_uint32(jebp__reader_t *reader, jebp_error_t *err) { + if (*err != JEBP_OK) { + return 0; + } +#ifdef JEBP__LITTLE_ENDIAN + jebp_uint value = 0; + *err = jebp__read_bytes(reader, 4, &value); + return value; +#else // JEBP__LITTLE_ENDIAN + jebp_ubyte bytes[4]; + *err = jebp__read_bytes(reader, 4, bytes); + return (jebp_uint)bytes[0] | ((jebp_uint)bytes[1] << 8) | + ((jebp_uint)bytes[2] << 16) | ((jebp_uint)bytes[3] << 24); +#endif // JEBP__LITTLE_ENDIAN +} + +/** + * RIFF container + */ +#define JEBP__RIFF_TAG 0x46464952 +#define JEBP__WEBP_TAG 0x50424557 + +typedef struct jebp__chunk_t { + jebp_uint tag; + jebp_uint size; +} jebp__chunk_t; + +typedef struct jebp__riff_reader_t { + jebp__reader_t *reader; + jebp__chunk_t header; +} jebp__riff_reader_t; + +static jebp_error_t jebp__read_chunk(jebp__riff_reader_t *riff, + jebp__chunk_t *chunk) { + jebp_error_t err = JEBP_OK; + chunk->tag = jebp__read_uint32(riff->reader, &err); + chunk->size = jebp__read_uint32(riff->reader, &err); + chunk->size += chunk->size % 2; // round up to even + return err; +} + +static jebp_error_t jebp__read_riff_header(jebp__riff_reader_t *riff, + jebp__reader_t *reader) { + jebp_error_t err; + riff->reader = reader; + if ((err = jebp__read_chunk(riff, &riff->header)) != JEBP_OK) { + return err; + } + if (riff->header.tag != JEBP__RIFF_TAG) { + return JEBP_ERROR_INVDATA_HEADER; + } + if (jebp__read_uint32(reader, &err) != JEBP__WEBP_TAG) { + return jebp__error(&err, JEBP_ERROR_INVDATA_HEADER); + } + return err; +} + +static jebp_error_t jebp__read_riff_chunk(jebp__riff_reader_t *riff, + jebp__chunk_t *chunk) { + jebp_error_t err; + if ((err = jebp__read_chunk(riff, chunk)) != JEBP_OK) { + return err; + } + if (chunk->size > riff->header.size) { + return JEBP_ERROR_INVDATA; + } + riff->header.size -= chunk->size; + return JEBP_OK; +} + +/** + * Bit reader + */ +#ifndef JEBP_NO_VP8L +typedef struct jebp__bit_reader_t { + jebp__reader_t *reader; + size_t nb_bytes; + jebp_int nb_bits; + jebp_uint bits; +} jebp__bit_reader_t; + +static void jepb__init_bit_reader(jebp__bit_reader_t *bits, + jebp__reader_t *reader, size_t size) { + bits->reader = reader; + bits->nb_bytes = size; + bits->nb_bits = 0; + bits->bits = 0; +} + +// buffer/peek/skip should be used together to optimize bit-reading +static jebp_error_t jebp__buffer_bits(jebp__bit_reader_t *bits, jebp_int size) { + jebp_error_t err = JEBP_OK; + while (bits->nb_bits < size && bits->nb_bytes > 0) { + bits->bits |= jebp__read_uint8(bits->reader, &err) << bits->nb_bits; + bits->nb_bits += 8; + bits->nb_bytes -= 1; + } + return err; +} + +JEBP__INLINE jebp_int jepb__peek_bits(jebp__bit_reader_t *bits, jebp_int size) { + return bits->bits & ((1 << size) - 1); +} + +JEBP__INLINE jebp_error_t jebp__skip_bits(jebp__bit_reader_t *bits, + jebp_int size) { + if (size > bits->nb_bits) { + return JEBP_ERROR_INVDATA; + } + bits->nb_bits -= size; + bits->bits >>= size; + return JEBP_OK; +} + +static jebp_uint jebp__read_bits(jebp__bit_reader_t *bits, jebp_int size, + jebp_error_t *err) { + if (*err != JEBP_OK) { + return 0; + } + if ((*err = jebp__buffer_bits(bits, size)) != JEBP_OK) { + return 0; + } + jebp_uint value = jepb__peek_bits(bits, size); + if ((*err = jebp__skip_bits(bits, size)) != JEBP_OK) { + return 0; + } + return value; +} + +/** + * Huffman coding + */ +#define JEBP__MAX_HUFFMAN_LENGTH 15 +#define JEBP__MAX_PRIMARY_LENGTH 8 +#define JEBP__MAX_SECONDARY_LENGTH \ + (JEBP__MAX_HUFFMAN_LENGTH - JEBP__MAX_PRIMARY_LENGTH) +#define JEBP__NB_PRIMARY_HUFFMANS (1 << JEBP__MAX_PRIMARY_LENGTH) +#define JEBP__NO_HUFFMAN_SYMBOL 0xffff + +#define JEBP__NB_META_SYMBOLS 19 +#define JEBP__NB_COLOR_SYMBOLS 256 +#define JEBP__NB_LENGTH_SYMBOLS 24 +#define JEBP__NB_DIST_SYMBOLS 40 +#define JEBP__NB_MAIN_SYMBOLS (JEBP__NB_COLOR_SYMBOLS + JEBP__NB_LENGTH_SYMBOLS) + +// The huffman decoding is done in one or two steps, both using a lookup table. +// These tables are called the "primary" table and "secondary" tables. First +// 8-bits are peeked from the stream to index the primary table. If the symbol +// is in this table (indicated by length <= 8) then the symbol from that is used +// and the length is used to skip that many bits. Codes which are smaller than +// 8-bits are represented by filling the table such that any index with a prefix +// of the given code will have the same entry. If the symbol requires more bits +// (indiciated by length > 8) then the symbol is used as an offset pointing to +// the secondary table which has an index size of (length - 8) bits. +typedef struct jebp__huffman_t { + // <= 8: length is the number of bits actually used, and symbol is the + // decoded symbol or `JEBP__NO_HUFFMAN_SYMBOL` for an invalid code. + // > 8: length is the maximum number of bits for any code with this prefix, + // and symbol is the offset in the array to the secondary table. + jebp_short length; + jebp_ushort symbol; +} jebp__huffman_t; + +typedef struct jebp__huffman_group_t { + jebp__huffman_t *main; + jebp__huffman_t *red; + jebp__huffman_t *blue; + jebp__huffman_t *alpha; + jebp__huffman_t *dist; +} jebp__huffman_group_t; + +static const jebp_byte jebp__meta_length_order[JEBP__NB_META_SYMBOLS]; + +// Reverse increment, returns truthy on overflow +JEBP__INLINE jebp_int jebp__increment_code(jebp_int *code, jebp_int length) { + jebp_int inc = 1 << (length - 1); + while (*code & inc) { + inc >>= 1; + } + if (inc == 0) { + return 1; + } + *code = (*code & (inc - 1)) + inc; + return 0; +} + +// This function is a bit confusing so I have attempted to document it well +static jebp_error_t jebp__alloc_huffman(jebp__huffman_t **huffmans, + jebp_int nb_lengths, + const jebp_byte *lengths) { + // Stack allocate the primary table and set it all to invalid values + jebp__huffman_t primary[JEBP__NB_PRIMARY_HUFFMANS]; + for (jebp_int i = 0; i < JEBP__NB_PRIMARY_HUFFMANS; i += 1) { + primary[i].symbol = JEBP__NO_HUFFMAN_SYMBOL; + } + + // Fill in the 8-bit codes in the primary table + jebp_int len = 1; + jebp_int code = 0; + jebp_int overflow = 0; + jebp_ushort symbol = JEBP__NO_HUFFMAN_SYMBOL; + jebp_int nb_symbols = 0; + for (; len <= JEBP__MAX_PRIMARY_LENGTH; len += 1) { + for (jebp_int i = 0; i < nb_lengths; i += 1) { + if (lengths[i] != len) { + continue; + } + if (overflow) { + // Fail now if the last increment overflowed + return JEBP_ERROR_INVDATA; + } + for (jebp_int c = code; c < JEBP__NB_PRIMARY_HUFFMANS; + c += 1 << len) { + primary[c].length = len; + primary[c].symbol = i; + } + overflow = jebp__increment_code(&code, len); + symbol = i; + nb_symbols += 1; + } + } + + // Fill in the secondary table lengths in the primary table + jebp_int secondary_code = code; + for (; len <= JEBP__MAX_HUFFMAN_LENGTH; len += 1) { + for (jebp_int i = 0; i < nb_lengths; i += 1) { + if (lengths[i] != len) { + continue; + } + if (overflow) { + return JEBP_ERROR_INVDATA; + } + jebp_int prefix = code & (JEBP__NB_PRIMARY_HUFFMANS - 1); + primary[prefix].length = len; + overflow = jebp__increment_code(&code, len); + symbol = i; + nb_symbols += 1; + } + } + + // Calculate the total no. of huffman entries and fill in the secondary + // table offsets + jebp_int nb_huffmans = JEBP__NB_PRIMARY_HUFFMANS; + for (jebp_int i = 0; i < JEBP__NB_PRIMARY_HUFFMANS; i += 1) { + if (nb_symbols <= 1) { + // Special case: if there is only one symbol, use this iteration to + // instead fill the primary table with 0-length + // entries + primary[i].length = 0; + primary[i].symbol = symbol; + continue; + } + jebp_int suffix_length = primary[i].length - JEBP__MAX_PRIMARY_LENGTH; + if (suffix_length > 0) { + primary[i].symbol = nb_huffmans; + nb_huffmans += 1 << suffix_length; + } + } + + // Allocate, copy over the primary table, and assign the rest to invalid + // values + *huffmans = JEBP_ALLOC(nb_huffmans * sizeof(jebp__huffman_t)); + if (*huffmans == NULL) { + return JEBP_ERROR_NOMEM; + } + memcpy(*huffmans, primary, sizeof(primary)); + if (nb_huffmans == JEBP__NB_PRIMARY_HUFFMANS) { + // Special case: we can stop here if we don't have to fill any secondary + // tables + return JEBP_OK; + } + for (jebp_int i = JEBP__NB_PRIMARY_HUFFMANS; i < nb_huffmans; i += 1) { + (*huffmans)[i].symbol = JEBP__NO_HUFFMAN_SYMBOL; + } + + // Fill in the secondary tables + len = JEBP__MAX_PRIMARY_LENGTH + 1; + code = secondary_code; + for (; len <= JEBP__MAX_HUFFMAN_LENGTH; len += 1) { + for (jebp_int i = 0; i < nb_lengths; i += 1) { + if (lengths[i] != len) { + continue; + } + jebp_int prefix = code & (JEBP__NB_PRIMARY_HUFFMANS - 1); + jebp_int nb_secondary_huffmans = 1 << primary[prefix].length; + jebp__huffman_t *secondary = *huffmans + primary[prefix].symbol; + for (jebp_int c = code; c < nb_secondary_huffmans; c += 1 << len) { + secondary[c >> JEBP__MAX_PRIMARY_LENGTH].length = len; + secondary[c >> JEBP__MAX_PRIMARY_LENGTH].symbol = i; + } + jebp__increment_code(&code, len); + } + } + return JEBP_OK; +} + +static jebp_int jebp__read_symbol(jebp__huffman_t *huffmans, + jebp__bit_reader_t *bits, jebp_error_t *err) { + if (*err != JEBP_OK) { + return 0; + } + if ((*err = jebp__buffer_bits(bits, JEBP__MAX_HUFFMAN_LENGTH)) != JEBP_OK) { + return 0; + } + jebp_int code = jepb__peek_bits(bits, JEBP__MAX_PRIMARY_LENGTH); + if (huffmans[code].symbol == JEBP__NO_HUFFMAN_SYMBOL) { + *err = JEBP_ERROR_INVDATA; + return 0; + } + jebp_int length = huffmans[code].length; + jebp_int skip = JEBP__MIN(length, JEBP__MAX_PRIMARY_LENGTH); + if ((*err = jebp__skip_bits(bits, skip)) != JEBP_OK) { + return 0; + } + if (skip == length) { + return huffmans[code].symbol; + } + + huffmans += huffmans[code].symbol; + code = jepb__peek_bits(bits, length - skip); + if (huffmans[code].symbol == JEBP__NO_HUFFMAN_SYMBOL) { + *err = JEBP_ERROR_INVDATA; + return 0; + } + if ((*err = jebp__skip_bits(bits, huffmans[code].length - skip)) != + JEBP_OK) { + return 0; + } + return huffmans[code].symbol; +} + +static jebp_error_t jebp__read_huffman(jebp__huffman_t **huffmans, + jebp__bit_reader_t *bits, + jebp_int nb_lengths, + jebp_byte *lengths) { + // This part of the spec is INCREDIBLY wrong and partly missing + jebp_error_t err = JEBP_OK; + JEBP__CLEAR(lengths, nb_lengths); + + if (jebp__read_bits(bits, 1, &err)) { + // simple length storage with only 1 (first) or 2 (second) symbols, both + // with a length of 1 + jebp_int has_second = jebp__read_bits(bits, 1, &err); + jebp_int first_bits = jebp__read_bits(bits, 1, &err) ? 8 : 1; + jebp_int first = jebp__read_bits(bits, first_bits, &err); + if (first >= nb_lengths) { + return jebp__error(&err, JEBP_ERROR_INVDATA); + } + lengths[first] = 1; + if (has_second) { + jebp_int second = jebp__read_bits(bits, 8, &err); + if (second >= nb_lengths) { + return jebp__error(&err, JEBP_ERROR_INVDATA); + } + lengths[second] = 1; + } + + } else { + jebp_byte meta_lengths[JEBP__NB_META_SYMBOLS] = {0}; + jebp_int nb_meta_lengths = jebp__read_bits(bits, 4, &err) + 4; + for (jebp_int i = 0; i < nb_meta_lengths; i += 1) { + meta_lengths[jebp__meta_length_order[i]] = + jebp__read_bits(bits, 3, &err); + } + if (err != JEBP_OK) { + return err; + } + jebp__huffman_t *meta_huffmans; + if ((err = jebp__alloc_huffman(&meta_huffmans, JEBP__NB_META_SYMBOLS, + meta_lengths)) != JEBP_OK) { + return err; + } + + jebp_int nb_meta_symbols = nb_lengths; + if (jebp__read_bits(bits, 1, &err)) { + // limit codes + jebp_int symbols_bits = jebp__read_bits(bits, 3, &err) * 2 + 2; + nb_meta_symbols = jebp__read_bits(bits, symbols_bits, &err) + 2; + } + + jebp_int prev_length = 8; + for (jebp_int i = 0; i < nb_lengths && nb_meta_symbols > 0; + nb_meta_symbols -= 1) { + jebp_int symbol = jebp__read_symbol(meta_huffmans, bits, &err); + jebp_int length; + jebp_int repeat; + switch (symbol) { + case 16: + length = prev_length; + repeat = jebp__read_bits(bits, 2, &err) + 3; + break; + case 17: + length = 0; + repeat = jebp__read_bits(bits, 3, &err) + 3; + break; + case 18: + length = 0; + repeat = jebp__read_bits(bits, 7, &err) + 11; + break; + default: + prev_length = symbol; + /* fallthrough */ + case 0: + // We don't ever repeat 0 values. + lengths[i++] = symbol; + continue; + } + if (i + repeat > nb_lengths) { + jebp__error(&err, JEBP_ERROR_INVDATA); + break; + } + for (jebp_int j = 0; j < repeat; j += 1) { + lengths[i++] = length; + } + } + JEBP_FREE(meta_huffmans); + } + + if (err != JEBP_OK) { + return err; + } + return jebp__alloc_huffman(huffmans, nb_lengths, lengths); +} + +static jebp_error_t jebp__read_huffman_group(jebp__huffman_group_t *group, + jebp__bit_reader_t *bits, + jebp_int nb_main_symbols, + jebp_byte *lengths) { + jebp_error_t err; + if ((err = jebp__read_huffman(&group->main, bits, nb_main_symbols, + lengths)) != JEBP_OK) { + return err; + } + if ((err = jebp__read_huffman(&group->red, bits, JEBP__NB_COLOR_SYMBOLS, + lengths)) != JEBP_OK) { + return err; + } + if ((err = jebp__read_huffman(&group->blue, bits, JEBP__NB_COLOR_SYMBOLS, + lengths)) != JEBP_OK) { + return err; + } + if ((err = jebp__read_huffman(&group->alpha, bits, JEBP__NB_COLOR_SYMBOLS, + lengths)) != JEBP_OK) { + return err; + } + if ((err = jebp__read_huffman(&group->dist, bits, JEBP__NB_DIST_SYMBOLS, + lengths)) != JEBP_OK) { + return err; + } + return JEBP_OK; +} + +static void jebp__free_huffman_group(jebp__huffman_group_t *group) { + JEBP_FREE(group->main); + JEBP_FREE(group->red); + JEBP_FREE(group->blue); + JEBP_FREE(group->alpha); + JEBP_FREE(group->dist); +} + +/** + * Color cache + */ +typedef struct jebp__colcache_t { + jebp_int bits; + jebp_color_t *colors; +} jebp__colcache_t; + +static jebp_error_t jebp__read_colcache(jebp__colcache_t *colcache, + jebp__bit_reader_t *bits) { + jebp_error_t err = JEBP_OK; + if (!jebp__read_bits(bits, 1, &err)) { + // no color cache + colcache->bits = 0; + return err; + } + colcache->bits = jebp__read_bits(bits, 4, &err); + if (err != JEBP_OK || colcache->bits < 1 || colcache->bits > 11) { + return jebp__error(&err, JEBP_ERROR_INVDATA); + } + + size_t colcache_size = ((size_t)1 << colcache->bits) * sizeof(jebp_color_t); + colcache->colors = JEBP_ALLOC(colcache_size); + if (colcache->colors == NULL) { + return JEBP_ERROR_NOMEM; + } + JEBP__CLEAR(colcache->colors, colcache_size); + return JEBP_OK; +} + +static void jebp__free_colcache(jebp__colcache_t *colcache) { + if (colcache->bits > 0) { + JEBP_FREE(colcache->colors); + } +} + +static void jebp__colcache_insert(jebp__colcache_t *colcache, + jebp_color_t *color) { + if (colcache->bits == 0) { + return; + } +#if defined(JEBP__LITTLE_ENDIAN) && defined(JEBP__SWAP32) + jebp_uint hash = *(jebp_uint *)color; // ABGR due to little-endian + hash = JEBP__SWAP32(hash); // RGBA + hash = (hash >> 8) | (hash << 24); // ARGB +#else + jebp_uint hash = ((jebp_uint)color->a << 24) | ((jebp_uint)color->r << 16) | + ((jebp_uint)color->g << 8) | (jebp_uint)color->b; +#endif + hash = (0x1e35a7bd * hash) >> (32 - colcache->bits); + colcache->colors[hash] = *color; +} + +/** + * VP8L image + */ +#define JEBP__NB_VP8L_OFFSETS 120 + +typedef struct jebp__subimage_t { + jebp_int width; + jebp_int height; + jebp_color_t *pixels; + jebp_int block_bits; +} jebp__subimage_t; + +static const jebp_byte jebp__vp8l_offsets[JEBP__NB_VP8L_OFFSETS][2]; + +JEBP__INLINE jebp_int jebp__read_vp8l_extrabits(jebp__bit_reader_t *bits, + jebp_int symbol, + jebp_error_t *err) { + if (*err != JEBP_OK) { + return 1; + } + if (symbol < 4) { + return symbol + 1; + } + jebp_int extrabits = symbol / 2 - 1; + symbol = ((symbol % 2 + 2) << extrabits) + 1; + return symbol + jebp__read_bits(bits, extrabits, err); +} + +static jebp_error_t jebp__read_vp8l_image(jebp_image_t *image, + jebp__bit_reader_t *bits, + jebp__colcache_t *colcache, + jebp__subimage_t *huffman_image) { + jebp_error_t err; + jebp_int nb_groups = 1; + jebp__huffman_group_t *groups = &(jebp__huffman_group_t){0}; + if (huffman_image != NULL) { + for (jebp_int i = 0; i < huffman_image->width * huffman_image->height; + i += 1) { + jebp_color_t *huffman = &huffman_image->pixels[i]; + if (huffman->r != 0) { + // Currently only 256 huffman groups are supported + return JEBP_ERROR_NOSUP; + } + nb_groups = JEBP__MAX(nb_groups, huffman->g + 1); + huffman += 1; + } + if (nb_groups > 1) { + groups = JEBP_ALLOC(nb_groups * sizeof(jebp__huffman_group_t)); + if (groups == NULL) { + return JEBP_ERROR_NOMEM; + } + } + } + + jebp_int nb_main_symbols = JEBP__NB_MAIN_SYMBOLS; + if (colcache->bits > 0) { + nb_main_symbols += 1 << colcache->bits; + } + jebp_byte *lengths = JEBP_ALLOC(nb_main_symbols); + if (lengths == NULL) { + err = JEBP_ERROR_NOMEM; + goto free_groups; + } + jebp_int nb_read_groups = 0; + for (; nb_read_groups < nb_groups; nb_read_groups += 1) { + if ((err = jebp__read_huffman_group(&groups[nb_read_groups], bits, + nb_main_symbols, lengths)) != + JEBP_OK) { + break; + } + } + JEBP_FREE(lengths); + if (err != JEBP_OK) { + goto free_read_groups; + } + if ((err = jebp__alloc_image(image)) != JEBP_OK) { + goto free_read_groups; + } + + jebp_color_t *pixel = image->pixels; + jebp_color_t *end = pixel + image->width * image->height; + jebp_int x = 0; + for (jebp_int y = 0; y < image->height;) { + jebp_color_t *huffman_row = NULL; + if (huffman_image != NULL) { + huffman_row = + &huffman_image->pixels[(y >> huffman_image->block_bits) * + huffman_image->width]; + } + do { + jebp__huffman_group_t *group; + if (huffman_image == NULL) { + group = groups; + } else { + jebp_color_t *huffman = + &huffman_row[x >> huffman_image->block_bits]; + group = &groups[huffman->g]; + } + + jebp_int main = jebp__read_symbol(group->main, bits, &err); + if (main < JEBP__NB_COLOR_SYMBOLS) { + pixel->g = main; + pixel->r = jebp__read_symbol(group->red, bits, &err); + pixel->b = jebp__read_symbol(group->blue, bits, &err); + pixel->a = jebp__read_symbol(group->alpha, bits, &err); + jebp__colcache_insert(colcache, pixel++); + x += 1; + } else if (main >= JEBP__NB_MAIN_SYMBOLS) { + *(pixel++) = colcache->colors[main - JEBP__NB_MAIN_SYMBOLS]; + x += 1; + } else { + jebp_int length = jebp__read_vp8l_extrabits( + bits, main - JEBP__NB_COLOR_SYMBOLS, &err); + jebp_int dist = jebp__read_symbol(group->dist, bits, &err); + dist = jebp__read_vp8l_extrabits(bits, dist, &err); + if (dist > JEBP__NB_VP8L_OFFSETS) { + dist -= JEBP__NB_VP8L_OFFSETS; + } else { + const jebp_byte *offset = jebp__vp8l_offsets[dist - 1]; + dist = offset[1] * image->width + offset[0]; + dist = JEBP__MAX(dist, 1); + } + jebp_color_t *repeat = pixel - dist; + if (repeat < image->pixels || pixel + length > end) { + jebp__error(&err, JEBP_ERROR_INVDATA); + break; + } + for (jebp_int i = 0; i < length; i += 1) { + jebp__colcache_insert(colcache, repeat); + *(pixel++) = *(repeat++); + } + x += length; + } + } while (x < image->width); + y += x / image->width; + x %= image->width; + } + + if (err != JEBP_OK) { + jebp_free_image(image); + } +free_read_groups: + for (nb_read_groups -= 1; nb_read_groups >= 0; nb_read_groups -= 1) { + jebp__free_huffman_group(&groups[nb_read_groups]); + } +free_groups: + if (nb_groups > 1) { + JEBP_FREE(groups); + } + return err; +} + +static jebp_error_t jebp__read_subimage(jebp__subimage_t *subimage, + jebp__bit_reader_t *bits, + jebp_image_t *image) { + jebp_error_t err = JEBP_OK; + subimage->block_bits = jebp__read_bits(bits, 3, &err) + 2; + subimage->width = JEBP__CEIL_SHIFT(image->width, subimage->block_bits); + subimage->height = JEBP__CEIL_SHIFT(image->height, subimage->block_bits); + if (err != JEBP_OK) { + return err; + } + jebp__colcache_t colcache; + if ((err = jebp__read_colcache(&colcache, bits)) != JEBP_OK) { + return err; + } + err = + jebp__read_vp8l_image((jebp_image_t *)subimage, bits, &colcache, NULL); + jebp__free_colcache(&colcache); + return err; +} + +/** + * VP8L predictions + */ +#define JEBP__NB_VP8L_PRED_TYPES 14 + +// I don't like the way it formats this +// clang-format off +#define JEBP__UNROLL4(var, body) \ + { var = 0; body } \ + { var = 1; body } \ + { var = 2; body } \ + { var = 3; body } +// clang-format on + +typedef void (*jebp__vp8l_pred_t)(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width); + +#ifdef JEBP__SIMD_SSE2 +typedef struct jebp__m128x4i { + __m128i v[4]; +} jebp__m128x4i; + +JEBP__INLINE __m128i jebp__sse_move_px1(__m128i v_dst, __m128i v_src) { + __m128 v_dstf = _mm_castsi128_ps(v_dst); + __m128 v_srcf = _mm_castsi128_ps(v_src); + __m128 v_movf = _mm_move_ss(v_dstf, v_srcf); + return _mm_castps_si128(v_movf); +} + +JEBP__INLINE __m128i jebp__sse_avg_u8x16(__m128i v1, __m128i v2) { + __m128i v_one = _mm_set1_epi8(1); + __m128i v_avg = _mm_avg_epu8(v1, v2); + // SSE2 `avg` rounds up, we have to check if a round-up occured (one of the + // low bits was set but the other wasn't) and subtract 1 if so + __m128i v_err = _mm_xor_si128(v1, v2); + v_err = _mm_and_si128(v_err, v_one); + return _mm_sub_epi8(v_avg, v_err); +} + +JEBP__INLINE __m128i jebp__sse_avg2_u8x16(__m128i v1, __m128i v2, __m128i v3) { + __m128i v_one = _mm_set1_epi8(1); + // We can further optimise two avg calls but noting that the error will + // propogate + __m128i v_avg1 = _mm_avg_epu8(v1, v2); + __m128i v_err1 = _mm_xor_si128(v1, v2); + __m128i v_avg2 = _mm_avg_epu8(v_avg1, v3); + __m128i v_err2 = _mm_xor_si128(v_avg1, v3); + v_err2 = _mm_or_si128(v_err1, v_err2); + v_err2 = _mm_and_si128(v_err2, v_one); + return _mm_sub_epi8(v_avg2, v_err2); +} + +JEBP__INLINE __m128i jebp__sse_flatten_px4(jebp__m128x4i v_pixel4) { + __m128i v_pixello = jebp__sse_move_px1(v_pixel4.v[1], v_pixel4.v[0]); + __m128i v_pixel3 = _mm_bsrli_si128(v_pixel4.v[3], 4); + __m128i v_pixelhi = _mm_unpackhi_epi32(v_pixel4.v[2], v_pixel3); + return _mm_unpacklo_epi64(v_pixello, v_pixelhi); +} + +// Bit-select and accumulate, used by prediction filters 11-13 +JEBP__INLINE __m128i jebp__sse_bsela_u8x16(__m128i v_acc, __m128i v_mask, + __m128i v1, __m128i v0) { + // This is faster than using and/andnot/or since SSE only supports two + // operands so prefers chaining outputs + __m128i v_sel = _mm_xor_si128(v0, v1); + v_sel = _mm_and_si128(v_sel, v_mask); + v_sel = _mm_xor_si128(v_sel, v0); + return _mm_add_epi8(v_acc, v_sel); +} +#endif // JEBP__SIMD_SSE2 + +#ifdef JEBP__SIMD_NEON +JEBP__INLINE uint8x16_t jebp__neon_load_px1(jebp_color_t *pixel) { + uint8x16_t v_pixel = vreinterpretq_u8_u32(vld1q_dup_u32((uint32_t *)pixel)); +#ifndef JEBP__LITTLE_ENDIAN + v_pixel = vrev32q_u8(v_pixel); +#endif // JEBP__LITTLE_ENDIAN + return v_pixel; +} + +JEBP__INLINE uint8x16_t jebp__neon_flatten_px4(uint8x16x4_t v_pixel4) { +#ifdef JEBP__SIMD_NEON64 + uint8x16_t v_table = vcombine_u8(vcreate_u8(0x1716151403020100), + vcreate_u8(0x3f3e3d3c2b2a2928)); + return vqtbl4q_u8(v_pixel4, v_table); +#else // JEBP__SIMD_NEON64 + uint8x16_t v_mask1 = + vcombine_u8(vcreate_u8((uint32_t)-1), vcreate_u8((uint32_t)-1)); + uint8x16_t v_mask2 = vcombine_u8(vcreate_u8((uint64_t)-1), vcreate_u8(0)); + uint8x16_t v_pixello = vbslq_u8(v_mask1, v_pixel4.val[0], v_pixel4.val[1]); + uint8x16_t v_pixelhi = vbslq_u8(v_mask1, v_pixel4.val[2], v_pixel4.val[3]); + return vbslq_u8(v_mask2, v_pixello, v_pixelhi); +#endif // JEBP__SIMD_NEON64 +} + +JEBP__INLINE uint32x4_t jebp__neon_sad_px4(uint8x16_t v_pix1, + uint8x16_t v_pix2) { + uint8x16_t v_diff8 = vabdq_u8(v_pix1, v_pix2); + uint16x8_t v_diff16 = vpaddlq_u8(v_diff8); + return vpaddlq_u16(v_diff16); +} +#endif // JEBP__SIMD_NEON + +JEBP__INLINE void jebp__vp8l_pred_black(jebp_color_t *pixel, jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + __m128i v_black = _mm_set1_epi32((int)0xff000000); + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + v_pixel = _mm_add_epi8(v_pixel, v_black); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + } +#elif defined(JEBP__SIMD_NEON) + uint8x8_t v_black = vdup_n_u8(0xff); + for (; x + 8 <= width; x += 8) { + uint8x8x4_t v_pixel = vld4_u8((uint8_t *)&pixel[x]); + v_pixel.val[3] = vadd_u8(v_pixel.val[3], v_black); + vst4_u8((uint8_t *)&pixel[x], v_pixel); + } +#endif + for (; x < width; x += 1) { + pixel[x].a += 0xff; + } +} + +static void jebp__vp8l_pred0(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + (void)top; + jebp__vp8l_pred_black(pixel, width); +} + +JEBP__INLINE void jebp__vp8l_pred_left(jebp_color_t *pixel, jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + __m128i v_left; + if (width >= 4) { + v_left = _mm_cvtsi32_si128(*(int *)&pixel[-1]); + } + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + v_pixel = _mm_add_epi8(v_pixel, v_left); + v_left = _mm_bslli_si128(v_pixel, 4); + v_pixel = _mm_add_epi8(v_pixel, v_left); + v_left = _mm_bslli_si128(v_pixel, 8); + v_pixel = _mm_add_epi8(v_pixel, v_left); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + v_left = _mm_bsrli_si128(v_pixel, 12); + } +#elif defined(JEBP__SIMD_NEON) + uint8x16_t v_zero = vdupq_n_u8(0); + uint8x16_t v_left; + if (width >= 4) { + v_left = jebp__neon_load_px1(&pixel[-1]); + v_left = vextq_u8(v_left, v_zero, 12); + } + for (; x + 4 <= width; x += 4) { + uint8x16_t v_pixel = vld1q_u8((uint8_t *)&pixel[x]); + v_pixel = vaddq_u8(v_pixel, v_left); + v_left = vextq_u8(v_zero, v_pixel, 12); + v_pixel = vaddq_u8(v_pixel, v_left); + v_left = vextq_u8(v_zero, v_pixel, 8); + v_pixel = vaddq_u8(v_pixel, v_left); + vst1q_u8((uint8_t *)&pixel[x], v_pixel); + v_left = vextq_u8(v_pixel, v_zero, 12); + } +#endif + for (; x < width; x += 1) { + pixel[x].r += pixel[x - 1].r; + pixel[x].g += pixel[x - 1].g; + pixel[x].b += pixel[x - 1].b; + pixel[x].a += pixel[x - 1].a; + } +} + +static void jebp__vp8l_pred1(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + (void)top; + jebp__vp8l_pred_left(pixel, width); +} + +JEBP__INLINE void jebp__vp8l_pred_top(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + __m128i v_top = _mm_loadu_si128((__m128i *)&top[x]); + v_pixel = _mm_add_epi8(v_pixel, v_top); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + } +#elif defined(JEBP__SIMD_NEON) + for (; x + 4 <= width; x += 4) { + uint8x16_t v_pixel = vld1q_u8((uint8_t *)&pixel[x]); + uint8x16_t v_top = vld1q_u8((uint8_t *)&top[x]); + v_pixel = vaddq_u8(v_pixel, v_top); + vst1q_u8((uint8_t *)&pixel[x], v_pixel); + } +#endif + for (; x < width; x += 1) { + pixel[x].r += top[x].r; + pixel[x].g += top[x].g; + pixel[x].b += top[x].b; + pixel[x].a += top[x].a; + } +} + +static void jebp__vp8l_pred2(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp__vp8l_pred_top(pixel, top, width); +} + +static void jebp__vp8l_pred3(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp__vp8l_pred_top(pixel, &top[1], width); +} + +static void jebp__vp8l_pred4(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp__vp8l_pred_top(pixel, &top[-1], width); +} + +static void jebp__vp8l_pred5(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + __m128i v_left; + __m128i v_top; + if (width >= 4) { + v_left = _mm_cvtsi32_si128(*(int *)&pixel[-1]); + v_top = _mm_loadu_si128((__m128i *)top); + } + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + __m128i v_next = _mm_loadu_si128((__m128i *)&top[x + 4]); + __m128i v_tr = jebp__sse_move_px1(v_top, v_next); + v_tr = _mm_shuffle_epi32(v_tr, _MM_SHUFFLE(0, 3, 2, 1)); + jebp__m128x4i v_pixel4; + JEBP__UNROLL4(jebp_int i, { + __m128i v_avg = jebp__sse_avg2_u8x16(v_left, v_tr, v_top); + v_pixel4.v[i] = _mm_add_epi8(v_pixel, v_avg); + v_left = _mm_shuffle_epi32(v_pixel4.v[i], _MM_SHUFFLE(2, 1, 0, 3)); + }) + v_pixel = jebp__sse_flatten_px4(v_pixel4); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + v_top = v_next; + } +#elif defined(JEBP__SIMD_NEON) + uint8x16_t v_left; + uint8x16_t v_top; + if (width >= 4) { + v_left = jebp__neon_load_px1(&pixel[-1]); + v_top = vld1q_u8((uint8_t *)top); + } + for (; x + 4 <= width; x += 4) { + uint8x16_t v_pixel = vld1q_u8((uint8_t *)&pixel[x]); + uint8x16_t v_next = vld1q_u8((uint8_t *)&top[x + 4]); + uint8x16_t v_tr = vextq_u8(v_top, v_next, 4); + uint8x16x4_t v_pixel4; + JEBP__UNROLL4(jebp_int i, { + uint8x16_t v_avg = vhaddq_u8(v_left, v_tr); + v_avg = vhaddq_u8(v_avg, v_top); + v_pixel4.val[i] = vaddq_u8(v_pixel, v_avg); + v_left = vextq_u8(v_pixel4.val[i], v_pixel4.val[i], 12); + }) + v_pixel = jebp__neon_flatten_px4(v_pixel4); + vst1q_u8((uint8_t *)&pixel[x], v_pixel); + v_top = v_next; + } +#endif + for (; x < width; x += 1) { + pixel[x].r += + JEBP__AVG(JEBP__AVG(pixel[x - 1].r, top[x + 1].r), top[x].r); + pixel[x].g += + JEBP__AVG(JEBP__AVG(pixel[x - 1].g, top[x + 1].g), top[x].g); + pixel[x].b += + JEBP__AVG(JEBP__AVG(pixel[x - 1].b, top[x + 1].b), top[x].b); + pixel[x].a += + JEBP__AVG(JEBP__AVG(pixel[x - 1].a, top[x + 1].a), top[x].a); + } +} + +JEBP__INLINE void jebp__vp8l_pred_avgtl(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + __m128i v_left; + if (width >= 4) { + v_left = _mm_cvtsi32_si128(*(int *)&pixel[-1]); + } + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + __m128i v_top = _mm_loadu_si128((__m128i *)&top[x]); + jebp__m128x4i v_pixel4; + JEBP__UNROLL4(jebp_int i, { + __m128i v_avg = jebp__sse_avg_u8x16(v_left, v_top); + v_pixel4.v[i] = _mm_add_epi8(v_pixel, v_avg); + v_left = _mm_shuffle_epi32(v_pixel4.v[i], _MM_SHUFFLE(2, 1, 0, 3)); + }) + v_pixel = jebp__sse_flatten_px4(v_pixel4); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + } +#elif defined(JEBP__SIMD_NEON) + uint8x16_t v_left; + if (width >= 4) { + v_left = jebp__neon_load_px1(&pixel[-1]); + } + for (; x + 4 <= width; x += 4) { + uint8x16_t v_pixel = vld1q_u8((uint8_t *)&pixel[x]); + uint8x16_t v_top = vld1q_u8((uint8_t *)&top[x]); + uint8x16x4_t v_pixel4; + JEBP__UNROLL4(jebp_int i, { + uint8x16_t v_avg = vhaddq_u8(v_left, v_top); + v_pixel4.val[i] = vaddq_u8(v_pixel, v_avg); + v_left = vextq_u8(v_pixel4.val[i], v_pixel4.val[i], 12); + }) + v_pixel = jebp__neon_flatten_px4(v_pixel4); + vst1q_u8((uint8_t *)&pixel[x], v_pixel); + } +#endif + for (; x < width; x += 1) { + pixel[x].r += JEBP__AVG(pixel[x - 1].r, top[x].r); + pixel[x].g += JEBP__AVG(pixel[x - 1].g, top[x].g); + pixel[x].b += JEBP__AVG(pixel[x - 1].b, top[x].b); + pixel[x].a += JEBP__AVG(pixel[x - 1].a, top[x].a); + } +} + +static void jebp__vp8l_pred6(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp__vp8l_pred_avgtl(pixel, &top[-1], width); +} + +static void jebp__vp8l_pred7(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp__vp8l_pred_avgtl(pixel, top, width); +} + +JEBP__INLINE void jebp__vp8l_pred_avgtr(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + __m128i v_top; + if (width >= 4) { + v_top = _mm_loadu_si128((__m128i *)top); + } + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + __m128i v_next = _mm_loadu_si128((__m128i *)&top[x + 4]); + __m128i v_tr = jebp__sse_move_px1(v_top, v_next); + v_tr = _mm_shuffle_epi32(v_tr, _MM_SHUFFLE(0, 3, 2, 1)); + v_tr = jebp__sse_avg_u8x16(v_top, v_tr); + v_pixel = _mm_add_epi8(v_pixel, v_tr); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + v_top = v_next; + } +#elif defined(JEBP__SIMD_NEON) + uint8x16_t v_top; + if (width >= 4) { + v_top = vld1q_u8((uint8_t *)top); + } + for (; x + 4 <= width; x += 4) { + uint8x16_t v_pixel = vld1q_u8((uint8_t *)&pixel[x]); + uint8x16_t v_next = vld1q_u8((uint8_t *)&top[x + 4]); + uint8x16_t v_tr = vextq_u8(v_top, v_next, 4); + v_tr = vhaddq_u8(v_top, v_tr); + v_pixel = vaddq_u8(v_pixel, v_tr); + vst1q_u8((uint8_t *)&pixel[x], v_pixel); + v_top = v_next; + } +#endif + for (; x < width; x += 1) { + pixel[x].r += JEBP__AVG(top[x].r, top[x + 1].r); + pixel[x].g += JEBP__AVG(top[x].g, top[x + 1].g); + pixel[x].b += JEBP__AVG(top[x].b, top[x + 1].b); + pixel[x].a += JEBP__AVG(top[x].a, top[x + 1].a); + } +} + +static void jebp__vp8l_pred8(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp__vp8l_pred_avgtr(pixel, &top[-1], width); +} + +static void jebp__vp8l_pred9(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp__vp8l_pred_avgtr(pixel, top, width); +} + +static void jebp__vp8l_pred10(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + __m128i v_left; + __m128i v_tl; + __m128i v_top; + if (width >= 4) { + v_left = _mm_cvtsi32_si128(*(int *)&pixel[-1]); + v_tl = _mm_cvtsi32_si128(*(int *)&top[-1]); + v_top = _mm_loadu_si128((__m128i *)top); + } + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + __m128i v_next = _mm_loadu_si128((__m128i *)&top[x + 4]); + __m128i v_rot = _mm_shuffle_epi32(v_top, _MM_SHUFFLE(2, 1, 0, 3)); + v_tl = jebp__sse_move_px1(v_rot, v_tl); + __m128i v_tr = jebp__sse_move_px1(v_top, v_next); + v_tr = _mm_shuffle_epi32(v_tr, _MM_SHUFFLE(0, 3, 2, 1)); + v_tr = jebp__sse_avg_u8x16(v_top, v_tr); + jebp__m128x4i v_pixel4; + JEBP__UNROLL4(jebp_int i, { + __m128i v_avg = jebp__sse_avg2_u8x16(v_left, v_tl, v_tr); + v_pixel4.v[i] = _mm_add_epi8(v_pixel, v_avg); + v_left = _mm_shuffle_epi32(v_pixel4.v[i], _MM_SHUFFLE(2, 1, 0, 3)); + }) + v_pixel = jebp__sse_flatten_px4(v_pixel4); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + v_tl = v_rot; + v_top = v_next; + } +#elif defined(JEBP__SIMD_NEON) + uint8x16_t v_left; + uint8x16_t v_tl; + uint8x16_t v_top; + if (width >= 4) { + v_left = jebp__neon_load_px1(&pixel[-1]); + v_tl = jebp__neon_load_px1(&top[-1]); + v_top = vld1q_u8((uint8_t *)top); + } + for (; x + 4 <= width; x += 4) { + uint8x16_t v_pixel = vld1q_u8((uint8_t *)&pixel[x]); + uint8x16_t v_next = vld1q_u8((uint8_t *)&top[x + 4]); + v_tl = vextq_u8(v_tl, v_top, 12); + uint8x16_t v_tr = vextq_u8(v_top, v_next, 4); + v_tr = vhaddq_u8(v_top, v_tr); + uint8x16x4_t v_pixel4; + JEBP__UNROLL4(jebp_int i, { + uint8x16_t v_avg = vhaddq_u8(v_left, v_tl); + v_avg = vhaddq_u8(v_avg, v_tr); + v_pixel4.val[i] = vaddq_u8(v_pixel, v_avg); + v_left = vextq_u8(v_pixel4.val[i], v_pixel4.val[i], 12); + }) + v_pixel = jebp__neon_flatten_px4(v_pixel4); + vst1q_u8((uint8_t *)&pixel[x], v_pixel); + v_tl = v_top; + v_top = v_next; + } +#endif + for (; x < width; x += 1) { + pixel[x].r += JEBP__AVG(JEBP__AVG(pixel[x - 1].r, top[x - 1].r), + JEBP__AVG(top[x].r, top[x + 1].r)); + pixel[x].g += JEBP__AVG(JEBP__AVG(pixel[x - 1].g, top[x - 1].g), + JEBP__AVG(top[x].g, top[x + 1].g)); + pixel[x].b += JEBP__AVG(JEBP__AVG(pixel[x - 1].b, top[x - 1].b), + JEBP__AVG(top[x].b, top[x + 1].b)); + pixel[x].a += JEBP__AVG(JEBP__AVG(pixel[x - 1].a, top[x - 1].a), + JEBP__AVG(top[x].a, top[x + 1].a)); + } +} + +JEBP__INLINE jebp_int jebp__vp8l_pred_dist(jebp_color_t *pix1, + jebp_color_t *pix2) { + return JEBP__ABS(pix1->r - pix2->r) + JEBP__ABS(pix1->g - pix2->g) + + JEBP__ABS(pix1->b - pix2->b) + JEBP__ABS(pix1->a - pix2->a); +} + +static void jebp__vp8l_pred11(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + __m128i v_left; + __m128i v_tl; + if (width >= 4) { + v_left = _mm_cvtsi32_si128(*(int *)&pixel[-1]); + v_tl = _mm_cvtsi32_si128(*(int *)&top[-1]); + } + for (; x + 4 <= width; x += 4) { + __m128i v_ldist, v_tdist, v_cmp, v_pixello, v_pixelhi; + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + __m128i v_top = _mm_loadu_si128((__m128i *)&top[x]); + __m128i v_rot = _mm_shuffle_epi32(v_top, _MM_SHUFFLE(2, 1, 0, 3)); + v_tl = jebp__sse_move_px1(v_rot, v_tl); + // Pixel 0 + // This does double the SAD result but if both distances are doubled the + // comparison should still be the same + __m128i v_tllo = _mm_unpacklo_epi32(v_tl, v_tl); + __m128i v_toplo = _mm_unpacklo_epi32(v_top, v_top); + v_ldist = _mm_sad_epu8(v_tllo, v_toplo); + v_tdist = _mm_unpacklo_epi32(v_left, v_left); + v_tdist = _mm_sad_epu8(v_tllo, v_tdist); + v_cmp = _mm_cmplt_epi32(v_ldist, v_tdist); + v_pixello = jebp__sse_bsela_u8x16(v_pixel, v_cmp, v_left, v_top); + v_left = _mm_bslli_si128(v_pixello, 4); + // Pixel 1 + v_tdist = _mm_unpacklo_epi32(v_left, v_left); + v_tdist = _mm_sad_epu8(v_tllo, v_tdist); + v_cmp = _mm_cmplt_epi32(v_ldist, v_tdist); + v_cmp = _mm_bsrli_si128(v_cmp, 4); + v_pixello = jebp__sse_bsela_u8x16(v_pixel, v_cmp, v_left, v_top); + v_pixello = _mm_unpacklo_epi32(v_left, v_pixello); + v_left = _mm_bsrli_si128(v_pixello, 4); + // Pixel 2 + __m128i v_tlhi = _mm_shuffle_epi32(v_tl, _MM_SHUFFLE(2, 2, 3, 3)); + __m128i v_tophi = _mm_shuffle_epi32(v_top, _MM_SHUFFLE(2, 2, 3, 3)); + v_ldist = _mm_sad_epu8(v_tlhi, v_tophi); + v_tdist = _mm_shuffle_epi32(v_left, _MM_SHUFFLE(2, 2, 3, 3)); + v_tdist = _mm_sad_epu8(v_tlhi, v_tdist); + v_cmp = _mm_cmplt_epi32(v_ldist, v_tdist); + v_pixelhi = jebp__sse_bsela_u8x16(v_pixel, v_cmp, v_left, v_top); + v_left = _mm_bslli_si128(v_pixelhi, 4); + // Pixel 3 + v_tdist = _mm_shuffle_epi32(v_left, _MM_SHUFFLE(2, 2, 3, 3)); + v_tdist = _mm_sad_epu8(v_tlhi, v_tdist); + v_cmp = _mm_cmplt_epi32(v_ldist, v_tdist); + v_cmp = _mm_bslli_si128(v_cmp, 12); + v_pixelhi = jebp__sse_bsela_u8x16(v_pixel, v_cmp, v_left, v_top); + v_pixelhi = _mm_unpackhi_epi32(v_left, v_pixelhi); + v_left = _mm_bsrli_si128(v_pixelhi, 12); + v_pixel = _mm_unpackhi_epi64(v_pixello, v_pixelhi); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + v_tl = v_rot; + } +#elif defined(JEBP__SIMD_NEON) + uint8x16_t v_left; + uint8x16_t v_tl; + if (width >= 4) { + v_left = jebp__neon_load_px1(&pixel[-1]); + v_tl = jebp__neon_load_px1(&top[-1]); + } + for (; x + 4 <= width; x += 4) { + uint8x16_t v_pixel = vld1q_u8((uint8_t *)&pixel[x]); + uint8x16_t v_top = vld1q_u8((uint8_t *)&top[x]); + v_tl = vextq_u8(v_tl, v_top, 12); + uint32x4_t v_ldist = jebp__neon_sad_px4(v_tl, v_top); + uint8x16x4_t v_pixel4; + JEBP__UNROLL4(jebp_int i, { + uint32x4_t v_tdist = jebp__neon_sad_px4(v_tl, v_left); + uint32x4_t v_cmp = vcltq_u32(v_ldist, v_tdist); + uint8x16_t v_pred = vbslq_u8((uint8x16_t)v_cmp, v_left, v_top); + v_pixel4.val[i] = vaddq_u8(v_pixel, v_pred); + v_left = vextq_u8(v_pixel4.val[i], v_pixel4.val[i], 12); + }) + v_pixel = jebp__neon_flatten_px4(v_pixel4); + vst1q_u8((uint8_t *)&pixel[x], v_pixel); + v_tl = v_top; + } +#endif + for (; x < width; x += 1) { + jebp_int ldist = jebp__vp8l_pred_dist(&top[x - 1], &top[x]); + jebp_int tdist = jebp__vp8l_pred_dist(&top[x - 1], &pixel[x - 1]); + if (ldist < tdist) { + jebp__vp8l_pred_left(&pixel[x], 1); + } else { + jebp__vp8l_pred_top(&pixel[x], &top[x], 1); + } + } +} + +static void jebp__vp8l_pred12(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + __m128i v_left; + __m128i v_tl; + if (width >= 4) { + v_left = _mm_cvtsi32_si128(*(int *)&pixel[-1]); + v_tl = _mm_cvtsi32_si128(*(int *)&top[-1]); + } + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + __m128i v_top = _mm_loadu_si128((__m128i *)&top[x]); + __m128i v_rot = _mm_shuffle_epi32(v_top, _MM_SHUFFLE(2, 1, 0, 3)); + v_tl = jebp__sse_move_px1(v_rot, v_tl); + __m128i v_max = _mm_max_epu8(v_top, v_tl); + __m128i v_min = _mm_min_epu8(v_top, v_tl); + __m128i v_diff = _mm_sub_epi8(v_max, v_min); + __m128i v_pos = _mm_cmpeq_epi8(v_max, v_top); + jebp__m128x4i v_pixel4; + JEBP__UNROLL4(jebp_int i, { + __m128i v_add = _mm_adds_epu8(v_left, v_diff); + __m128i v_sub = _mm_subs_epu8(v_left, v_diff); + v_pixel4.v[i] = jebp__sse_bsela_u8x16(v_pixel, v_pos, v_add, v_sub); + v_left = _mm_shuffle_epi32(v_pixel4.v[i], _MM_SHUFFLE(2, 1, 0, 3)); + }) + v_pixel = jebp__sse_flatten_px4(v_pixel4); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + v_tl = v_rot; + } +#elif defined(JEBP__SIMD_NEON) + uint8x16_t v_left; + uint8x16_t v_tl; + if (width >= 4) { + v_left = jebp__neon_load_px1(&pixel[-1]); + v_tl = jebp__neon_load_px1(&top[-1]); + } + for (; x + 4 <= width; x += 4) { + uint8x16_t v_pixel = vld1q_u8((uint8_t *)&pixel[x]); + uint8x16_t v_top = vld1q_u8((uint8_t *)&top[x]); + v_tl = vextq_u8(v_tl, v_top, 12); + uint8x16_t v_diff = vabdq_u8(v_top, v_tl); + uint8x16_t v_neg = vcltq_u8(v_top, v_tl); + uint8x16x4_t v_pixel4; + JEBP__UNROLL4(jebp_int i, { + uint8x16_t v_add = vqaddq_u8(v_left, v_diff); + uint8x16_t v_sub = vqsubq_u8(v_left, v_diff); + uint8x16_t v_pred = vbslq_u8(v_neg, v_sub, v_add); + v_pixel4.val[i] = vaddq_u8(v_pixel, v_pred); + v_left = vextq_u8(v_pixel4.val[i], v_pixel4.val[i], 12); + }) + v_pixel = jebp__neon_flatten_px4(v_pixel4); + vst1q_u8((uint8_t *)&pixel[x], v_pixel); + v_tl = v_top; + } +#endif + for (; x < width; x += 1) { + pixel[x].r += + JEBP__CLAMP_UBYTE(pixel[x - 1].r + top[x].r - top[x - 1].r); + pixel[x].g += + JEBP__CLAMP_UBYTE(pixel[x - 1].g + top[x].g - top[x - 1].g); + pixel[x].b += + JEBP__CLAMP_UBYTE(pixel[x - 1].b + top[x].b - top[x - 1].b); + pixel[x].a += + JEBP__CLAMP_UBYTE(pixel[x - 1].a + top[x].a - top[x - 1].a); + } +} + +static void jebp__vp8l_pred13(jebp_color_t *pixel, jebp_color_t *top, + jebp_int width) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + __m128i v_mask = _mm_set1_epi8(0x7f); + __m128i v_left; + __m128i v_tl; + if (width >= 4) { + v_left = _mm_cvtsi32_si128(*(int *)&pixel[-1]); + v_tl = _mm_cvtsi32_si128(*(int *)&top[-1]); + } + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + __m128i v_top = _mm_loadu_si128((__m128i *)&top[x]); + __m128i v_rot = _mm_shuffle_epi32(v_top, _MM_SHUFFLE(2, 1, 0, 3)); + v_tl = jebp__sse_move_px1(v_rot, v_tl); + jebp__m128x4i v_pixel4; + JEBP__UNROLL4(jebp_int i, { + __m128i v_avg = jebp__sse_avg_u8x16(v_left, v_top); + __m128i v_max = _mm_max_epu8(v_avg, v_tl); + __m128i v_min = _mm_min_epu8(v_avg, v_tl); + __m128i v_diff = _mm_sub_epi8(v_max, v_min); + v_diff = _mm_srli_epi16(v_diff, 1); + v_diff = _mm_and_si128(v_diff, v_mask); + __m128i v_pos = _mm_cmpeq_epi8(v_max, v_avg); + __m128i v_add = _mm_adds_epu8(v_avg, v_diff); + __m128i v_sub = _mm_subs_epu8(v_avg, v_diff); + v_pixel4.v[i] = jebp__sse_bsela_u8x16(v_pixel, v_pos, v_add, v_sub); + v_left = _mm_shuffle_epi32(v_pixel4.v[i], _MM_SHUFFLE(2, 1, 0, 3)); + }) + v_pixel = jebp__sse_flatten_px4(v_pixel4); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + v_tl = v_rot; + } +#elif defined(JEBP__SIMD_NEON) + uint8x16_t v_left; + uint8x16_t v_tl; + if (width >= 4) { + v_left = jebp__neon_load_px1(&pixel[-1]); + v_tl = jebp__neon_load_px1(&top[-1]); + } + for (; x + 4 <= width; x += 4) { + uint8x16_t v_pixel = vld1q_u8((uint8_t *)&pixel[x]); + uint8x16_t v_top = vld1q_u8((uint8_t *)&top[x]); + v_tl = vextq_u8(v_tl, v_top, 12); + uint8x16x4_t v_pixel4; + JEBP__UNROLL4(jebp_int i, { + uint8x16_t v_avg = vhaddq_u8(v_left, v_top); + uint8x16_t v_diff = vabdq_u8(v_avg, v_tl); + v_diff = vshrq_n_u8(v_diff, 1); + uint8x16_t v_neg = vcltq_u8(v_avg, v_tl); + uint8x16_t v_add = vqaddq_u8(v_avg, v_diff); + uint8x16_t v_sub = vqsubq_u8(v_avg, v_diff); + uint8x16_t v_pred = vbslq_u8(v_neg, v_sub, v_add); + v_pixel4.val[i] = vaddq_u8(v_pixel, v_pred); + v_left = vextq_u8(v_pixel4.val[i], v_pixel4.val[i], 12); + }) + v_pixel = jebp__neon_flatten_px4(v_pixel4); + vst1q_u8((uint8_t *)&pixel[x], v_pixel); + v_tl = v_top; + } +#endif + for (; x < width; x += 1) { + jebp_color_t avg = {JEBP__AVG(pixel[x - 1].r, top[x].r), + JEBP__AVG(pixel[x - 1].g, top[x].g), + JEBP__AVG(pixel[x - 1].b, top[x].b), + JEBP__AVG(pixel[x - 1].a, top[x].a)}; + pixel[x].r += JEBP__CLAMP_UBYTE(avg.r + (avg.r - top[x - 1].r) / 2); + pixel[x].g += JEBP__CLAMP_UBYTE(avg.g + (avg.g - top[x - 1].g) / 2); + pixel[x].b += JEBP__CLAMP_UBYTE(avg.b + (avg.b - top[x - 1].b) / 2); + pixel[x].a += JEBP__CLAMP_UBYTE(avg.a + (avg.a - top[x - 1].a) / 2); + } +} + +static const jebp__vp8l_pred_t jebp__vp8l_preds[JEBP__NB_VP8L_PRED_TYPES] = { + jebp__vp8l_pred0, jebp__vp8l_pred1, jebp__vp8l_pred2, jebp__vp8l_pred3, + jebp__vp8l_pred4, jebp__vp8l_pred5, jebp__vp8l_pred6, jebp__vp8l_pred7, + jebp__vp8l_pred8, jebp__vp8l_pred9, jebp__vp8l_pred10, jebp__vp8l_pred11, + jebp__vp8l_pred12, jebp__vp8l_pred13}; + +/** + * VP8L transforms + */ +typedef enum jebp__transform_type_t { + JEBP__TRANSFORM_PREDICT, + JEBP__TRANSFORM_COLOR, + JEBP__TRANSFORM_GREEN, + JEBP__TRANSFORM_PALETTE, + JEBP__NB_TRANSFORMS +} jebp__transform_type_t; + +typedef struct jebp__transform_t { + jebp__transform_type_t type; + jebp__subimage_t image; +} jebp__transform_t; + +static jebp_error_t jebp__read_transform(jebp__transform_t *transform, + jebp__bit_reader_t *bits, + jebp_image_t *image) { + jebp_error_t err = JEBP_OK; + transform->type = jebp__read_bits(bits, 2, &err); + if (err != JEBP_OK) { + return err; + } + if (transform->type == JEBP__TRANSFORM_PALETTE) { + // TODO: support palette images + return JEBP_ERROR_NOSUP_PALETTE; + } else if (transform->type != JEBP__TRANSFORM_GREEN) { + err = jebp__read_subimage(&transform->image, bits, image); + } + return err; +} + +static void jebp__free_transform(jebp__transform_t *transform) { + if (transform->type != JEBP__TRANSFORM_GREEN) { + jebp_free_image((jebp_image_t *)&transform->image); + } +} + +JEBP__INLINE jebp_error_t jebp__apply_predict_row(jebp_color_t *pixel, + jebp_color_t *top, + jebp_int width, + jebp_color_t *predict_pixel) { + if (predict_pixel->g >= JEBP__NB_VP8L_PRED_TYPES) { + return JEBP_ERROR_INVDATA; + } + jebp__vp8l_preds[predict_pixel->g](pixel, top, width); + return JEBP_OK; +} + +JEBP__INLINE jebp_error_t jebp__apply_predict_transform( + jebp_image_t *image, jebp__subimage_t *predict_image) { + jebp_error_t err; + jebp_color_t *pixel = image->pixels; + jebp_color_t *top = pixel; + jebp_int predict_width = predict_image->width - 1; + jebp_int block_size = 1 << predict_image->block_bits; + jebp_int end_size = + image->width - (predict_width << predict_image->block_bits); + if (predict_width == 0) { + // Special case: if there is only one block the first block which is + // shortened by one pixel (due to the left prediction) + // needs to be `end_size` and the proper end block then + // needs to be skipped. + block_size = end_size; + end_size = 0; + } + // Use opaque-black prediction for the top-left pixel + jebp__vp8l_pred_black(pixel, 1); + // Use left prediction for the top row + jebp__vp8l_pred_left(pixel + 1, image->width - 1); + pixel += image->width; + for (jebp_int y = 1; y < image->height; y += 1) { + jebp_color_t *predict_row = + &predict_image->pixels[(y >> predict_image->block_bits) * + predict_image->width]; + // Use top prediction for the left column + jebp__vp8l_pred_top(pixel, top, 1); + // Finish the rest of the first block + if ((err = jebp__apply_predict_row(pixel + 1, top + 1, block_size - 1, + predict_row)) != JEBP_OK) { + return err; + } + pixel += block_size; + top += block_size; + for (jebp_int x = 1; x < predict_width; x += 1) { + if ((err = jebp__apply_predict_row(pixel, top, block_size, + &predict_row[x])) != JEBP_OK) { + return err; + } + pixel += block_size; + top += block_size; + } + jebp__apply_predict_row(pixel, top, end_size, + &predict_row[predict_width]); + pixel += end_size; + top += end_size; + } + return JEBP_OK; +} + +JEBP__INLINE void jebp__apply_color_row(jebp_color_t *pixel, jebp_int width, + jebp_color_t *color_pixel) { + jebp_int x = 0; +#if defined(JEBP__SIMD_SSE2) + jebp_ushort color_r = ((jebp_short)(color_pixel->r << 8) >> 5); + jebp_ushort color_g = ((jebp_short)(color_pixel->g << 8) >> 5); + jebp_ushort color_b = ((jebp_short)(color_pixel->b << 8) >> 5); + __m128i v_color_bg = _mm_set1_epi32(color_b | ((jebp_uint)color_g << 16)); + __m128i v_color_r = _mm_set1_epi32(color_r); + __m128i v_masklo = _mm_set1_epi16((short)0x00ff); + __m128i v_maskhi = _mm_set1_epi16((short)0xff00); + for (; x + 4 <= width; x += 4) { + __m128i v_pixel = _mm_loadu_si128((__m128i *)&pixel[x]); + __m128i v_green = _mm_and_si128(v_pixel, v_maskhi); + v_green = _mm_shufflelo_epi16(v_green, _MM_SHUFFLE(2, 2, 0, 0)); + v_green = _mm_shufflehi_epi16(v_green, _MM_SHUFFLE(2, 2, 0, 0)); + __m128i v_bg = _mm_mulhi_epi16(v_green, v_color_bg); + v_bg = _mm_and_si128(v_bg, v_masklo); + v_pixel = _mm_add_epi8(v_pixel, v_bg); + __m128i v_red = _mm_slli_epi16(v_pixel, 8); + v_red = _mm_mulhi_epi16(v_red, v_color_r); + v_red = _mm_and_si128(v_red, v_masklo); + v_red = _mm_slli_epi32(v_red, 16); + v_pixel = _mm_add_epi8(v_pixel, v_red); + _mm_storeu_si128((__m128i *)&pixel[x], v_pixel); + } +#elif defined(JEBP__SIMD_NEON) + int8x8x3_t v_color_pixel = vld3_dup_s8((jebp_byte *)color_pixel); + for (; x + 8 <= width; x += 8) { + int16x8_t v_mul; + int8x8_t v_shr; + int8x8x4_t v_pixel = vld4_s8((jebp_byte *)&pixel[x]); + v_mul = vmull_s8(v_pixel.val[1], v_color_pixel.val[2]); + v_shr = vshrn_n_s16(v_mul, 5); + v_pixel.val[0] = vadd_s8(v_pixel.val[0], v_shr); + v_mul = vmull_s8(v_pixel.val[1], v_color_pixel.val[1]); + v_shr = vshrn_n_s16(v_mul, 5); + v_pixel.val[2] = vadd_s8(v_pixel.val[2], v_shr); + v_mul = vmull_s8(v_pixel.val[0], v_color_pixel.val[0]); + v_shr = vshrn_n_s16(v_mul, 5); + v_pixel.val[2] = vadd_s8(v_pixel.val[2], v_shr); + vst4_s8((jebp_byte *)&pixel[x], v_pixel); + } +#endif + for (; x < width; x += 1) { + pixel[x].r += ((jebp_byte)pixel[x].g * (jebp_byte)color_pixel->b) >> 5; + pixel[x].b += ((jebp_byte)pixel[x].g * (jebp_byte)color_pixel->g) >> 5; + pixel[x].b += ((jebp_byte)pixel[x].r * (jebp_byte)color_pixel->r) >> 5; + } +} + +JEBP__INLINE jebp_error_t jebp__apply_color_transform( + jebp_image_t *image, jebp__subimage_t *color_image) { + jebp_color_t *pixel = image->pixels; + jebp_int color_width = color_image->width - 1; + jebp_int block_size = 1 << color_image->block_bits; + jebp_int end_size = image->width - (color_width << color_image->block_bits); + for (jebp_int y = 0; y < image->height; y += 1) { + jebp_color_t *color_row = + &color_image + ->pixels[(y >> color_image->block_bits) * color_image->width]; + for (jebp_int x = 0; x < color_width; x += 1) { + jebp__apply_color_row(pixel, block_size, &color_row[x]); + pixel += block_size; + } + jebp__apply_color_row(pixel, end_size, &color_row[color_width]); + pixel += end_size; + } + return JEBP_OK; +} + +JEBP__INLINE jebp_error_t jebp__apply_green_transform(jebp_image_t *image) { + jebp_int size = image->width * image->height; + jebp_int i = 0; +#if defined(JEBP__SIMD_SSE2) + for (; i + 4 <= size; i += 4) { + __m128i *pixel = (__m128i *)&image->pixels[i]; + __m128i v_pixel = _mm_loadu_si128(pixel); + __m128i v_green = _mm_srli_epi16(v_pixel, 8); + v_green = _mm_shufflelo_epi16(v_green, _MM_SHUFFLE(2, 2, 0, 0)); + v_green = _mm_shufflehi_epi16(v_green, _MM_SHUFFLE(2, 2, 0, 0)); + v_pixel = _mm_add_epi8(v_pixel, v_green); + _mm_storeu_si128(pixel, v_pixel); + } +#elif defined(JEBP__SIMD_NEON) + for (; i + 16 <= size; i += 16) { + jebp_ubyte *pixel = (jebp_ubyte *)&image->pixels[i]; + uint8x16x4_t v_pixel = vld4q_u8(pixel); + v_pixel.val[0] = vaddq_u8(v_pixel.val[0], v_pixel.val[1]); + v_pixel.val[2] = vaddq_u8(v_pixel.val[2], v_pixel.val[1]); + vst4q_u8(pixel, v_pixel); + } +#endif + for (; i < size; i += 1) { + jebp_color_t *pixel = &image->pixels[i]; + pixel->r += pixel->g; + pixel->b += pixel->g; + } + return JEBP_OK; +} + +static jebp_error_t jebp__apply_transform(jebp__transform_t *transform, + jebp_image_t *image) { + switch (transform->type) { + case JEBP__TRANSFORM_PREDICT: + return jebp__apply_predict_transform(image, &transform->image); + case JEBP__TRANSFORM_COLOR: + return jebp__apply_color_transform(image, &transform->image); + case JEBP__TRANSFORM_GREEN: + return jebp__apply_green_transform(image); + default: + return JEBP_ERROR_NOSUP; + } +} + +/** + * VP8L lossless codec + */ +#define JEBP__VP8L_TAG 0x4c385056 +#define JEBP__VP8L_MAGIC 0x2f + +static jebp_error_t jebp__read_vp8l_header(jebp_image_t *image, + jebp__reader_t *reader, + jebp__bit_reader_t *bits, + jebp__chunk_t *chunk) { + jebp_error_t err = JEBP_OK; + if (chunk->size < 5) { + return JEBP_ERROR_INVDATA_HEADER; + } + if (jebp__read_uint8(reader, &err) != JEBP__VP8L_MAGIC) { + return jebp__error(&err, JEBP_ERROR_INVDATA_HEADER); + } + jepb__init_bit_reader(bits, reader, chunk->size - 1); + image->width = jebp__read_bits(bits, 14, &err) + 1; + image->height = jebp__read_bits(bits, 14, &err) + 1; + jebp__read_bits(bits, 1, &err); // alpha does not impact decoding + if (jebp__read_bits(bits, 3, &err) != 0) { + // version must be 0 + return jebp__error(&err, JEBP_ERROR_NOSUP); + } + return err; +} + +static jebp_error_t jebp__read_vp8l_size(jebp_image_t *image, + jebp__reader_t *reader, + jebp__chunk_t *chunk) { + jebp__bit_reader_t bits; + return jebp__read_vp8l_header(image, reader, &bits, chunk); +} + +static jebp_error_t jebp__read_vp8l_nohead(jebp_image_t *image, + jebp__bit_reader_t *bits) { + jebp_error_t err = JEBP_OK; + jebp__transform_t transforms[4]; + jebp_int nb_transforms = 0; + for (; nb_transforms <= JEBP__NB_TRANSFORMS; nb_transforms += 1) { + if (!jebp__read_bits(bits, 1, &err)) { + // no more transforms to read + break; + } + if (err != JEBP_OK || nb_transforms == JEBP__NB_TRANSFORMS) { + // too many transforms + jebp__error(&err, JEBP_ERROR_INVDATA); + goto free_transforms; + } + if ((err = jebp__read_transform(&transforms[nb_transforms], bits, + image)) != JEBP_OK) { + goto free_transforms; + } + } + if (err != JEBP_OK) { + goto free_transforms; + } + + jebp__colcache_t colcache; + if ((err = jebp__read_colcache(&colcache, bits)) != JEBP_OK) { + goto free_transforms; + } + jebp__subimage_t *huffman_image = &(jebp__subimage_t){0}; + if (!jebp__read_bits(bits, 1, &err)) { + // there is no huffman image + huffman_image = NULL; + } + if (err != JEBP_OK) { + jebp__free_colcache(&colcache); + goto free_transforms; + } + if (huffman_image != NULL) { + if ((err = jebp__read_subimage(huffman_image, bits, image)) != + JEBP_OK) { + jebp__free_colcache(&colcache); + goto free_transforms; + } + } + err = jebp__read_vp8l_image(image, bits, &colcache, huffman_image); + jebp__free_colcache(&colcache); + jebp_free_image((jebp_image_t *)huffman_image); + +free_transforms: + for (nb_transforms -= 1; nb_transforms >= 0; nb_transforms -= 1) { + if (err == JEBP_OK) { + err = jebp__apply_transform(&transforms[nb_transforms], image); + } + jebp__free_transform(&transforms[nb_transforms]); + } + return err; +} + +static jebp_error_t jebp__read_vp8l(jebp_image_t *image, jebp__reader_t *reader, + jebp__chunk_t *chunk) { + jebp_error_t err; + jebp__bit_reader_t bits; + if ((err = jebp__read_vp8l_header(image, reader, &bits, chunk)) != + JEBP_OK) { + return err; + } + if ((err = jebp__read_vp8l_nohead(image, &bits)) != JEBP_OK) { + return err; + } + return JEBP_OK; +} +#endif // JEBP_NO_VP8L + +/** + * Public API + */ +static const char *const jebp__error_strings[JEBP_NB_ERRORS]; + +const char *jebp_error_string(jebp_error_t err) { + if (err < 0 || err >= JEBP_NB_ERRORS) { + err = JEBP_ERROR_UNKNOWN; + } + return jebp__error_strings[err]; +} + +void jebp_free_image(jebp_image_t *image) { + if (image != NULL) { + JEBP_FREE(image->pixels); + JEBP__CLEAR(image, sizeof(jebp_image_t)); + } +} + +static jebp_error_t jebp__read_size(jebp_image_t *image, + jebp__reader_t *reader) { + jebp_error_t err; + jebp__riff_reader_t riff; + JEBP__CLEAR(image, sizeof(jebp_image_t)); + if ((err = jebp__read_riff_header(&riff, reader)) != JEBP_OK) { + return err; + } + jebp__chunk_t chunk; + if ((err = jebp__read_riff_chunk(&riff, &chunk)) != JEBP_OK) { + return err; + } + + switch (chunk.tag) { +#ifndef JEBP_NO_VP8L + case JEBP__VP8L_TAG: + return jebp__read_vp8l_size(image, reader, &chunk); +#endif // JEBP_NO_VP8L + default: + return JEBP_ERROR_NOSUP_CODEC; + } +} + +jebp_error_t jebp_decode_size(jebp_image_t *image, size_t size, + const void *data) { + if (image == NULL || data == NULL) { + return JEBP_ERROR_INVAL; + } + jebp__reader_t reader; + jebp__init_memory(&reader, size, data); + return jebp__read_size(image, &reader); +} + +static jebp_error_t jebp__read(jebp_image_t *image, jebp__reader_t *reader) { + jebp_error_t err; + jebp__riff_reader_t riff; + JEBP__CLEAR(image, sizeof(jebp_image_t)); + if ((err = jebp__read_riff_header(&riff, reader)) != JEBP_OK) { + return err; + } + jebp__chunk_t chunk; + if ((err = jebp__read_riff_chunk(&riff, &chunk)) != JEBP_OK) { + return err; + } + + switch (chunk.tag) { +#ifndef JEBP_NO_VP8L + case JEBP__VP8L_TAG: + return jebp__read_vp8l(image, reader, &chunk); +#endif // JEBP_NO_VP8L + default: + return JEBP_ERROR_NOSUP_CODEC; + } +} + +jebp_error_t jebp_decode(jebp_image_t *image, size_t size, const void *data) { + if (image == NULL || data == NULL) { + return JEBP_ERROR_INVAL; + } + jebp__reader_t reader; + jebp__init_memory(&reader, size, data); + return jebp__read(image, &reader); +} + +#ifndef JEBP_NO_STDIO +jebp_error_t jebp_read_size(jebp_image_t *image, const char *path) { + jebp_error_t err; + if (image == NULL || path == NULL) { + return JEBP_ERROR_INVAL; + } + jebp__reader_t reader; + if ((err = jebp__open_file(&reader, path)) != JEBP_OK) { + return err; + } + err = jebp__read_size(image, &reader); + jebp__close_file(&reader); + return err; +} + +jebp_error_t jebp_read(jebp_image_t *image, const char *path) { + jebp_error_t err; + if (image == NULL || path == NULL) { + return JEBP_ERROR_INVAL; + } + jebp__reader_t reader; + if ((err = jebp__open_file(&reader, path)) != JEBP_OK) { + return err; + } + err = jebp__read(image, &reader); + jebp__close_file(&reader); + return err; +} +#endif // JEBP_NO_STDIO + +/** + * Lookup tables + */ +// These are moved to the end of the file since some of them are very large and +// putting them in the middle of the code would disrupt the flow of reading. +// Especially since in most situations the values in these tables are +// unimportant to the developer. +#ifndef JEBP_NO_VP8L +// The order that meta lengths are read +static const jebp_byte jebp__meta_length_order[JEBP__NB_META_SYMBOLS] = { + 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + +// {X, Y} offsets from the pixel when decoding short distance codes +static const jebp_byte jebp__vp8l_offsets[JEBP__NB_VP8L_OFFSETS][2] = { + {0, 1}, {1, 0}, {1, 1}, {-1, 1}, {0, 2}, {2, 0}, {1, 2}, {-1, 2}, + {2, 1}, {-2, 1}, {2, 2}, {-2, 2}, {0, 3}, {3, 0}, {1, 3}, {-1, 3}, + {3, 1}, {-3, 1}, {2, 3}, {-2, 3}, {3, 2}, {-3, 2}, {0, 4}, {4, 0}, + {1, 4}, {-1, 4}, {4, 1}, {-4, 1}, {3, 3}, {-3, 3}, {2, 4}, {-2, 4}, + {4, 2}, {-4, 2}, {0, 5}, {3, 4}, {-3, 4}, {4, 3}, {-4, 3}, {5, 0}, + {1, 5}, {-1, 5}, {5, 1}, {-5, 1}, {2, 5}, {-2, 5}, {5, 2}, {-5, 2}, + {4, 4}, {-4, 4}, {3, 5}, {-3, 5}, {5, 3}, {-5, 3}, {0, 6}, {6, 0}, + {1, 6}, {-1, 6}, {6, 1}, {-6, 1}, {2, 6}, {-2, 6}, {6, 2}, {-6, 2}, + {4, 5}, {-4, 5}, {5, 4}, {-5, 4}, {3, 6}, {-3, 6}, {6, 3}, {-6, 3}, + {0, 7}, {7, 0}, {1, 7}, {-1, 7}, {5, 5}, {-5, 5}, {7, 1}, {-7, 1}, + {4, 6}, {-4, 6}, {6, 4}, {-6, 4}, {2, 7}, {-2, 7}, {7, 2}, {-7, 2}, + {3, 7}, {-3, 7}, {7, 3}, {-7, 3}, {5, 6}, {-5, 6}, {6, 5}, {-6, 5}, + {8, 0}, {4, 7}, {-4, 7}, {7, 4}, {-7, 4}, {8, 1}, {8, 2}, {6, 6}, + {-6, 6}, {8, 3}, {5, 7}, {-5, 7}, {7, 5}, {-7, 5}, {8, 4}, {6, 7}, + {-6, 7}, {7, 6}, {-7, 6}, {8, 5}, {7, 7}, {-7, 7}, {8, 6}, {8, 7}}; +#endif // JEBP_NO_VP8L + +// Error strings to return from jebp_error_string +static const char *const jebp__error_strings[JEBP_NB_ERRORS] = { + "Ok", + "Invalid value or argument", + "Invalid data or corrupted file", + "Invalid WebP header or corrupted file", + "End of file", + "Feature not supported", + "Codec not supported", + "Color-indexing or palettes are not supported", + "Not enough memory", + "I/O error", + "Unknown error"}; +#endif // JEBP_IMPLEMENTATION diff --git a/src/cdeps/stb_image.h b/src/cdeps/stb_image.h new file mode 100644 index 0000000..9eedabe --- /dev/null +++ b/src/cdeps/stb_image.h @@ -0,0 +1,7988 @@ +/* stb_image - v2.30 - public domain image loader - http://nothings.org/stb + no warranty implied; use at your own risk + + Do this: + #define STB_IMAGE_IMPLEMENTATION + before you include this file in *one* C or C++ file to create the implementation. + + // i.e. it should look like this: + #include ... + #include ... + #include ... + #define STB_IMAGE_IMPLEMENTATION + #include "stb_image.h" + + You can #define STBI_ASSERT(x) before the #include to avoid using assert.h. + And #define STBI_MALLOC, STBI_REALLOC, and STBI_FREE to avoid using malloc,realloc,free + + + QUICK NOTES: + Primarily of interest to game developers and other people who can + avoid problematic images and only need the trivial interface + + JPEG baseline & progressive (12 bpc/arithmetic not supported, same as stock IJG lib) + PNG 1/2/4/8/16-bit-per-channel + + TGA (not sure what subset, if a subset) + BMP non-1bpp, non-RLE + PSD (composited view only, no extra channels, 8/16 bit-per-channel) + + GIF (*comp always reports as 4-channel) + HDR (radiance rgbE format) + PIC (Softimage PIC) + PNM (PPM and PGM binary only) + + Animated GIF still needs a proper API, but here's one way to do it: + http://gist.github.com/urraka/685d9a6340b26b830d49 + + - decode from memory or through FILE (define STBI_NO_STDIO to remove code) + - decode from arbitrary I/O callbacks + - SIMD acceleration on x86/x64 (SSE2) and ARM (NEON) + + Full documentation under "DOCUMENTATION" below. + + +LICENSE + + See end of file for license information. + +RECENT REVISION HISTORY: + + 2.30 (2024-05-31) avoid erroneous gcc warning + 2.29 (2023-05-xx) optimizations + 2.28 (2023-01-29) many error fixes, security errors, just tons of stuff + 2.27 (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes + 2.26 (2020-07-13) many minor fixes + 2.25 (2020-02-02) fix warnings + 2.24 (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically + 2.23 (2019-08-11) fix clang static analysis warning + 2.22 (2019-03-04) gif fixes, fix warnings + 2.21 (2019-02-25) fix typo in comment + 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs + 2.19 (2018-02-11) fix warning + 2.18 (2018-01-30) fix warnings + 2.17 (2018-01-29) bugfix, 1-bit BMP, 16-bitness query, fix warnings + 2.16 (2017-07-23) all functions have 16-bit variants; optimizations; bugfixes + 2.15 (2017-03-18) fix png-1,2,4; all Imagenet JPGs; no runtime SSE detection on GCC + 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs + 2.13 (2016-12-04) experimental 16-bit API, only for PNG so far; fixes + 2.12 (2016-04-02) fix typo in 2.11 PSD fix that caused crashes + 2.11 (2016-04-02) 16-bit PNGS; enable SSE2 in non-gcc x64 + RGB-format JPEG; remove white matting in PSD; + allocate large structures on the stack; + correct channel count for PNG & BMP + 2.10 (2016-01-22) avoid warning introduced in 2.09 + 2.09 (2016-01-16) 16-bit TGA; comments in PNM files; STBI_REALLOC_SIZED + + See end of file for full revision history. + + + ============================ Contributors ========================= + + Image formats Extensions, features + Sean Barrett (jpeg, png, bmp) Jetro Lauha (stbi_info) + Nicolas Schulz (hdr, psd) Martin "SpartanJ" Golini (stbi_info) + Jonathan Dummer (tga) James "moose2000" Brown (iPhone PNG) + Jean-Marc Lienher (gif) Ben "Disch" Wenger (io callbacks) + Tom Seddon (pic) Omar Cornut (1/2/4-bit PNG) + Thatcher Ulrich (psd) Nicolas Guillemot (vertical flip) + Ken Miller (pgm, ppm) Richard Mitton (16-bit PSD) + github:urraka (animated gif) Junggon Kim (PNM comments) + Christopher Forseth (animated gif) Daniel Gibson (16-bit TGA) + socks-the-fox (16-bit PNG) + Jeremy Sawicki (handle all ImageNet JPGs) + Optimizations & bugfixes Mikhail Morozov (1-bit BMP) + Fabian "ryg" Giesen Anael Seghezzi (is-16-bit query) + Arseny Kapoulkine Simon Breuss (16-bit PNM) + John-Mark Allen + Carmelo J Fdez-Aguera + + Bug & warning fixes + Marc LeBlanc David Woo Guillaume George Martins Mozeiko + Christpher Lloyd Jerry Jansson Joseph Thomson Blazej Dariusz Roszkowski + Phil Jordan Dave Moore Roy Eltham + Hayaki Saito Nathan Reed Won Chun + Luke Graham Johan Duparc Nick Verigakis the Horde3D community + Thomas Ruf Ronny Chevalier github:rlyeh + Janez Zemva John Bartholomew Michal Cichon github:romigrou + Jonathan Blow Ken Hamada Tero Hanninen github:svdijk + Eugene Golushkov Laurent Gomila Cort Stratton github:snagar + Aruelien Pocheville Sergio Gonzalez Thibault Reuille github:Zelex + Cass Everitt Ryamond Barbiero github:grim210 + Paul Du Bois Engin Manap Aldo Culquicondor github:sammyhw + Philipp Wiesemann Dale Weiler Oriol Ferrer Mesia github:phprus + Josh Tobin Neil Bickford Matthew Gregan github:poppolopoppo + Julian Raschke Gregory Mullen Christian Floisand github:darealshinji + Baldur Karlsson Kevin Schmidt JR Smith github:Michaelangel007 + Brad Weinberger Matvey Cherevko github:mosra + Luca Sas Alexander Veselov Zack Middleton [reserved] + Ryan C. Gordon [reserved] [reserved] + DO NOT ADD YOUR NAME HERE + + Jacko Dirks + + To add your name to the credits, pick a random blank space in the middle and fill it. + 80% of merge conflicts on stb PRs are due to people adding their name at the end + of the credits. +*/ + +#ifndef STBI_INCLUDE_STB_IMAGE_H +#define STBI_INCLUDE_STB_IMAGE_H + +// DOCUMENTATION +// +// Limitations: +// - no 12-bit-per-channel JPEG +// - no JPEGs with arithmetic coding +// - GIF always returns *comp=4 +// +// Basic usage (see HDR discussion below for HDR usage): +// int x,y,n; +// unsigned char *data = stbi_load(filename, &x, &y, &n, 0); +// // ... process data if not NULL ... +// // ... x = width, y = height, n = # 8-bit components per pixel ... +// // ... replace '0' with '1'..'4' to force that many components per pixel +// // ... but 'n' will always be the number that it would have been if you said 0 +// stbi_image_free(data); +// +// Standard parameters: +// int *x -- outputs image width in pixels +// int *y -- outputs image height in pixels +// int *channels_in_file -- outputs # of image components in image file +// int desired_channels -- if non-zero, # of image components requested in result +// +// The return value from an image loader is an 'unsigned char *' which points +// to the pixel data, or NULL on an allocation failure or if the image is +// corrupt or invalid. The pixel data consists of *y scanlines of *x pixels, +// with each pixel consisting of N interleaved 8-bit components; the first +// pixel pointed to is top-left-most in the image. There is no padding between +// image scanlines or between pixels, regardless of format. The number of +// components N is 'desired_channels' if desired_channels is non-zero, or +// *channels_in_file otherwise. If desired_channels is non-zero, +// *channels_in_file has the number of components that _would_ have been +// output otherwise. E.g. if you set desired_channels to 4, you will always +// get RGBA output, but you can check *channels_in_file to see if it's trivially +// opaque because e.g. there were only 3 channels in the source image. +// +// An output image with N components has the following components interleaved +// in this order in each pixel: +// +// N=#comp components +// 1 grey +// 2 grey, alpha +// 3 red, green, blue +// 4 red, green, blue, alpha +// +// If image loading fails for any reason, the return value will be NULL, +// and *x, *y, *channels_in_file will be unchanged. The function +// stbi_failure_reason() can be queried for an extremely brief, end-user +// unfriendly explanation of why the load failed. Define STBI_NO_FAILURE_STRINGS +// to avoid compiling these strings at all, and STBI_FAILURE_USERMSG to get slightly +// more user-friendly ones. +// +// Paletted PNG, BMP, GIF, and PIC images are automatically depalettized. +// +// To query the width, height and component count of an image without having to +// decode the full file, you can use the stbi_info family of functions: +// +// int x,y,n,ok; +// ok = stbi_info(filename, &x, &y, &n); +// // returns ok=1 and sets x, y, n if image is a supported format, +// // 0 otherwise. +// +// Note that stb_image pervasively uses ints in its public API for sizes, +// including sizes of memory buffers. This is now part of the API and thus +// hard to change without causing breakage. As a result, the various image +// loaders all have certain limits on image size; these differ somewhat +// by format but generally boil down to either just under 2GB or just under +// 1GB. When the decoded image would be larger than this, stb_image decoding +// will fail. +// +// Additionally, stb_image will reject image files that have any of their +// dimensions set to a larger value than the configurable STBI_MAX_DIMENSIONS, +// which defaults to 2**24 = 16777216 pixels. Due to the above memory limit, +// the only way to have an image with such dimensions load correctly +// is for it to have a rather extreme aspect ratio. Either way, the +// assumption here is that such larger images are likely to be malformed +// or malicious. If you do need to load an image with individual dimensions +// larger than that, and it still fits in the overall size limit, you can +// #define STBI_MAX_DIMENSIONS on your own to be something larger. +// +// =========================================================================== +// +// UNICODE: +// +// If compiling for Windows and you wish to use Unicode filenames, compile +// with +// #define STBI_WINDOWS_UTF8 +// and pass utf8-encoded filenames. Call stbi_convert_wchar_to_utf8 to convert +// Windows wchar_t filenames to utf8. +// +// =========================================================================== +// +// Philosophy +// +// stb libraries are designed with the following priorities: +// +// 1. easy to use +// 2. easy to maintain +// 3. good performance +// +// Sometimes I let "good performance" creep up in priority over "easy to maintain", +// and for best performance I may provide less-easy-to-use APIs that give higher +// performance, in addition to the easy-to-use ones. Nevertheless, it's important +// to keep in mind that from the standpoint of you, a client of this library, +// all you care about is #1 and #3, and stb libraries DO NOT emphasize #3 above all. +// +// Some secondary priorities arise directly from the first two, some of which +// provide more explicit reasons why performance can't be emphasized. +// +// - Portable ("ease of use") +// - Small source code footprint ("easy to maintain") +// - No dependencies ("ease of use") +// +// =========================================================================== +// +// I/O callbacks +// +// I/O callbacks allow you to read from arbitrary sources, like packaged +// files or some other source. Data read from callbacks are processed +// through a small internal buffer (currently 128 bytes) to try to reduce +// overhead. +// +// The three functions you must define are "read" (reads some bytes of data), +// "skip" (skips some bytes of data), "eof" (reports if the stream is at the end). +// +// =========================================================================== +// +// SIMD support +// +// The JPEG decoder will try to automatically use SIMD kernels on x86 when +// supported by the compiler. For ARM Neon support, you must explicitly +// request it. +// +// (The old do-it-yourself SIMD API is no longer supported in the current +// code.) +// +// On x86, SSE2 will automatically be used when available based on a run-time +// test; if not, the generic C versions are used as a fall-back. On ARM targets, +// the typical path is to have separate builds for NEON and non-NEON devices +// (at least this is true for iOS and Android). Therefore, the NEON support is +// toggled by a build flag: define STBI_NEON to get NEON loops. +// +// If for some reason you do not want to use any of SIMD code, or if +// you have issues compiling it, you can disable it entirely by +// defining STBI_NO_SIMD. +// +// =========================================================================== +// +// HDR image support (disable by defining STBI_NO_HDR) +// +// stb_image supports loading HDR images in general, and currently the Radiance +// .HDR file format specifically. You can still load any file through the existing +// interface; if you attempt to load an HDR file, it will be automatically remapped +// to LDR, assuming gamma 2.2 and an arbitrary scale factor defaulting to 1; +// both of these constants can be reconfigured through this interface: +// +// stbi_hdr_to_ldr_gamma(2.2f); +// stbi_hdr_to_ldr_scale(1.0f); +// +// (note, do not use _inverse_ constants; stbi_image will invert them +// appropriately). +// +// Additionally, there is a new, parallel interface for loading files as +// (linear) floats to preserve the full dynamic range: +// +// float *data = stbi_loadf(filename, &x, &y, &n, 0); +// +// If you load LDR images through this interface, those images will +// be promoted to floating point values, run through the inverse of +// constants corresponding to the above: +// +// stbi_ldr_to_hdr_scale(1.0f); +// stbi_ldr_to_hdr_gamma(2.2f); +// +// Finally, given a filename (or an open file or memory block--see header +// file for details) containing image data, you can query for the "most +// appropriate" interface to use (that is, whether the image is HDR or +// not), using: +// +// stbi_is_hdr(char *filename); +// +// =========================================================================== +// +// iPhone PNG support: +// +// We optionally support converting iPhone-formatted PNGs (which store +// premultiplied BGRA) back to RGB, even though they're internally encoded +// differently. To enable this conversion, call +// stbi_convert_iphone_png_to_rgb(1). +// +// Call stbi_set_unpremultiply_on_load(1) as well to force a divide per +// pixel to remove any premultiplied alpha *only* if the image file explicitly +// says there's premultiplied data (currently only happens in iPhone images, +// and only if iPhone convert-to-rgb processing is on). +// +// =========================================================================== +// +// ADDITIONAL CONFIGURATION +// +// - You can suppress implementation of any of the decoders to reduce +// your code footprint by #defining one or more of the following +// symbols before creating the implementation. +// +// STBI_NO_JPEG +// STBI_NO_PNG +// STBI_NO_BMP +// STBI_NO_PSD +// STBI_NO_TGA +// STBI_NO_GIF +// STBI_NO_HDR +// STBI_NO_PIC +// STBI_NO_PNM (.ppm and .pgm) +// +// - You can request *only* certain decoders and suppress all other ones +// (this will be more forward-compatible, as addition of new decoders +// doesn't require you to disable them explicitly): +// +// STBI_ONLY_JPEG +// STBI_ONLY_PNG +// STBI_ONLY_BMP +// STBI_ONLY_PSD +// STBI_ONLY_TGA +// STBI_ONLY_GIF +// STBI_ONLY_HDR +// STBI_ONLY_PIC +// STBI_ONLY_PNM (.ppm and .pgm) +// +// - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still +// want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB +// +// - If you define STBI_MAX_DIMENSIONS, stb_image will reject images greater +// than that size (in either width or height) without further processing. +// This is to let programs in the wild set an upper bound to prevent +// denial-of-service attacks on untrusted data, as one could generate a +// valid image of gigantic dimensions and force stb_image to allocate a +// huge block of memory and spend disproportionate time decoding it. By +// default this is set to (1 << 24), which is 16777216, but that's still +// very big. + +#ifndef STBI_NO_STDIO +#include <stdio.h> +#endif // STBI_NO_STDIO + +#define STBI_VERSION 1 + +enum +{ + STBI_default = 0, // only used for desired_channels + + STBI_grey = 1, + STBI_grey_alpha = 2, + STBI_rgb = 3, + STBI_rgb_alpha = 4 +}; + +#include <stdlib.h> +typedef unsigned char stbi_uc; +typedef unsigned short stbi_us; + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef STBIDEF +#ifdef STB_IMAGE_STATIC +#define STBIDEF static +#else +#define STBIDEF extern +#endif +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// PRIMARY API - works on images of any type +// + +// +// load image by filename, open file, or memory buffer +// + +typedef struct +{ + int (*read) (void *user,char *data,int size); // fill 'data' with 'size' bytes. return number of bytes actually read + void (*skip) (void *user,int n); // skip the next 'n' bytes, or 'unget' the last -n bytes if negative + int (*eof) (void *user); // returns nonzero if we are at end of file/data +} stbi_io_callbacks; + +//////////////////////////////////// +// +// 8-bits-per-channel interface +// + +STBIDEF stbi_uc *stbi_load_from_memory (stbi_uc const *buffer, int len , int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk , void *user, int *x, int *y, int *channels_in_file, int desired_channels); + +#ifndef STBI_NO_STDIO +STBIDEF stbi_uc *stbi_load (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); +// for stbi_load_from_file, file pointer is left pointing immediately after image +#endif + +#ifndef STBI_NO_GIF +STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp); +#endif + +#ifdef STBI_WINDOWS_UTF8 +STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif + +//////////////////////////////////// +// +// 16-bits-per-channel interface +// + +STBIDEF stbi_us *stbi_load_16_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); + +#ifndef STBI_NO_STDIO +STBIDEF stbi_us *stbi_load_16 (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_us *stbi_load_from_file_16(FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); +#endif + +//////////////////////////////////// +// +// float-per-channel interface +// +#ifndef STBI_NO_LINEAR + STBIDEF float *stbi_loadf_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); + STBIDEF float *stbi_loadf_from_callbacks (stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); + + #ifndef STBI_NO_STDIO + STBIDEF float *stbi_loadf (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); + STBIDEF float *stbi_loadf_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); + #endif +#endif + +#ifndef STBI_NO_HDR + STBIDEF void stbi_hdr_to_ldr_gamma(float gamma); + STBIDEF void stbi_hdr_to_ldr_scale(float scale); +#endif // STBI_NO_HDR + +#ifndef STBI_NO_LINEAR + STBIDEF void stbi_ldr_to_hdr_gamma(float gamma); + STBIDEF void stbi_ldr_to_hdr_scale(float scale); +#endif // STBI_NO_LINEAR + +// stbi_is_hdr is always defined, but always returns false if STBI_NO_HDR +STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user); +STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len); +#ifndef STBI_NO_STDIO +STBIDEF int stbi_is_hdr (char const *filename); +STBIDEF int stbi_is_hdr_from_file(FILE *f); +#endif // STBI_NO_STDIO + + +// get a VERY brief reason for failure +// on most compilers (and ALL modern mainstream compilers) this is threadsafe +STBIDEF const char *stbi_failure_reason (void); + +// free the loaded image -- this is just free() +STBIDEF void stbi_image_free (void *retval_from_stbi_load); + +// get image dimensions & components without fully decoding +STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp); +STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp); +STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len); +STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *clbk, void *user); + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_info (char const *filename, int *x, int *y, int *comp); +STBIDEF int stbi_info_from_file (FILE *f, int *x, int *y, int *comp); +STBIDEF int stbi_is_16_bit (char const *filename); +STBIDEF int stbi_is_16_bit_from_file(FILE *f); +#endif + + + +// for image formats that explicitly notate that they have premultiplied alpha, +// we just return the colors as stored in the file. set this flag to force +// unpremultiplication. results are undefined if the unpremultiply overflow. +STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply); + +// indicate whether we should process iphone images back to canonical format, +// or just pass them through "as-is" +STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert); + +// flip the image vertically, so the first pixel in the output array is the bottom left +STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip); + +// as above, but only applies to images loaded on the thread that calls the function +// this function is only available if your compiler supports thread-local variables; +// calling it will fail to link if your compiler doesn't +STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply); +STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert); +STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip); + +// ZLIB client - used by PNG, available for other purposes + +STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen); +STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header); +STBIDEF char *stbi_zlib_decode_malloc(const char *buffer, int len, int *outlen); +STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); + +STBIDEF char *stbi_zlib_decode_noheader_malloc(const char *buffer, int len, int *outlen); +STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); + + +#ifdef __cplusplus +} +#endif + +// +// +//// end header file ///////////////////////////////////////////////////// +#endif // STBI_INCLUDE_STB_IMAGE_H + +#ifdef STB_IMAGE_IMPLEMENTATION + +#if defined(STBI_ONLY_JPEG) || defined(STBI_ONLY_PNG) || defined(STBI_ONLY_BMP) \ + || defined(STBI_ONLY_TGA) || defined(STBI_ONLY_GIF) || defined(STBI_ONLY_PSD) \ + || defined(STBI_ONLY_HDR) || defined(STBI_ONLY_PIC) || defined(STBI_ONLY_PNM) \ + || defined(STBI_ONLY_ZLIB) + #ifndef STBI_ONLY_JPEG + #define STBI_NO_JPEG + #endif + #ifndef STBI_ONLY_PNG + #define STBI_NO_PNG + #endif + #ifndef STBI_ONLY_BMP + #define STBI_NO_BMP + #endif + #ifndef STBI_ONLY_PSD + #define STBI_NO_PSD + #endif + #ifndef STBI_ONLY_TGA + #define STBI_NO_TGA + #endif + #ifndef STBI_ONLY_GIF + #define STBI_NO_GIF + #endif + #ifndef STBI_ONLY_HDR + #define STBI_NO_HDR + #endif + #ifndef STBI_ONLY_PIC + #define STBI_NO_PIC + #endif + #ifndef STBI_ONLY_PNM + #define STBI_NO_PNM + #endif +#endif + +#if defined(STBI_NO_PNG) && !defined(STBI_SUPPORT_ZLIB) && !defined(STBI_NO_ZLIB) +#define STBI_NO_ZLIB +#endif + + +#include <stdarg.h> +#include <stddef.h> // ptrdiff_t on osx +#include <stdlib.h> +#include <string.h> +#include <limits.h> + +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) +#include <math.h> // ldexp, pow +#endif + +#ifndef STBI_NO_STDIO +#include <stdio.h> +#endif + +#ifndef STBI_ASSERT +#include <assert.h> +#define STBI_ASSERT(x) assert(x) +#endif + +#ifdef __cplusplus +#define STBI_EXTERN extern "C" +#else +#define STBI_EXTERN extern +#endif + + +#ifndef _MSC_VER + #ifdef __cplusplus + #define stbi_inline inline + #else + #define stbi_inline + #endif +#else + #define stbi_inline __forceinline +#endif + +#ifndef STBI_NO_THREAD_LOCALS + #if defined(__cplusplus) && __cplusplus >= 201103L + #define STBI_THREAD_LOCAL thread_local + #elif defined(__GNUC__) && __GNUC__ < 5 + #define STBI_THREAD_LOCAL __thread + #elif defined(_MSC_VER) + #define STBI_THREAD_LOCAL __declspec(thread) + #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) + #define STBI_THREAD_LOCAL _Thread_local + #endif + + #ifndef STBI_THREAD_LOCAL + #if defined(__GNUC__) + #define STBI_THREAD_LOCAL __thread + #endif + #endif +#endif + +#if defined(_MSC_VER) || defined(__SYMBIAN32__) +typedef unsigned short stbi__uint16; +typedef signed short stbi__int16; +typedef unsigned int stbi__uint32; +typedef signed int stbi__int32; +#else +#include <stdint.h> +typedef uint16_t stbi__uint16; +typedef int16_t stbi__int16; +typedef uint32_t stbi__uint32; +typedef int32_t stbi__int32; +#endif + +// should produce compiler error if size is wrong +typedef unsigned char validate_uint32[sizeof(stbi__uint32)==4 ? 1 : -1]; + +#ifdef _MSC_VER +#define STBI_NOTUSED(v) (void)(v) +#else +#define STBI_NOTUSED(v) (void)sizeof(v) +#endif + +#ifdef _MSC_VER +#define STBI_HAS_LROTL +#endif + +#ifdef STBI_HAS_LROTL + #define stbi_lrot(x,y) _lrotl(x,y) +#else + #define stbi_lrot(x,y) (((x) << (y)) | ((x) >> (-(y) & 31))) +#endif + +#if defined(STBI_MALLOC) && defined(STBI_FREE) && (defined(STBI_REALLOC) || defined(STBI_REALLOC_SIZED)) +// ok +#elif !defined(STBI_MALLOC) && !defined(STBI_FREE) && !defined(STBI_REALLOC) && !defined(STBI_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBI_MALLOC, STBI_FREE, and STBI_REALLOC (or STBI_REALLOC_SIZED)." +#endif + +#ifndef STBI_MALLOC +#define STBI_MALLOC(sz) malloc(sz) +#define STBI_REALLOC(p,newsz) realloc(p,newsz) +#define STBI_FREE(p) free(p) +#endif + +#ifndef STBI_REALLOC_SIZED +#define STBI_REALLOC_SIZED(p,oldsz,newsz) STBI_REALLOC(p,newsz) +#endif + +// x86/x64 detection +#if defined(__x86_64__) || defined(_M_X64) +#define STBI__X64_TARGET +#elif defined(__i386) || defined(_M_IX86) +#define STBI__X86_TARGET +#endif + +#if defined(__GNUC__) && defined(STBI__X86_TARGET) && !defined(__SSE2__) && !defined(STBI_NO_SIMD) +// gcc doesn't support sse2 intrinsics unless you compile with -msse2, +// which in turn means it gets to use SSE2 everywhere. This is unfortunate, +// but previous attempts to provide the SSE2 functions with runtime +// detection caused numerous issues. The way architecture extensions are +// exposed in GCC/Clang is, sadly, not really suited for one-file libs. +// New behavior: if compiled with -msse2, we use SSE2 without any +// detection; if not, we don't use it at all. +#define STBI_NO_SIMD +#endif + +#if defined(__MINGW32__) && defined(STBI__X86_TARGET) && !defined(STBI_MINGW_ENABLE_SSE2) && !defined(STBI_NO_SIMD) +// Note that __MINGW32__ doesn't actually mean 32-bit, so we have to avoid STBI__X64_TARGET +// +// 32-bit MinGW wants ESP to be 16-byte aligned, but this is not in the +// Windows ABI and VC++ as well as Windows DLLs don't maintain that invariant. +// As a result, enabling SSE2 on 32-bit MinGW is dangerous when not +// simultaneously enabling "-mstackrealign". +// +// See https://github.com/nothings/stb/issues/81 for more information. +// +// So default to no SSE2 on 32-bit MinGW. If you've read this far and added +// -mstackrealign to your build settings, feel free to #define STBI_MINGW_ENABLE_SSE2. +#define STBI_NO_SIMD +#endif + +#if !defined(STBI_NO_SIMD) && (defined(STBI__X86_TARGET) || defined(STBI__X64_TARGET)) +#define STBI_SSE2 +#include <emmintrin.h> + +#ifdef _MSC_VER + +#if _MSC_VER >= 1400 // not VC6 +#include <intrin.h> // __cpuid +static int stbi__cpuid3(void) +{ + int info[4]; + __cpuid(info,1); + return info[3]; +} +#else +static int stbi__cpuid3(void) +{ + int res; + __asm { + mov eax,1 + cpuid + mov res,edx + } + return res; +} +#endif + +#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name + +#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) +static int stbi__sse2_available(void) +{ + int info3 = stbi__cpuid3(); + return ((info3 >> 26) & 1) != 0; +} +#endif + +#else // assume GCC-style if not VC++ +#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) + +#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) +static int stbi__sse2_available(void) +{ + // If we're even attempting to compile this on GCC/Clang, that means + // -msse2 is on, which means the compiler is allowed to use SSE2 + // instructions at will, and so are we. + return 1; +} +#endif + +#endif +#endif + +// ARM NEON +#if defined(STBI_NO_SIMD) && defined(STBI_NEON) +#undef STBI_NEON +#endif + +#ifdef STBI_NEON +#include <arm_neon.h> +#ifdef _MSC_VER +#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name +#else +#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) +#endif +#endif + +#ifndef STBI_SIMD_ALIGN +#define STBI_SIMD_ALIGN(type, name) type name +#endif + +#ifndef STBI_MAX_DIMENSIONS +#define STBI_MAX_DIMENSIONS (1 << 24) +#endif + +/////////////////////////////////////////////// +// +// stbi__context struct and start_xxx functions + +// stbi__context structure is our basic context used by all images, so it +// contains all the IO context, plus some basic image information +typedef struct +{ + stbi__uint32 img_x, img_y; + int img_n, img_out_n; + + stbi_io_callbacks io; + void *io_user_data; + + int read_from_callbacks; + int buflen; + stbi_uc buffer_start[128]; + int callback_already_read; + + stbi_uc *img_buffer, *img_buffer_end; + stbi_uc *img_buffer_original, *img_buffer_original_end; +} stbi__context; + + +static void stbi__refill_buffer(stbi__context *s); + +// initialize a memory-decode context +static void stbi__start_mem(stbi__context *s, stbi_uc const *buffer, int len) +{ + s->io.read = NULL; + s->read_from_callbacks = 0; + s->callback_already_read = 0; + s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer; + s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len; +} + +// initialize a callback-based context +static void stbi__start_callbacks(stbi__context *s, stbi_io_callbacks *c, void *user) +{ + s->io = *c; + s->io_user_data = user; + s->buflen = sizeof(s->buffer_start); + s->read_from_callbacks = 1; + s->callback_already_read = 0; + s->img_buffer = s->img_buffer_original = s->buffer_start; + stbi__refill_buffer(s); + s->img_buffer_original_end = s->img_buffer_end; +} + +#ifndef STBI_NO_STDIO + +static int stbi__stdio_read(void *user, char *data, int size) +{ + return (int) fread(data,1,size,(FILE*) user); +} + +static void stbi__stdio_skip(void *user, int n) +{ + int ch; + fseek((FILE*) user, n, SEEK_CUR); + ch = fgetc((FILE*) user); /* have to read a byte to reset feof()'s flag */ + if (ch != EOF) { + ungetc(ch, (FILE *) user); /* push byte back onto stream if valid. */ + } +} + +static int stbi__stdio_eof(void *user) +{ + return feof((FILE*) user) || ferror((FILE *) user); +} + +static stbi_io_callbacks stbi__stdio_callbacks = +{ + stbi__stdio_read, + stbi__stdio_skip, + stbi__stdio_eof, +}; + +static void stbi__start_file(stbi__context *s, FILE *f) +{ + stbi__start_callbacks(s, &stbi__stdio_callbacks, (void *) f); +} + +//static void stop_file(stbi__context *s) { } + +#endif // !STBI_NO_STDIO + +static void stbi__rewind(stbi__context *s) +{ + // conceptually rewind SHOULD rewind to the beginning of the stream, + // but we just rewind to the beginning of the initial buffer, because + // we only use it after doing 'test', which only ever looks at at most 92 bytes + s->img_buffer = s->img_buffer_original; + s->img_buffer_end = s->img_buffer_original_end; +} + +enum +{ + STBI_ORDER_RGB, + STBI_ORDER_BGR +}; + +typedef struct +{ + int bits_per_channel; + int num_channels; + int channel_order; +} stbi__result_info; + +#ifndef STBI_NO_JPEG +static int stbi__jpeg_test(stbi__context *s); +static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PNG +static int stbi__png_test(stbi__context *s); +static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__png_is16(stbi__context *s); +#endif + +#ifndef STBI_NO_BMP +static int stbi__bmp_test(stbi__context *s); +static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_TGA +static int stbi__tga_test(stbi__context *s); +static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__tga_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PSD +static int stbi__psd_test(stbi__context *s); +static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc); +static int stbi__psd_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__psd_is16(stbi__context *s); +#endif + +#ifndef STBI_NO_HDR +static int stbi__hdr_test(stbi__context *s); +static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PIC +static int stbi__pic_test(stbi__context *s); +static void *stbi__pic_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_GIF +static int stbi__gif_test(stbi__context *s); +static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp); +static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PNM +static int stbi__pnm_test(stbi__context *s); +static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__pnm_is16(stbi__context *s); +#endif + +static +#ifdef STBI_THREAD_LOCAL +STBI_THREAD_LOCAL +#endif +const char *stbi__g_failure_reason; + +STBIDEF const char *stbi_failure_reason(void) +{ + return stbi__g_failure_reason; +} + +#ifndef STBI_NO_FAILURE_STRINGS +static int stbi__err(const char *str) +{ + stbi__g_failure_reason = str; + return 0; +} +#endif + +static void *stbi__malloc(size_t size) +{ + return STBI_MALLOC(size); +} + +// stb_image uses ints pervasively, including for offset calculations. +// therefore the largest decoded image size we can support with the +// current code, even on 64-bit targets, is INT_MAX. this is not a +// significant limitation for the intended use case. +// +// we do, however, need to make sure our size calculations don't +// overflow. hence a few helper functions for size calculations that +// multiply integers together, making sure that they're non-negative +// and no overflow occurs. + +// return 1 if the sum is valid, 0 on overflow. +// negative terms are considered invalid. +static int stbi__addsizes_valid(int a, int b) +{ + if (b < 0) return 0; + // now 0 <= b <= INT_MAX, hence also + // 0 <= INT_MAX - b <= INTMAX. + // And "a + b <= INT_MAX" (which might overflow) is the + // same as a <= INT_MAX - b (no overflow) + return a <= INT_MAX - b; +} + +// returns 1 if the product is valid, 0 on overflow. +// negative factors are considered invalid. +static int stbi__mul2sizes_valid(int a, int b) +{ + if (a < 0 || b < 0) return 0; + if (b == 0) return 1; // mul-by-0 is always safe + // portable way to check for no overflows in a*b + return a <= INT_MAX/b; +} + +#if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR) +// returns 1 if "a*b + add" has no negative terms/factors and doesn't overflow +static int stbi__mad2sizes_valid(int a, int b, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__addsizes_valid(a*b, add); +} +#endif + +// returns 1 if "a*b*c + add" has no negative terms/factors and doesn't overflow +static int stbi__mad3sizes_valid(int a, int b, int c, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && + stbi__addsizes_valid(a*b*c, add); +} + +// returns 1 if "a*b*c*d + add" has no negative terms/factors and doesn't overflow +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) +static int stbi__mad4sizes_valid(int a, int b, int c, int d, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && + stbi__mul2sizes_valid(a*b*c, d) && stbi__addsizes_valid(a*b*c*d, add); +} +#endif + +#if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR) +// mallocs with size overflow checking +static void *stbi__malloc_mad2(int a, int b, int add) +{ + if (!stbi__mad2sizes_valid(a, b, add)) return NULL; + return stbi__malloc(a*b + add); +} +#endif + +static void *stbi__malloc_mad3(int a, int b, int c, int add) +{ + if (!stbi__mad3sizes_valid(a, b, c, add)) return NULL; + return stbi__malloc(a*b*c + add); +} + +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) +static void *stbi__malloc_mad4(int a, int b, int c, int d, int add) +{ + if (!stbi__mad4sizes_valid(a, b, c, d, add)) return NULL; + return stbi__malloc(a*b*c*d + add); +} +#endif + +// returns 1 if the sum of two signed ints is valid (between -2^31 and 2^31-1 inclusive), 0 on overflow. +static int stbi__addints_valid(int a, int b) +{ + if ((a >= 0) != (b >= 0)) return 1; // a and b have different signs, so no overflow + if (a < 0 && b < 0) return a >= INT_MIN - b; // same as a + b >= INT_MIN; INT_MIN - b cannot overflow since b < 0. + return a <= INT_MAX - b; +} + +// returns 1 if the product of two ints fits in a signed short, 0 on overflow. +static int stbi__mul2shorts_valid(int a, int b) +{ + if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow + if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid + if (b < 0) return a <= SHRT_MIN / b; // same as a * b >= SHRT_MIN + return a >= SHRT_MIN / b; +} + +// stbi__err - error +// stbi__errpf - error returning pointer to float +// stbi__errpuc - error returning pointer to unsigned char + +#ifdef STBI_NO_FAILURE_STRINGS + #define stbi__err(x,y) 0 +#elif defined(STBI_FAILURE_USERMSG) + #define stbi__err(x,y) stbi__err(y) +#else + #define stbi__err(x,y) stbi__err(x) +#endif + +#define stbi__errpf(x,y) ((float *)(size_t) (stbi__err(x,y)?NULL:NULL)) +#define stbi__errpuc(x,y) ((unsigned char *)(size_t) (stbi__err(x,y)?NULL:NULL)) + +STBIDEF void stbi_image_free(void *retval_from_stbi_load) +{ + STBI_FREE(retval_from_stbi_load); +} + +#ifndef STBI_NO_LINEAR +static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp); +#endif + +#ifndef STBI_NO_HDR +static stbi_uc *stbi__hdr_to_ldr(float *data, int x, int y, int comp); +#endif + +static int stbi__vertically_flip_on_load_global = 0; + +STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip) +{ + stbi__vertically_flip_on_load_global = flag_true_if_should_flip; +} + +#ifndef STBI_THREAD_LOCAL +#define stbi__vertically_flip_on_load stbi__vertically_flip_on_load_global +#else +static STBI_THREAD_LOCAL int stbi__vertically_flip_on_load_local, stbi__vertically_flip_on_load_set; + +STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip) +{ + stbi__vertically_flip_on_load_local = flag_true_if_should_flip; + stbi__vertically_flip_on_load_set = 1; +} + +#define stbi__vertically_flip_on_load (stbi__vertically_flip_on_load_set \ + ? stbi__vertically_flip_on_load_local \ + : stbi__vertically_flip_on_load_global) +#endif // STBI_THREAD_LOCAL + +static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) +{ + memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields + ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed + ri->channel_order = STBI_ORDER_RGB; // all current input & output are this, but this is here so we can add BGR order + ri->num_channels = 0; + + // test the formats with a very explicit header first (at least a FOURCC + // or distinctive magic number first) + #ifndef STBI_NO_PNG + if (stbi__png_test(s)) return stbi__png_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_BMP + if (stbi__bmp_test(s)) return stbi__bmp_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_GIF + if (stbi__gif_test(s)) return stbi__gif_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_PSD + if (stbi__psd_test(s)) return stbi__psd_load(s,x,y,comp,req_comp, ri, bpc); + #else + STBI_NOTUSED(bpc); + #endif + #ifndef STBI_NO_PIC + if (stbi__pic_test(s)) return stbi__pic_load(s,x,y,comp,req_comp, ri); + #endif + + // then the formats that can end up attempting to load with just 1 or 2 + // bytes matching expectations; these are prone to false positives, so + // try them later + #ifndef STBI_NO_JPEG + if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_PNM + if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri); + #endif + + #ifndef STBI_NO_HDR + if (stbi__hdr_test(s)) { + float *hdr = stbi__hdr_load(s, x,y,comp,req_comp, ri); + return stbi__hdr_to_ldr(hdr, *x, *y, req_comp ? req_comp : *comp); + } + #endif + + #ifndef STBI_NO_TGA + // test tga last because it's a crappy test! + if (stbi__tga_test(s)) + return stbi__tga_load(s,x,y,comp,req_comp, ri); + #endif + + return stbi__errpuc("unknown image type", "Image not of any known type, or corrupt"); +} + +static stbi_uc *stbi__convert_16_to_8(stbi__uint16 *orig, int w, int h, int channels) +{ + int i; + int img_len = w * h * channels; + stbi_uc *reduced; + + reduced = (stbi_uc *) stbi__malloc(img_len); + if (reduced == NULL) return stbi__errpuc("outofmem", "Out of memory"); + + for (i = 0; i < img_len; ++i) + reduced[i] = (stbi_uc)((orig[i] >> 8) & 0xFF); // top half of each byte is sufficient approx of 16->8 bit scaling + + STBI_FREE(orig); + return reduced; +} + +static stbi__uint16 *stbi__convert_8_to_16(stbi_uc *orig, int w, int h, int channels) +{ + int i; + int img_len = w * h * channels; + stbi__uint16 *enlarged; + + enlarged = (stbi__uint16 *) stbi__malloc(img_len*2); + if (enlarged == NULL) return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); + + for (i = 0; i < img_len; ++i) + enlarged[i] = (stbi__uint16)((orig[i] << 8) + orig[i]); // replicate to high and low byte, maps 0->0, 255->0xffff + + STBI_FREE(orig); + return enlarged; +} + +static void stbi__vertical_flip(void *image, int w, int h, int bytes_per_pixel) +{ + int row; + size_t bytes_per_row = (size_t)w * bytes_per_pixel; + stbi_uc temp[2048]; + stbi_uc *bytes = (stbi_uc *)image; + + for (row = 0; row < (h>>1); row++) { + stbi_uc *row0 = bytes + row*bytes_per_row; + stbi_uc *row1 = bytes + (h - row - 1)*bytes_per_row; + // swap row0 with row1 + size_t bytes_left = bytes_per_row; + while (bytes_left) { + size_t bytes_copy = (bytes_left < sizeof(temp)) ? bytes_left : sizeof(temp); + memcpy(temp, row0, bytes_copy); + memcpy(row0, row1, bytes_copy); + memcpy(row1, temp, bytes_copy); + row0 += bytes_copy; + row1 += bytes_copy; + bytes_left -= bytes_copy; + } + } +} + +#ifndef STBI_NO_GIF +static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int bytes_per_pixel) +{ + int slice; + int slice_size = w * h * bytes_per_pixel; + + stbi_uc *bytes = (stbi_uc *)image; + for (slice = 0; slice < z; ++slice) { + stbi__vertical_flip(bytes, w, h, bytes_per_pixel); + bytes += slice_size; + } +} +#endif + +static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + stbi__result_info ri; + void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8); + + if (result == NULL) + return NULL; + + // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. + STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); + + if (ri.bits_per_channel != 8) { + result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp); + ri.bits_per_channel = 8; + } + + // @TODO: move stbi__convert_format to here + + if (stbi__vertically_flip_on_load) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi_uc)); + } + + return (unsigned char *) result; +} + +static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + stbi__result_info ri; + void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16); + + if (result == NULL) + return NULL; + + // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. + STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); + + if (ri.bits_per_channel != 16) { + result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp); + ri.bits_per_channel = 16; + } + + // @TODO: move stbi__convert_format16 to here + // @TODO: special case RGB-to-Y (and RGBA-to-YA) for 8-bit-to-16-bit case to keep more precision + + if (stbi__vertically_flip_on_load) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi__uint16)); + } + + return (stbi__uint16 *) result; +} + +#if !defined(STBI_NO_HDR) && !defined(STBI_NO_LINEAR) +static void stbi__float_postprocess(float *result, int *x, int *y, int *comp, int req_comp) +{ + if (stbi__vertically_flip_on_load && result != NULL) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(float)); + } +} +#endif + +#ifndef STBI_NO_STDIO + +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) +STBI_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBI_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); +#endif + +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) +STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbi__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) + return 0; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + + +STBIDEF stbi_uc *stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + FILE *f = stbi__fopen(filename, "rb"); + unsigned char *result; + if (!f) return stbi__errpuc("can't fopen", "Unable to open file"); + result = stbi_load_from_file(f,x,y,comp,req_comp); + fclose(f); + return result; +} + +STBIDEF stbi_uc *stbi_load_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + unsigned char *result; + stbi__context s; + stbi__start_file(&s,f); + result = stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); + if (result) { + // need to 'unget' all the characters in the IO buffer + fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); + } + return result; +} + +STBIDEF stbi__uint16 *stbi_load_from_file_16(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + stbi__uint16 *result; + stbi__context s; + stbi__start_file(&s,f); + result = stbi__load_and_postprocess_16bit(&s,x,y,comp,req_comp); + if (result) { + // need to 'unget' all the characters in the IO buffer + fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); + } + return result; +} + +STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + FILE *f = stbi__fopen(filename, "rb"); + stbi__uint16 *result; + if (!f) return (stbi_us *) stbi__errpuc("can't fopen", "Unable to open file"); + result = stbi_load_from_file_16(f,x,y,comp,req_comp); + fclose(f); + return result; +} + + +#endif //!STBI_NO_STDIO + +STBIDEF stbi_us *stbi_load_16_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); +} + +STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *)clbk, user); + return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); +} + +STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); +} + +STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); +} + +#ifndef STBI_NO_GIF +STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp) +{ + unsigned char *result; + stbi__context s; + stbi__start_mem(&s,buffer,len); + + result = (unsigned char*) stbi__load_gif_main(&s, delays, x, y, z, comp, req_comp); + if (stbi__vertically_flip_on_load) { + stbi__vertical_flip_slices( result, *x, *y, *z, *comp ); + } + + return result; +} +#endif + +#ifndef STBI_NO_LINEAR +static float *stbi__loadf_main(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + unsigned char *data; + #ifndef STBI_NO_HDR + if (stbi__hdr_test(s)) { + stbi__result_info ri; + float *hdr_data = stbi__hdr_load(s,x,y,comp,req_comp, &ri); + if (hdr_data) + stbi__float_postprocess(hdr_data,x,y,comp,req_comp); + return hdr_data; + } + #endif + data = stbi__load_and_postprocess_8bit(s, x, y, comp, req_comp); + if (data) + return stbi__ldr_to_hdr(data, *x, *y, req_comp ? req_comp : *comp); + return stbi__errpf("unknown image type", "Image not of any known type, or corrupt"); +} + +STBIDEF float *stbi_loadf_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__loadf_main(&s,x,y,comp,req_comp); +} + +STBIDEF float *stbi_loadf_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi__loadf_main(&s,x,y,comp,req_comp); +} + +#ifndef STBI_NO_STDIO +STBIDEF float *stbi_loadf(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + float *result; + FILE *f = stbi__fopen(filename, "rb"); + if (!f) return stbi__errpf("can't fopen", "Unable to open file"); + result = stbi_loadf_from_file(f,x,y,comp,req_comp); + fclose(f); + return result; +} + +STBIDEF float *stbi_loadf_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_file(&s,f); + return stbi__loadf_main(&s,x,y,comp,req_comp); +} +#endif // !STBI_NO_STDIO + +#endif // !STBI_NO_LINEAR + +// these is-hdr-or-not is defined independent of whether STBI_NO_LINEAR is +// defined, for API simplicity; if STBI_NO_LINEAR is defined, it always +// reports false! + +STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len) +{ + #ifndef STBI_NO_HDR + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__hdr_test(&s); + #else + STBI_NOTUSED(buffer); + STBI_NOTUSED(len); + return 0; + #endif +} + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_is_hdr (char const *filename) +{ + FILE *f = stbi__fopen(filename, "rb"); + int result=0; + if (f) { + result = stbi_is_hdr_from_file(f); + fclose(f); + } + return result; +} + +STBIDEF int stbi_is_hdr_from_file(FILE *f) +{ + #ifndef STBI_NO_HDR + long pos = ftell(f); + int res; + stbi__context s; + stbi__start_file(&s,f); + res = stbi__hdr_test(&s); + fseek(f, pos, SEEK_SET); + return res; + #else + STBI_NOTUSED(f); + return 0; + #endif +} +#endif // !STBI_NO_STDIO + +STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user) +{ + #ifndef STBI_NO_HDR + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi__hdr_test(&s); + #else + STBI_NOTUSED(clbk); + STBI_NOTUSED(user); + return 0; + #endif +} + +#ifndef STBI_NO_LINEAR +static float stbi__l2h_gamma=2.2f, stbi__l2h_scale=1.0f; + +STBIDEF void stbi_ldr_to_hdr_gamma(float gamma) { stbi__l2h_gamma = gamma; } +STBIDEF void stbi_ldr_to_hdr_scale(float scale) { stbi__l2h_scale = scale; } +#endif + +static float stbi__h2l_gamma_i=1.0f/2.2f, stbi__h2l_scale_i=1.0f; + +STBIDEF void stbi_hdr_to_ldr_gamma(float gamma) { stbi__h2l_gamma_i = 1/gamma; } +STBIDEF void stbi_hdr_to_ldr_scale(float scale) { stbi__h2l_scale_i = 1/scale; } + + +////////////////////////////////////////////////////////////////////////////// +// +// Common code used by all image loaders +// + +enum +{ + STBI__SCAN_load=0, + STBI__SCAN_type, + STBI__SCAN_header +}; + +static void stbi__refill_buffer(stbi__context *s) +{ + int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen); + s->callback_already_read += (int) (s->img_buffer - s->img_buffer_original); + if (n == 0) { + // at end of file, treat same as if from memory, but need to handle case + // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file + s->read_from_callbacks = 0; + s->img_buffer = s->buffer_start; + s->img_buffer_end = s->buffer_start+1; + *s->img_buffer = 0; + } else { + s->img_buffer = s->buffer_start; + s->img_buffer_end = s->buffer_start + n; + } +} + +stbi_inline static stbi_uc stbi__get8(stbi__context *s) +{ + if (s->img_buffer < s->img_buffer_end) + return *s->img_buffer++; + if (s->read_from_callbacks) { + stbi__refill_buffer(s); + return *s->img_buffer++; + } + return 0; +} + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_HDR) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) +// nothing +#else +stbi_inline static int stbi__at_eof(stbi__context *s) +{ + if (s->io.read) { + if (!(s->io.eof)(s->io_user_data)) return 0; + // if feof() is true, check if buffer = end + // special case: we've only got the special 0 character at the end + if (s->read_from_callbacks == 0) return 1; + } + + return s->img_buffer >= s->img_buffer_end; +} +#endif + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) +// nothing +#else +static void stbi__skip(stbi__context *s, int n) +{ + if (n == 0) return; // already there! + if (n < 0) { + s->img_buffer = s->img_buffer_end; + return; + } + if (s->io.read) { + int blen = (int) (s->img_buffer_end - s->img_buffer); + if (blen < n) { + s->img_buffer = s->img_buffer_end; + (s->io.skip)(s->io_user_data, n - blen); + return; + } + } + s->img_buffer += n; +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_TGA) && defined(STBI_NO_HDR) && defined(STBI_NO_PNM) +// nothing +#else +static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n) +{ + if (s->io.read) { + int blen = (int) (s->img_buffer_end - s->img_buffer); + if (blen < n) { + int res, count; + + memcpy(buffer, s->img_buffer, blen); + + count = (s->io.read)(s->io_user_data, (char*) buffer + blen, n - blen); + res = (count == (n-blen)); + s->img_buffer = s->img_buffer_end; + return res; + } + } + + if (s->img_buffer+n <= s->img_buffer_end) { + memcpy(buffer, s->img_buffer, n); + s->img_buffer += n; + return 1; + } else + return 0; +} +#endif + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC) +// nothing +#else +static int stbi__get16be(stbi__context *s) +{ + int z = stbi__get8(s); + return (z << 8) + stbi__get8(s); +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC) +// nothing +#else +static stbi__uint32 stbi__get32be(stbi__context *s) +{ + stbi__uint32 z = stbi__get16be(s); + return (z << 16) + stbi__get16be(s); +} +#endif + +#if defined(STBI_NO_BMP) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) +// nothing +#else +static int stbi__get16le(stbi__context *s) +{ + int z = stbi__get8(s); + return z + (stbi__get8(s) << 8); +} +#endif + +#ifndef STBI_NO_BMP +static stbi__uint32 stbi__get32le(stbi__context *s) +{ + stbi__uint32 z = stbi__get16le(s); + z += (stbi__uint32)stbi__get16le(s) << 16; + return z; +} +#endif + +#define STBI__BYTECAST(x) ((stbi_uc) ((x) & 255)) // truncate int to byte without warnings + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) +// nothing +#else +////////////////////////////////////////////////////////////////////////////// +// +// generic converter from built-in img_n to req_comp +// individual types do this automatically as much as possible (e.g. jpeg +// does all cases internally since it needs to colorspace convert anyway, +// and it never has alpha, so very few cases ). png can automatically +// interleave an alpha=255 channel, but falls back to this for other cases +// +// assume data buffer is malloced, so malloc a new one and free that one +// only failure mode is malloc failing + +static stbi_uc stbi__compute_y(int r, int g, int b) +{ + return (stbi_uc) (((r*77) + (g*150) + (29*b)) >> 8); +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) +// nothing +#else +static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int req_comp, unsigned int x, unsigned int y) +{ + int i,j; + unsigned char *good; + + if (req_comp == img_n) return data; + STBI_ASSERT(req_comp >= 1 && req_comp <= 4); + + good = (unsigned char *) stbi__malloc_mad3(req_comp, x, y, 0); + if (good == NULL) { + STBI_FREE(data); + return stbi__errpuc("outofmem", "Out of memory"); + } + + for (j=0; j < (int) y; ++j) { + unsigned char *src = data + j * x * img_n ; + unsigned char *dest = good + j * x * req_comp; + + #define STBI__COMBO(a,b) ((a)*8+(b)) + #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) + // convert source image with img_n components to one with req_comp components; + // avoid switch per pixel, so use switch per scanline and massive macros + switch (STBI__COMBO(img_n, req_comp)) { + STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=255; } break; + STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=255; } break; + STBI__CASE(2,1) { dest[0]=src[0]; } break; + STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; + STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=255; } break; + STBI__CASE(3,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; + STBI__CASE(3,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = 255; } break; + STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; + STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break; + STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; + default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return stbi__errpuc("unsupported", "Unsupported format conversion"); + } + #undef STBI__CASE + } + + STBI_FREE(data); + return good; +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) +// nothing +#else +static stbi__uint16 stbi__compute_y_16(int r, int g, int b) +{ + return (stbi__uint16) (((r*77) + (g*150) + (29*b)) >> 8); +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) +// nothing +#else +static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int req_comp, unsigned int x, unsigned int y) +{ + int i,j; + stbi__uint16 *good; + + if (req_comp == img_n) return data; + STBI_ASSERT(req_comp >= 1 && req_comp <= 4); + + good = (stbi__uint16 *) stbi__malloc(req_comp * x * y * 2); + if (good == NULL) { + STBI_FREE(data); + return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); + } + + for (j=0; j < (int) y; ++j) { + stbi__uint16 *src = data + j * x * img_n ; + stbi__uint16 *dest = good + j * x * req_comp; + + #define STBI__COMBO(a,b) ((a)*8+(b)) + #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) + // convert source image with img_n components to one with req_comp components; + // avoid switch per pixel, so use switch per scanline and massive macros + switch (STBI__COMBO(img_n, req_comp)) { + STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=0xffff; } break; + STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=0xffff; } break; + STBI__CASE(2,1) { dest[0]=src[0]; } break; + STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; + STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=0xffff; } break; + STBI__CASE(3,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; + STBI__CASE(3,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = 0xffff; } break; + STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; + STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break; + STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; + default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return (stbi__uint16*) stbi__errpuc("unsupported", "Unsupported format conversion"); + } + #undef STBI__CASE + } + + STBI_FREE(data); + return good; +} +#endif + +#ifndef STBI_NO_LINEAR +static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp) +{ + int i,k,n; + float *output; + if (!data) return NULL; + output = (float *) stbi__malloc_mad4(x, y, comp, sizeof(float), 0); + if (output == NULL) { STBI_FREE(data); return stbi__errpf("outofmem", "Out of memory"); } + // compute number of non-alpha components + if (comp & 1) n = comp; else n = comp-1; + for (i=0; i < x*y; ++i) { + for (k=0; k < n; ++k) { + output[i*comp + k] = (float) (pow(data[i*comp+k]/255.0f, stbi__l2h_gamma) * stbi__l2h_scale); + } + } + if (n < comp) { + for (i=0; i < x*y; ++i) { + output[i*comp + n] = data[i*comp + n]/255.0f; + } + } + STBI_FREE(data); + return output; +} +#endif + +#ifndef STBI_NO_HDR +#define stbi__float2int(x) ((int) (x)) +static stbi_uc *stbi__hdr_to_ldr(float *data, int x, int y, int comp) +{ + int i,k,n; + stbi_uc *output; + if (!data) return NULL; + output = (stbi_uc *) stbi__malloc_mad3(x, y, comp, 0); + if (output == NULL) { STBI_FREE(data); return stbi__errpuc("outofmem", "Out of memory"); } + // compute number of non-alpha components + if (comp & 1) n = comp; else n = comp-1; + for (i=0; i < x*y; ++i) { + for (k=0; k < n; ++k) { + float z = (float) pow(data[i*comp+k]*stbi__h2l_scale_i, stbi__h2l_gamma_i) * 255 + 0.5f; + if (z < 0) z = 0; + if (z > 255) z = 255; + output[i*comp + k] = (stbi_uc) stbi__float2int(z); + } + if (k < comp) { + float z = data[i*comp+k] * 255 + 0.5f; + if (z < 0) z = 0; + if (z > 255) z = 255; + output[i*comp + k] = (stbi_uc) stbi__float2int(z); + } + } + STBI_FREE(data); + return output; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// "baseline" JPEG/JFIF decoder +// +// simple implementation +// - doesn't support delayed output of y-dimension +// - simple interface (only one output format: 8-bit interleaved RGB) +// - doesn't try to recover corrupt jpegs +// - doesn't allow partial loading, loading multiple at once +// - still fast on x86 (copying globals into locals doesn't help x86) +// - allocates lots of intermediate memory (full size of all components) +// - non-interleaved case requires this anyway +// - allows good upsampling (see next) +// high-quality +// - upsampled channels are bilinearly interpolated, even across blocks +// - quality integer IDCT derived from IJG's 'slow' +// performance +// - fast huffman; reasonable integer IDCT +// - some SIMD kernels for common paths on targets with SSE2/NEON +// - uses a lot of intermediate memory, could cache poorly + +#ifndef STBI_NO_JPEG + +// huffman decoding acceleration +#define FAST_BITS 9 // larger handles more cases; smaller stomps less cache + +typedef struct +{ + stbi_uc fast[1 << FAST_BITS]; + // weirdly, repacking this into AoS is a 10% speed loss, instead of a win + stbi__uint16 code[256]; + stbi_uc values[256]; + stbi_uc size[257]; + unsigned int maxcode[18]; + int delta[17]; // old 'firstsymbol' - old 'firstcode' +} stbi__huffman; + +typedef struct +{ + stbi__context *s; + stbi__huffman huff_dc[4]; + stbi__huffman huff_ac[4]; + stbi__uint16 dequant[4][64]; + stbi__int16 fast_ac[4][1 << FAST_BITS]; + +// sizes for components, interleaved MCUs + int img_h_max, img_v_max; + int img_mcu_x, img_mcu_y; + int img_mcu_w, img_mcu_h; + +// definition of jpeg image component + struct + { + int id; + int h,v; + int tq; + int hd,ha; + int dc_pred; + + int x,y,w2,h2; + stbi_uc *data; + void *raw_data, *raw_coeff; + stbi_uc *linebuf; + short *coeff; // progressive only + int coeff_w, coeff_h; // number of 8x8 coefficient blocks + } img_comp[4]; + + stbi__uint32 code_buffer; // jpeg entropy-coded buffer + int code_bits; // number of valid bits + unsigned char marker; // marker seen while filling entropy buffer + int nomore; // flag if we saw a marker so must stop + + int progressive; + int spec_start; + int spec_end; + int succ_high; + int succ_low; + int eob_run; + int jfif; + int app14_color_transform; // Adobe APP14 tag + int rgb; + + int scan_n, order[4]; + int restart_interval, todo; + +// kernels + void (*idct_block_kernel)(stbi_uc *out, int out_stride, short data[64]); + void (*YCbCr_to_RGB_kernel)(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step); + stbi_uc *(*resample_row_hv_2_kernel)(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs); +} stbi__jpeg; + +static int stbi__build_huffman(stbi__huffman *h, int *count) +{ + int i,j,k=0; + unsigned int code; + // build size list for each symbol (from JPEG spec) + for (i=0; i < 16; ++i) { + for (j=0; j < count[i]; ++j) { + h->size[k++] = (stbi_uc) (i+1); + if(k >= 257) return stbi__err("bad size list","Corrupt JPEG"); + } + } + h->size[k] = 0; + + // compute actual symbols (from jpeg spec) + code = 0; + k = 0; + for(j=1; j <= 16; ++j) { + // compute delta to add to code to compute symbol id + h->delta[j] = k - code; + if (h->size[k] == j) { + while (h->size[k] == j) + h->code[k++] = (stbi__uint16) (code++); + if (code-1 >= (1u << j)) return stbi__err("bad code lengths","Corrupt JPEG"); + } + // compute largest code + 1 for this size, preshifted as needed later + h->maxcode[j] = code << (16-j); + code <<= 1; + } + h->maxcode[j] = 0xffffffff; + + // build non-spec acceleration table; 255 is flag for not-accelerated + memset(h->fast, 255, 1 << FAST_BITS); + for (i=0; i < k; ++i) { + int s = h->size[i]; + if (s <= FAST_BITS) { + int c = h->code[i] << (FAST_BITS-s); + int m = 1 << (FAST_BITS-s); + for (j=0; j < m; ++j) { + h->fast[c+j] = (stbi_uc) i; + } + } + } + return 1; +} + +// build a table that decodes both magnitude and value of small ACs in +// one go. +static void stbi__build_fast_ac(stbi__int16 *fast_ac, stbi__huffman *h) +{ + int i; + for (i=0; i < (1 << FAST_BITS); ++i) { + stbi_uc fast = h->fast[i]; + fast_ac[i] = 0; + if (fast < 255) { + int rs = h->values[fast]; + int run = (rs >> 4) & 15; + int magbits = rs & 15; + int len = h->size[fast]; + + if (magbits && len + magbits <= FAST_BITS) { + // magnitude code followed by receive_extend code + int k = ((i << len) & ((1 << FAST_BITS) - 1)) >> (FAST_BITS - magbits); + int m = 1 << (magbits - 1); + if (k < m) k += (~0U << magbits) + 1; + // if the result is small enough, we can fit it in fast_ac table + if (k >= -128 && k <= 127) + fast_ac[i] = (stbi__int16) ((k * 256) + (run * 16) + (len + magbits)); + } + } + } +} + +static void stbi__grow_buffer_unsafe(stbi__jpeg *j) +{ + do { + unsigned int b = j->nomore ? 0 : stbi__get8(j->s); + if (b == 0xff) { + int c = stbi__get8(j->s); + while (c == 0xff) c = stbi__get8(j->s); // consume fill bytes + if (c != 0) { + j->marker = (unsigned char) c; + j->nomore = 1; + return; + } + } + j->code_buffer |= b << (24 - j->code_bits); + j->code_bits += 8; + } while (j->code_bits <= 24); +} + +// (1 << n) - 1 +static const stbi__uint32 stbi__bmask[17]={0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535}; + +// decode a jpeg huffman value from the bitstream +stbi_inline static int stbi__jpeg_huff_decode(stbi__jpeg *j, stbi__huffman *h) +{ + unsigned int temp; + int c,k; + + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + + // look at the top FAST_BITS and determine what symbol ID it is, + // if the code is <= FAST_BITS + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + k = h->fast[c]; + if (k < 255) { + int s = h->size[k]; + if (s > j->code_bits) + return -1; + j->code_buffer <<= s; + j->code_bits -= s; + return h->values[k]; + } + + // naive test is to shift the code_buffer down so k bits are + // valid, then test against maxcode. To speed this up, we've + // preshifted maxcode left so that it has (16-k) 0s at the + // end; in other words, regardless of the number of bits, it + // wants to be compared against something shifted to have 16; + // that way we don't need to shift inside the loop. + temp = j->code_buffer >> 16; + for (k=FAST_BITS+1 ; ; ++k) + if (temp < h->maxcode[k]) + break; + if (k == 17) { + // error! code not found + j->code_bits -= 16; + return -1; + } + + if (k > j->code_bits) + return -1; + + // convert the huffman code to the symbol id + c = ((j->code_buffer >> (32 - k)) & stbi__bmask[k]) + h->delta[k]; + if(c < 0 || c >= 256) // symbol id out of bounds! + return -1; + STBI_ASSERT((((j->code_buffer) >> (32 - h->size[c])) & stbi__bmask[h->size[c]]) == h->code[c]); + + // convert the id to a symbol + j->code_bits -= k; + j->code_buffer <<= k; + return h->values[c]; +} + +// bias[n] = (-1<<n) + 1 +static const int stbi__jbias[16] = {0,-1,-3,-7,-15,-31,-63,-127,-255,-511,-1023,-2047,-4095,-8191,-16383,-32767}; + +// combined JPEG 'receive' and JPEG 'extend', since baseline +// always extends everything it receives. +stbi_inline static int stbi__extend_receive(stbi__jpeg *j, int n) +{ + unsigned int k; + int sgn; + if (j->code_bits < n) stbi__grow_buffer_unsafe(j); + if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing + + sgn = j->code_buffer >> 31; // sign bit always in MSB; 0 if MSB clear (positive), 1 if MSB set (negative) + k = stbi_lrot(j->code_buffer, n); + j->code_buffer = k & ~stbi__bmask[n]; + k &= stbi__bmask[n]; + j->code_bits -= n; + return k + (stbi__jbias[n] & (sgn - 1)); +} + +// get some unsigned bits +stbi_inline static int stbi__jpeg_get_bits(stbi__jpeg *j, int n) +{ + unsigned int k; + if (j->code_bits < n) stbi__grow_buffer_unsafe(j); + if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing + k = stbi_lrot(j->code_buffer, n); + j->code_buffer = k & ~stbi__bmask[n]; + k &= stbi__bmask[n]; + j->code_bits -= n; + return k; +} + +stbi_inline static int stbi__jpeg_get_bit(stbi__jpeg *j) +{ + unsigned int k; + if (j->code_bits < 1) stbi__grow_buffer_unsafe(j); + if (j->code_bits < 1) return 0; // ran out of bits from stream, return 0s intead of continuing + k = j->code_buffer; + j->code_buffer <<= 1; + --j->code_bits; + return k & 0x80000000; +} + +// given a value that's at position X in the zigzag stream, +// where does it appear in the 8x8 matrix coded as row-major? +static const stbi_uc stbi__jpeg_dezigzag[64+15] = +{ + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + // let corrupt input sample past end + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63 +}; + +// decode one 64-entry block-- +static int stbi__jpeg_decode_block(stbi__jpeg *j, short data[64], stbi__huffman *hdc, stbi__huffman *hac, stbi__int16 *fac, int b, stbi__uint16 *dequant) +{ + int diff,dc,k; + int t; + + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + t = stbi__jpeg_huff_decode(j, hdc); + if (t < 0 || t > 15) return stbi__err("bad huffman code","Corrupt JPEG"); + + // 0 all the ac values now so we can do it 32-bits at a time + memset(data,0,64*sizeof(data[0])); + + diff = t ? stbi__extend_receive(j, t) : 0; + if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta","Corrupt JPEG"); + dc = j->img_comp[b].dc_pred + diff; + j->img_comp[b].dc_pred = dc; + if (!stbi__mul2shorts_valid(dc, dequant[0])) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + data[0] = (short) (dc * dequant[0]); + + // decode AC components, see JPEG spec + k = 1; + do { + unsigned int zig; + int c,r,s; + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + r = fac[c]; + if (r) { // fast-AC path + k += (r >> 4) & 15; // run + s = r & 15; // combined length + if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); + j->code_buffer <<= s; + j->code_bits -= s; + // decode into unzigzag'd location + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) ((r >> 8) * dequant[zig]); + } else { + int rs = stbi__jpeg_huff_decode(j, hac); + if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (rs != 0xf0) break; // end block + k += 16; + } else { + k += r; + // decode into unzigzag'd location + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) (stbi__extend_receive(j,s) * dequant[zig]); + } + } + } while (k < 64); + return 1; +} + +static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__huffman *hdc, int b) +{ + int diff,dc; + int t; + if (j->spec_end != 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + + if (j->succ_high == 0) { + // first scan for DC coefficient, must be first + memset(data,0,64*sizeof(data[0])); // 0 all the ac values now + t = stbi__jpeg_huff_decode(j, hdc); + if (t < 0 || t > 15) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + diff = t ? stbi__extend_receive(j, t) : 0; + + if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta", "Corrupt JPEG"); + dc = j->img_comp[b].dc_pred + diff; + j->img_comp[b].dc_pred = dc; + if (!stbi__mul2shorts_valid(dc, 1 << j->succ_low)) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + data[0] = (short) (dc * (1 << j->succ_low)); + } else { + // refinement scan for DC coefficient + if (stbi__jpeg_get_bit(j)) + data[0] += (short) (1 << j->succ_low); + } + return 1; +} + +// @OPTIMIZE: store non-zigzagged during the decode passes, +// and only de-zigzag when dequantizing +static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__huffman *hac, stbi__int16 *fac) +{ + int k; + if (j->spec_start == 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + + if (j->succ_high == 0) { + int shift = j->succ_low; + + if (j->eob_run) { + --j->eob_run; + return 1; + } + + k = j->spec_start; + do { + unsigned int zig; + int c,r,s; + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + r = fac[c]; + if (r) { // fast-AC path + k += (r >> 4) & 15; // run + s = r & 15; // combined length + if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); + j->code_buffer <<= s; + j->code_bits -= s; + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) ((r >> 8) * (1 << shift)); + } else { + int rs = stbi__jpeg_huff_decode(j, hac); + if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (r < 15) { + j->eob_run = (1 << r); + if (r) + j->eob_run += stbi__jpeg_get_bits(j, r); + --j->eob_run; + break; + } + k += 16; + } else { + k += r; + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) (stbi__extend_receive(j,s) * (1 << shift)); + } + } + } while (k <= j->spec_end); + } else { + // refinement scan for these AC coefficients + + short bit = (short) (1 << j->succ_low); + + if (j->eob_run) { + --j->eob_run; + for (k = j->spec_start; k <= j->spec_end; ++k) { + short *p = &data[stbi__jpeg_dezigzag[k]]; + if (*p != 0) + if (stbi__jpeg_get_bit(j)) + if ((*p & bit)==0) { + if (*p > 0) + *p += bit; + else + *p -= bit; + } + } + } else { + k = j->spec_start; + do { + int r,s; + int rs = stbi__jpeg_huff_decode(j, hac); // @OPTIMIZE see if we can use the fast path here, advance-by-r is so slow, eh + if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (r < 15) { + j->eob_run = (1 << r) - 1; + if (r) + j->eob_run += stbi__jpeg_get_bits(j, r); + r = 64; // force end of block + } else { + // r=15 s=0 should write 16 0s, so we just do + // a run of 15 0s and then write s (which is 0), + // so we don't have to do anything special here + } + } else { + if (s != 1) return stbi__err("bad huffman code", "Corrupt JPEG"); + // sign bit + if (stbi__jpeg_get_bit(j)) + s = bit; + else + s = -bit; + } + + // advance by r + while (k <= j->spec_end) { + short *p = &data[stbi__jpeg_dezigzag[k++]]; + if (*p != 0) { + if (stbi__jpeg_get_bit(j)) + if ((*p & bit)==0) { + if (*p > 0) + *p += bit; + else + *p -= bit; + } + } else { + if (r == 0) { + *p = (short) s; + break; + } + --r; + } + } + } while (k <= j->spec_end); + } + } + return 1; +} + +// take a -128..127 value and stbi__clamp it and convert to 0..255 +stbi_inline static stbi_uc stbi__clamp(int x) +{ + // trick to use a single test to catch both cases + if ((unsigned int) x > 255) { + if (x < 0) return 0; + if (x > 255) return 255; + } + return (stbi_uc) x; +} + +#define stbi__f2f(x) ((int) (((x) * 4096 + 0.5))) +#define stbi__fsh(x) ((x) * 4096) + +// derived from jidctint -- DCT_ISLOW +#define STBI__IDCT_1D(s0,s1,s2,s3,s4,s5,s6,s7) \ + int t0,t1,t2,t3,p1,p2,p3,p4,p5,x0,x1,x2,x3; \ + p2 = s2; \ + p3 = s6; \ + p1 = (p2+p3) * stbi__f2f(0.5411961f); \ + t2 = p1 + p3*stbi__f2f(-1.847759065f); \ + t3 = p1 + p2*stbi__f2f( 0.765366865f); \ + p2 = s0; \ + p3 = s4; \ + t0 = stbi__fsh(p2+p3); \ + t1 = stbi__fsh(p2-p3); \ + x0 = t0+t3; \ + x3 = t0-t3; \ + x1 = t1+t2; \ + x2 = t1-t2; \ + t0 = s7; \ + t1 = s5; \ + t2 = s3; \ + t3 = s1; \ + p3 = t0+t2; \ + p4 = t1+t3; \ + p1 = t0+t3; \ + p2 = t1+t2; \ + p5 = (p3+p4)*stbi__f2f( 1.175875602f); \ + t0 = t0*stbi__f2f( 0.298631336f); \ + t1 = t1*stbi__f2f( 2.053119869f); \ + t2 = t2*stbi__f2f( 3.072711026f); \ + t3 = t3*stbi__f2f( 1.501321110f); \ + p1 = p5 + p1*stbi__f2f(-0.899976223f); \ + p2 = p5 + p2*stbi__f2f(-2.562915447f); \ + p3 = p3*stbi__f2f(-1.961570560f); \ + p4 = p4*stbi__f2f(-0.390180644f); \ + t3 += p1+p4; \ + t2 += p2+p3; \ + t1 += p2+p4; \ + t0 += p1+p3; + +static void stbi__idct_block(stbi_uc *out, int out_stride, short data[64]) +{ + int i,val[64],*v=val; + stbi_uc *o; + short *d = data; + + // columns + for (i=0; i < 8; ++i,++d, ++v) { + // if all zeroes, shortcut -- this avoids dequantizing 0s and IDCTing + if (d[ 8]==0 && d[16]==0 && d[24]==0 && d[32]==0 + && d[40]==0 && d[48]==0 && d[56]==0) { + // no shortcut 0 seconds + // (1|2|3|4|5|6|7)==0 0 seconds + // all separate -0.047 seconds + // 1 && 2|3 && 4|5 && 6|7: -0.047 seconds + int dcterm = d[0]*4; + v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = dcterm; + } else { + STBI__IDCT_1D(d[ 0],d[ 8],d[16],d[24],d[32],d[40],d[48],d[56]) + // constants scaled things up by 1<<12; let's bring them back + // down, but keep 2 extra bits of precision + x0 += 512; x1 += 512; x2 += 512; x3 += 512; + v[ 0] = (x0+t3) >> 10; + v[56] = (x0-t3) >> 10; + v[ 8] = (x1+t2) >> 10; + v[48] = (x1-t2) >> 10; + v[16] = (x2+t1) >> 10; + v[40] = (x2-t1) >> 10; + v[24] = (x3+t0) >> 10; + v[32] = (x3-t0) >> 10; + } + } + + for (i=0, v=val, o=out; i < 8; ++i,v+=8,o+=out_stride) { + // no fast case since the first 1D IDCT spread components out + STBI__IDCT_1D(v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7]) + // constants scaled things up by 1<<12, plus we had 1<<2 from first + // loop, plus horizontal and vertical each scale by sqrt(8) so together + // we've got an extra 1<<3, so 1<<17 total we need to remove. + // so we want to round that, which means adding 0.5 * 1<<17, + // aka 65536. Also, we'll end up with -128 to 127 that we want + // to encode as 0..255 by adding 128, so we'll add that before the shift + x0 += 65536 + (128<<17); + x1 += 65536 + (128<<17); + x2 += 65536 + (128<<17); + x3 += 65536 + (128<<17); + // tried computing the shifts into temps, or'ing the temps to see + // if any were out of range, but that was slower + o[0] = stbi__clamp((x0+t3) >> 17); + o[7] = stbi__clamp((x0-t3) >> 17); + o[1] = stbi__clamp((x1+t2) >> 17); + o[6] = stbi__clamp((x1-t2) >> 17); + o[2] = stbi__clamp((x2+t1) >> 17); + o[5] = stbi__clamp((x2-t1) >> 17); + o[3] = stbi__clamp((x3+t0) >> 17); + o[4] = stbi__clamp((x3-t0) >> 17); + } +} + +#ifdef STBI_SSE2 +// sse2 integer IDCT. not the fastest possible implementation but it +// produces bit-identical results to the generic C version so it's +// fully "transparent". +static void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64]) +{ + // This is constructed to match our regular (generic) integer IDCT exactly. + __m128i row0, row1, row2, row3, row4, row5, row6, row7; + __m128i tmp; + + // dot product constant: even elems=x, odd elems=y + #define dct_const(x,y) _mm_setr_epi16((x),(y),(x),(y),(x),(y),(x),(y)) + + // out(0) = c0[even]*x + c0[odd]*y (c0, x, y 16-bit, out 32-bit) + // out(1) = c1[even]*x + c1[odd]*y + #define dct_rot(out0,out1, x,y,c0,c1) \ + __m128i c0##lo = _mm_unpacklo_epi16((x),(y)); \ + __m128i c0##hi = _mm_unpackhi_epi16((x),(y)); \ + __m128i out0##_l = _mm_madd_epi16(c0##lo, c0); \ + __m128i out0##_h = _mm_madd_epi16(c0##hi, c0); \ + __m128i out1##_l = _mm_madd_epi16(c0##lo, c1); \ + __m128i out1##_h = _mm_madd_epi16(c0##hi, c1) + + // out = in << 12 (in 16-bit, out 32-bit) + #define dct_widen(out, in) \ + __m128i out##_l = _mm_srai_epi32(_mm_unpacklo_epi16(_mm_setzero_si128(), (in)), 4); \ + __m128i out##_h = _mm_srai_epi32(_mm_unpackhi_epi16(_mm_setzero_si128(), (in)), 4) + + // wide add + #define dct_wadd(out, a, b) \ + __m128i out##_l = _mm_add_epi32(a##_l, b##_l); \ + __m128i out##_h = _mm_add_epi32(a##_h, b##_h) + + // wide sub + #define dct_wsub(out, a, b) \ + __m128i out##_l = _mm_sub_epi32(a##_l, b##_l); \ + __m128i out##_h = _mm_sub_epi32(a##_h, b##_h) + + // butterfly a/b, add bias, then shift by "s" and pack + #define dct_bfly32o(out0, out1, a,b,bias,s) \ + { \ + __m128i abiased_l = _mm_add_epi32(a##_l, bias); \ + __m128i abiased_h = _mm_add_epi32(a##_h, bias); \ + dct_wadd(sum, abiased, b); \ + dct_wsub(dif, abiased, b); \ + out0 = _mm_packs_epi32(_mm_srai_epi32(sum_l, s), _mm_srai_epi32(sum_h, s)); \ + out1 = _mm_packs_epi32(_mm_srai_epi32(dif_l, s), _mm_srai_epi32(dif_h, s)); \ + } + + // 8-bit interleave step (for transposes) + #define dct_interleave8(a, b) \ + tmp = a; \ + a = _mm_unpacklo_epi8(a, b); \ + b = _mm_unpackhi_epi8(tmp, b) + + // 16-bit interleave step (for transposes) + #define dct_interleave16(a, b) \ + tmp = a; \ + a = _mm_unpacklo_epi16(a, b); \ + b = _mm_unpackhi_epi16(tmp, b) + + #define dct_pass(bias,shift) \ + { \ + /* even part */ \ + dct_rot(t2e,t3e, row2,row6, rot0_0,rot0_1); \ + __m128i sum04 = _mm_add_epi16(row0, row4); \ + __m128i dif04 = _mm_sub_epi16(row0, row4); \ + dct_widen(t0e, sum04); \ + dct_widen(t1e, dif04); \ + dct_wadd(x0, t0e, t3e); \ + dct_wsub(x3, t0e, t3e); \ + dct_wadd(x1, t1e, t2e); \ + dct_wsub(x2, t1e, t2e); \ + /* odd part */ \ + dct_rot(y0o,y2o, row7,row3, rot2_0,rot2_1); \ + dct_rot(y1o,y3o, row5,row1, rot3_0,rot3_1); \ + __m128i sum17 = _mm_add_epi16(row1, row7); \ + __m128i sum35 = _mm_add_epi16(row3, row5); \ + dct_rot(y4o,y5o, sum17,sum35, rot1_0,rot1_1); \ + dct_wadd(x4, y0o, y4o); \ + dct_wadd(x5, y1o, y5o); \ + dct_wadd(x6, y2o, y5o); \ + dct_wadd(x7, y3o, y4o); \ + dct_bfly32o(row0,row7, x0,x7,bias,shift); \ + dct_bfly32o(row1,row6, x1,x6,bias,shift); \ + dct_bfly32o(row2,row5, x2,x5,bias,shift); \ + dct_bfly32o(row3,row4, x3,x4,bias,shift); \ + } + + __m128i rot0_0 = dct_const(stbi__f2f(0.5411961f), stbi__f2f(0.5411961f) + stbi__f2f(-1.847759065f)); + __m128i rot0_1 = dct_const(stbi__f2f(0.5411961f) + stbi__f2f( 0.765366865f), stbi__f2f(0.5411961f)); + __m128i rot1_0 = dct_const(stbi__f2f(1.175875602f) + stbi__f2f(-0.899976223f), stbi__f2f(1.175875602f)); + __m128i rot1_1 = dct_const(stbi__f2f(1.175875602f), stbi__f2f(1.175875602f) + stbi__f2f(-2.562915447f)); + __m128i rot2_0 = dct_const(stbi__f2f(-1.961570560f) + stbi__f2f( 0.298631336f), stbi__f2f(-1.961570560f)); + __m128i rot2_1 = dct_const(stbi__f2f(-1.961570560f), stbi__f2f(-1.961570560f) + stbi__f2f( 3.072711026f)); + __m128i rot3_0 = dct_const(stbi__f2f(-0.390180644f) + stbi__f2f( 2.053119869f), stbi__f2f(-0.390180644f)); + __m128i rot3_1 = dct_const(stbi__f2f(-0.390180644f), stbi__f2f(-0.390180644f) + stbi__f2f( 1.501321110f)); + + // rounding biases in column/row passes, see stbi__idct_block for explanation. + __m128i bias_0 = _mm_set1_epi32(512); + __m128i bias_1 = _mm_set1_epi32(65536 + (128<<17)); + + // load + row0 = _mm_load_si128((const __m128i *) (data + 0*8)); + row1 = _mm_load_si128((const __m128i *) (data + 1*8)); + row2 = _mm_load_si128((const __m128i *) (data + 2*8)); + row3 = _mm_load_si128((const __m128i *) (data + 3*8)); + row4 = _mm_load_si128((const __m128i *) (data + 4*8)); + row5 = _mm_load_si128((const __m128i *) (data + 5*8)); + row6 = _mm_load_si128((const __m128i *) (data + 6*8)); + row7 = _mm_load_si128((const __m128i *) (data + 7*8)); + + // column pass + dct_pass(bias_0, 10); + + { + // 16bit 8x8 transpose pass 1 + dct_interleave16(row0, row4); + dct_interleave16(row1, row5); + dct_interleave16(row2, row6); + dct_interleave16(row3, row7); + + // transpose pass 2 + dct_interleave16(row0, row2); + dct_interleave16(row1, row3); + dct_interleave16(row4, row6); + dct_interleave16(row5, row7); + + // transpose pass 3 + dct_interleave16(row0, row1); + dct_interleave16(row2, row3); + dct_interleave16(row4, row5); + dct_interleave16(row6, row7); + } + + // row pass + dct_pass(bias_1, 17); + + { + // pack + __m128i p0 = _mm_packus_epi16(row0, row1); // a0a1a2a3...a7b0b1b2b3...b7 + __m128i p1 = _mm_packus_epi16(row2, row3); + __m128i p2 = _mm_packus_epi16(row4, row5); + __m128i p3 = _mm_packus_epi16(row6, row7); + + // 8bit 8x8 transpose pass 1 + dct_interleave8(p0, p2); // a0e0a1e1... + dct_interleave8(p1, p3); // c0g0c1g1... + + // transpose pass 2 + dct_interleave8(p0, p1); // a0c0e0g0... + dct_interleave8(p2, p3); // b0d0f0h0... + + // transpose pass 3 + dct_interleave8(p0, p2); // a0b0c0d0... + dct_interleave8(p1, p3); // a4b4c4d4... + + // store + _mm_storel_epi64((__m128i *) out, p0); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p0, 0x4e)); out += out_stride; + _mm_storel_epi64((__m128i *) out, p2); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p2, 0x4e)); out += out_stride; + _mm_storel_epi64((__m128i *) out, p1); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p1, 0x4e)); out += out_stride; + _mm_storel_epi64((__m128i *) out, p3); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p3, 0x4e)); + } + +#undef dct_const +#undef dct_rot +#undef dct_widen +#undef dct_wadd +#undef dct_wsub +#undef dct_bfly32o +#undef dct_interleave8 +#undef dct_interleave16 +#undef dct_pass +} + +#endif // STBI_SSE2 + +#ifdef STBI_NEON + +// NEON integer IDCT. should produce bit-identical +// results to the generic C version. +static void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64]) +{ + int16x8_t row0, row1, row2, row3, row4, row5, row6, row7; + + int16x4_t rot0_0 = vdup_n_s16(stbi__f2f(0.5411961f)); + int16x4_t rot0_1 = vdup_n_s16(stbi__f2f(-1.847759065f)); + int16x4_t rot0_2 = vdup_n_s16(stbi__f2f( 0.765366865f)); + int16x4_t rot1_0 = vdup_n_s16(stbi__f2f( 1.175875602f)); + int16x4_t rot1_1 = vdup_n_s16(stbi__f2f(-0.899976223f)); + int16x4_t rot1_2 = vdup_n_s16(stbi__f2f(-2.562915447f)); + int16x4_t rot2_0 = vdup_n_s16(stbi__f2f(-1.961570560f)); + int16x4_t rot2_1 = vdup_n_s16(stbi__f2f(-0.390180644f)); + int16x4_t rot3_0 = vdup_n_s16(stbi__f2f( 0.298631336f)); + int16x4_t rot3_1 = vdup_n_s16(stbi__f2f( 2.053119869f)); + int16x4_t rot3_2 = vdup_n_s16(stbi__f2f( 3.072711026f)); + int16x4_t rot3_3 = vdup_n_s16(stbi__f2f( 1.501321110f)); + +#define dct_long_mul(out, inq, coeff) \ + int32x4_t out##_l = vmull_s16(vget_low_s16(inq), coeff); \ + int32x4_t out##_h = vmull_s16(vget_high_s16(inq), coeff) + +#define dct_long_mac(out, acc, inq, coeff) \ + int32x4_t out##_l = vmlal_s16(acc##_l, vget_low_s16(inq), coeff); \ + int32x4_t out##_h = vmlal_s16(acc##_h, vget_high_s16(inq), coeff) + +#define dct_widen(out, inq) \ + int32x4_t out##_l = vshll_n_s16(vget_low_s16(inq), 12); \ + int32x4_t out##_h = vshll_n_s16(vget_high_s16(inq), 12) + +// wide add +#define dct_wadd(out, a, b) \ + int32x4_t out##_l = vaddq_s32(a##_l, b##_l); \ + int32x4_t out##_h = vaddq_s32(a##_h, b##_h) + +// wide sub +#define dct_wsub(out, a, b) \ + int32x4_t out##_l = vsubq_s32(a##_l, b##_l); \ + int32x4_t out##_h = vsubq_s32(a##_h, b##_h) + +// butterfly a/b, then shift using "shiftop" by "s" and pack +#define dct_bfly32o(out0,out1, a,b,shiftop,s) \ + { \ + dct_wadd(sum, a, b); \ + dct_wsub(dif, a, b); \ + out0 = vcombine_s16(shiftop(sum_l, s), shiftop(sum_h, s)); \ + out1 = vcombine_s16(shiftop(dif_l, s), shiftop(dif_h, s)); \ + } + +#define dct_pass(shiftop, shift) \ + { \ + /* even part */ \ + int16x8_t sum26 = vaddq_s16(row2, row6); \ + dct_long_mul(p1e, sum26, rot0_0); \ + dct_long_mac(t2e, p1e, row6, rot0_1); \ + dct_long_mac(t3e, p1e, row2, rot0_2); \ + int16x8_t sum04 = vaddq_s16(row0, row4); \ + int16x8_t dif04 = vsubq_s16(row0, row4); \ + dct_widen(t0e, sum04); \ + dct_widen(t1e, dif04); \ + dct_wadd(x0, t0e, t3e); \ + dct_wsub(x3, t0e, t3e); \ + dct_wadd(x1, t1e, t2e); \ + dct_wsub(x2, t1e, t2e); \ + /* odd part */ \ + int16x8_t sum15 = vaddq_s16(row1, row5); \ + int16x8_t sum17 = vaddq_s16(row1, row7); \ + int16x8_t sum35 = vaddq_s16(row3, row5); \ + int16x8_t sum37 = vaddq_s16(row3, row7); \ + int16x8_t sumodd = vaddq_s16(sum17, sum35); \ + dct_long_mul(p5o, sumodd, rot1_0); \ + dct_long_mac(p1o, p5o, sum17, rot1_1); \ + dct_long_mac(p2o, p5o, sum35, rot1_2); \ + dct_long_mul(p3o, sum37, rot2_0); \ + dct_long_mul(p4o, sum15, rot2_1); \ + dct_wadd(sump13o, p1o, p3o); \ + dct_wadd(sump24o, p2o, p4o); \ + dct_wadd(sump23o, p2o, p3o); \ + dct_wadd(sump14o, p1o, p4o); \ + dct_long_mac(x4, sump13o, row7, rot3_0); \ + dct_long_mac(x5, sump24o, row5, rot3_1); \ + dct_long_mac(x6, sump23o, row3, rot3_2); \ + dct_long_mac(x7, sump14o, row1, rot3_3); \ + dct_bfly32o(row0,row7, x0,x7,shiftop,shift); \ + dct_bfly32o(row1,row6, x1,x6,shiftop,shift); \ + dct_bfly32o(row2,row5, x2,x5,shiftop,shift); \ + dct_bfly32o(row3,row4, x3,x4,shiftop,shift); \ + } + + // load + row0 = vld1q_s16(data + 0*8); + row1 = vld1q_s16(data + 1*8); + row2 = vld1q_s16(data + 2*8); + row3 = vld1q_s16(data + 3*8); + row4 = vld1q_s16(data + 4*8); + row5 = vld1q_s16(data + 5*8); + row6 = vld1q_s16(data + 6*8); + row7 = vld1q_s16(data + 7*8); + + // add DC bias + row0 = vaddq_s16(row0, vsetq_lane_s16(1024, vdupq_n_s16(0), 0)); + + // column pass + dct_pass(vrshrn_n_s32, 10); + + // 16bit 8x8 transpose + { +// these three map to a single VTRN.16, VTRN.32, and VSWP, respectively. +// whether compilers actually get this is another story, sadly. +#define dct_trn16(x, y) { int16x8x2_t t = vtrnq_s16(x, y); x = t.val[0]; y = t.val[1]; } +#define dct_trn32(x, y) { int32x4x2_t t = vtrnq_s32(vreinterpretq_s32_s16(x), vreinterpretq_s32_s16(y)); x = vreinterpretq_s16_s32(t.val[0]); y = vreinterpretq_s16_s32(t.val[1]); } +#define dct_trn64(x, y) { int16x8_t x0 = x; int16x8_t y0 = y; x = vcombine_s16(vget_low_s16(x0), vget_low_s16(y0)); y = vcombine_s16(vget_high_s16(x0), vget_high_s16(y0)); } + + // pass 1 + dct_trn16(row0, row1); // a0b0a2b2a4b4a6b6 + dct_trn16(row2, row3); + dct_trn16(row4, row5); + dct_trn16(row6, row7); + + // pass 2 + dct_trn32(row0, row2); // a0b0c0d0a4b4c4d4 + dct_trn32(row1, row3); + dct_trn32(row4, row6); + dct_trn32(row5, row7); + + // pass 3 + dct_trn64(row0, row4); // a0b0c0d0e0f0g0h0 + dct_trn64(row1, row5); + dct_trn64(row2, row6); + dct_trn64(row3, row7); + +#undef dct_trn16 +#undef dct_trn32 +#undef dct_trn64 + } + + // row pass + // vrshrn_n_s32 only supports shifts up to 16, we need + // 17. so do a non-rounding shift of 16 first then follow + // up with a rounding shift by 1. + dct_pass(vshrn_n_s32, 16); + + { + // pack and round + uint8x8_t p0 = vqrshrun_n_s16(row0, 1); + uint8x8_t p1 = vqrshrun_n_s16(row1, 1); + uint8x8_t p2 = vqrshrun_n_s16(row2, 1); + uint8x8_t p3 = vqrshrun_n_s16(row3, 1); + uint8x8_t p4 = vqrshrun_n_s16(row4, 1); + uint8x8_t p5 = vqrshrun_n_s16(row5, 1); + uint8x8_t p6 = vqrshrun_n_s16(row6, 1); + uint8x8_t p7 = vqrshrun_n_s16(row7, 1); + + // again, these can translate into one instruction, but often don't. +#define dct_trn8_8(x, y) { uint8x8x2_t t = vtrn_u8(x, y); x = t.val[0]; y = t.val[1]; } +#define dct_trn8_16(x, y) { uint16x4x2_t t = vtrn_u16(vreinterpret_u16_u8(x), vreinterpret_u16_u8(y)); x = vreinterpret_u8_u16(t.val[0]); y = vreinterpret_u8_u16(t.val[1]); } +#define dct_trn8_32(x, y) { uint32x2x2_t t = vtrn_u32(vreinterpret_u32_u8(x), vreinterpret_u32_u8(y)); x = vreinterpret_u8_u32(t.val[0]); y = vreinterpret_u8_u32(t.val[1]); } + + // sadly can't use interleaved stores here since we only write + // 8 bytes to each scan line! + + // 8x8 8-bit transpose pass 1 + dct_trn8_8(p0, p1); + dct_trn8_8(p2, p3); + dct_trn8_8(p4, p5); + dct_trn8_8(p6, p7); + + // pass 2 + dct_trn8_16(p0, p2); + dct_trn8_16(p1, p3); + dct_trn8_16(p4, p6); + dct_trn8_16(p5, p7); + + // pass 3 + dct_trn8_32(p0, p4); + dct_trn8_32(p1, p5); + dct_trn8_32(p2, p6); + dct_trn8_32(p3, p7); + + // store + vst1_u8(out, p0); out += out_stride; + vst1_u8(out, p1); out += out_stride; + vst1_u8(out, p2); out += out_stride; + vst1_u8(out, p3); out += out_stride; + vst1_u8(out, p4); out += out_stride; + vst1_u8(out, p5); out += out_stride; + vst1_u8(out, p6); out += out_stride; + vst1_u8(out, p7); + +#undef dct_trn8_8 +#undef dct_trn8_16 +#undef dct_trn8_32 + } + +#undef dct_long_mul +#undef dct_long_mac +#undef dct_widen +#undef dct_wadd +#undef dct_wsub +#undef dct_bfly32o +#undef dct_pass +} + +#endif // STBI_NEON + +#define STBI__MARKER_none 0xff +// if there's a pending marker from the entropy stream, return that +// otherwise, fetch from the stream and get a marker. if there's no +// marker, return 0xff, which is never a valid marker value +static stbi_uc stbi__get_marker(stbi__jpeg *j) +{ + stbi_uc x; + if (j->marker != STBI__MARKER_none) { x = j->marker; j->marker = STBI__MARKER_none; return x; } + x = stbi__get8(j->s); + if (x != 0xff) return STBI__MARKER_none; + while (x == 0xff) + x = stbi__get8(j->s); // consume repeated 0xff fill bytes + return x; +} + +// in each scan, we'll have scan_n components, and the order +// of the components is specified by order[] +#define STBI__RESTART(x) ((x) >= 0xd0 && (x) <= 0xd7) + +// after a restart interval, stbi__jpeg_reset the entropy decoder and +// the dc prediction +static void stbi__jpeg_reset(stbi__jpeg *j) +{ + j->code_bits = 0; + j->code_buffer = 0; + j->nomore = 0; + j->img_comp[0].dc_pred = j->img_comp[1].dc_pred = j->img_comp[2].dc_pred = j->img_comp[3].dc_pred = 0; + j->marker = STBI__MARKER_none; + j->todo = j->restart_interval ? j->restart_interval : 0x7fffffff; + j->eob_run = 0; + // no more than 1<<31 MCUs if no restart_interal? that's plenty safe, + // since we don't even allow 1<<30 pixels +} + +static int stbi__parse_entropy_coded_data(stbi__jpeg *z) +{ + stbi__jpeg_reset(z); + if (!z->progressive) { + if (z->scan_n == 1) { + int i,j; + STBI_SIMD_ALIGN(short, data[64]); + int n = z->order[0]; + // non-interleaved data, we just need to process one block at a time, + // in trivial scanline order + // number of blocks to do just depends on how many actual "pixels" this + // component has, independent of interleaved MCU blocking and such + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; + z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); + // every data block is an MCU, so countdown the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + // if it's NOT a restart, then just bail, so we get corrupt data + // rather than no data + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } else { // interleaved + int i,j,k,x,y; + STBI_SIMD_ALIGN(short, data[64]); + for (j=0; j < z->img_mcu_y; ++j) { + for (i=0; i < z->img_mcu_x; ++i) { + // scan an interleaved mcu... process scan_n components in order + for (k=0; k < z->scan_n; ++k) { + int n = z->order[k]; + // scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (y=0; y < z->img_comp[n].v; ++y) { + for (x=0; x < z->img_comp[n].h; ++x) { + int x2 = (i*z->img_comp[n].h + x)*8; + int y2 = (j*z->img_comp[n].v + y)*8; + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; + z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*y2+x2, z->img_comp[n].w2, data); + } + } + } + // after all interleaved components, that's an interleaved MCU, + // so now count down the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + } else { + if (z->scan_n == 1) { + int i,j; + int n = z->order[0]; + // non-interleaved data, we just need to process one block at a time, + // in trivial scanline order + // number of blocks to do just depends on how many actual "pixels" this + // component has, independent of interleaved MCU blocking and such + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); + if (z->spec_start == 0) { + if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) + return 0; + } else { + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block_prog_ac(z, data, &z->huff_ac[ha], z->fast_ac[ha])) + return 0; + } + // every data block is an MCU, so countdown the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } else { // interleaved + int i,j,k,x,y; + for (j=0; j < z->img_mcu_y; ++j) { + for (i=0; i < z->img_mcu_x; ++i) { + // scan an interleaved mcu... process scan_n components in order + for (k=0; k < z->scan_n; ++k) { + int n = z->order[k]; + // scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (y=0; y < z->img_comp[n].v; ++y) { + for (x=0; x < z->img_comp[n].h; ++x) { + int x2 = (i*z->img_comp[n].h + x); + int y2 = (j*z->img_comp[n].v + y); + short *data = z->img_comp[n].coeff + 64 * (x2 + y2 * z->img_comp[n].coeff_w); + if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) + return 0; + } + } + } + // after all interleaved components, that's an interleaved MCU, + // so now count down the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + } +} + +static void stbi__jpeg_dequantize(short *data, stbi__uint16 *dequant) +{ + int i; + for (i=0; i < 64; ++i) + data[i] *= dequant[i]; +} + +static void stbi__jpeg_finish(stbi__jpeg *z) +{ + if (z->progressive) { + // dequantize and idct the data + int i,j,n; + for (n=0; n < z->s->img_n; ++n) { + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); + stbi__jpeg_dequantize(data, z->dequant[z->img_comp[n].tq]); + z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); + } + } + } + } +} + +static int stbi__process_marker(stbi__jpeg *z, int m) +{ + int L; + switch (m) { + case STBI__MARKER_none: // no marker found + return stbi__err("expected marker","Corrupt JPEG"); + + case 0xDD: // DRI - specify restart interval + if (stbi__get16be(z->s) != 4) return stbi__err("bad DRI len","Corrupt JPEG"); + z->restart_interval = stbi__get16be(z->s); + return 1; + + case 0xDB: // DQT - define quantization table + L = stbi__get16be(z->s)-2; + while (L > 0) { + int q = stbi__get8(z->s); + int p = q >> 4, sixteen = (p != 0); + int t = q & 15,i; + if (p != 0 && p != 1) return stbi__err("bad DQT type","Corrupt JPEG"); + if (t > 3) return stbi__err("bad DQT table","Corrupt JPEG"); + + for (i=0; i < 64; ++i) + z->dequant[t][stbi__jpeg_dezigzag[i]] = (stbi__uint16)(sixteen ? stbi__get16be(z->s) : stbi__get8(z->s)); + L -= (sixteen ? 129 : 65); + } + return L==0; + + case 0xC4: // DHT - define huffman table + L = stbi__get16be(z->s)-2; + while (L > 0) { + stbi_uc *v; + int sizes[16],i,n=0; + int q = stbi__get8(z->s); + int tc = q >> 4; + int th = q & 15; + if (tc > 1 || th > 3) return stbi__err("bad DHT header","Corrupt JPEG"); + for (i=0; i < 16; ++i) { + sizes[i] = stbi__get8(z->s); + n += sizes[i]; + } + if(n > 256) return stbi__err("bad DHT header","Corrupt JPEG"); // Loop over i < n would write past end of values! + L -= 17; + if (tc == 0) { + if (!stbi__build_huffman(z->huff_dc+th, sizes)) return 0; + v = z->huff_dc[th].values; + } else { + if (!stbi__build_huffman(z->huff_ac+th, sizes)) return 0; + v = z->huff_ac[th].values; + } + for (i=0; i < n; ++i) + v[i] = stbi__get8(z->s); + if (tc != 0) + stbi__build_fast_ac(z->fast_ac[th], z->huff_ac + th); + L -= n; + } + return L==0; + } + + // check for comment block or APP blocks + if ((m >= 0xE0 && m <= 0xEF) || m == 0xFE) { + L = stbi__get16be(z->s); + if (L < 2) { + if (m == 0xFE) + return stbi__err("bad COM len","Corrupt JPEG"); + else + return stbi__err("bad APP len","Corrupt JPEG"); + } + L -= 2; + + if (m == 0xE0 && L >= 5) { // JFIF APP0 segment + static const unsigned char tag[5] = {'J','F','I','F','\0'}; + int ok = 1; + int i; + for (i=0; i < 5; ++i) + if (stbi__get8(z->s) != tag[i]) + ok = 0; + L -= 5; + if (ok) + z->jfif = 1; + } else if (m == 0xEE && L >= 12) { // Adobe APP14 segment + static const unsigned char tag[6] = {'A','d','o','b','e','\0'}; + int ok = 1; + int i; + for (i=0; i < 6; ++i) + if (stbi__get8(z->s) != tag[i]) + ok = 0; + L -= 6; + if (ok) { + stbi__get8(z->s); // version + stbi__get16be(z->s); // flags0 + stbi__get16be(z->s); // flags1 + z->app14_color_transform = stbi__get8(z->s); // color transform + L -= 6; + } + } + + stbi__skip(z->s, L); + return 1; + } + + return stbi__err("unknown marker","Corrupt JPEG"); +} + +// after we see SOS +static int stbi__process_scan_header(stbi__jpeg *z) +{ + int i; + int Ls = stbi__get16be(z->s); + z->scan_n = stbi__get8(z->s); + if (z->scan_n < 1 || z->scan_n > 4 || z->scan_n > (int) z->s->img_n) return stbi__err("bad SOS component count","Corrupt JPEG"); + if (Ls != 6+2*z->scan_n) return stbi__err("bad SOS len","Corrupt JPEG"); + for (i=0; i < z->scan_n; ++i) { + int id = stbi__get8(z->s), which; + int q = stbi__get8(z->s); + for (which = 0; which < z->s->img_n; ++which) + if (z->img_comp[which].id == id) + break; + if (which == z->s->img_n) return 0; // no match + z->img_comp[which].hd = q >> 4; if (z->img_comp[which].hd > 3) return stbi__err("bad DC huff","Corrupt JPEG"); + z->img_comp[which].ha = q & 15; if (z->img_comp[which].ha > 3) return stbi__err("bad AC huff","Corrupt JPEG"); + z->order[i] = which; + } + + { + int aa; + z->spec_start = stbi__get8(z->s); + z->spec_end = stbi__get8(z->s); // should be 63, but might be 0 + aa = stbi__get8(z->s); + z->succ_high = (aa >> 4); + z->succ_low = (aa & 15); + if (z->progressive) { + if (z->spec_start > 63 || z->spec_end > 63 || z->spec_start > z->spec_end || z->succ_high > 13 || z->succ_low > 13) + return stbi__err("bad SOS", "Corrupt JPEG"); + } else { + if (z->spec_start != 0) return stbi__err("bad SOS","Corrupt JPEG"); + if (z->succ_high != 0 || z->succ_low != 0) return stbi__err("bad SOS","Corrupt JPEG"); + z->spec_end = 63; + } + } + + return 1; +} + +static int stbi__free_jpeg_components(stbi__jpeg *z, int ncomp, int why) +{ + int i; + for (i=0; i < ncomp; ++i) { + if (z->img_comp[i].raw_data) { + STBI_FREE(z->img_comp[i].raw_data); + z->img_comp[i].raw_data = NULL; + z->img_comp[i].data = NULL; + } + if (z->img_comp[i].raw_coeff) { + STBI_FREE(z->img_comp[i].raw_coeff); + z->img_comp[i].raw_coeff = 0; + z->img_comp[i].coeff = 0; + } + if (z->img_comp[i].linebuf) { + STBI_FREE(z->img_comp[i].linebuf); + z->img_comp[i].linebuf = NULL; + } + } + return why; +} + +static int stbi__process_frame_header(stbi__jpeg *z, int scan) +{ + stbi__context *s = z->s; + int Lf,p,i,q, h_max=1,v_max=1,c; + Lf = stbi__get16be(s); if (Lf < 11) return stbi__err("bad SOF len","Corrupt JPEG"); // JPEG + p = stbi__get8(s); if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline + s->img_y = stbi__get16be(s); if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG + s->img_x = stbi__get16be(s); if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + c = stbi__get8(s); + if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG"); + s->img_n = c; + for (i=0; i < c; ++i) { + z->img_comp[i].data = NULL; + z->img_comp[i].linebuf = NULL; + } + + if (Lf != 8+3*s->img_n) return stbi__err("bad SOF len","Corrupt JPEG"); + + z->rgb = 0; + for (i=0; i < s->img_n; ++i) { + static const unsigned char rgb[3] = { 'R', 'G', 'B' }; + z->img_comp[i].id = stbi__get8(s); + if (s->img_n == 3 && z->img_comp[i].id == rgb[i]) + ++z->rgb; + q = stbi__get8(s); + z->img_comp[i].h = (q >> 4); if (!z->img_comp[i].h || z->img_comp[i].h > 4) return stbi__err("bad H","Corrupt JPEG"); + z->img_comp[i].v = q & 15; if (!z->img_comp[i].v || z->img_comp[i].v > 4) return stbi__err("bad V","Corrupt JPEG"); + z->img_comp[i].tq = stbi__get8(s); if (z->img_comp[i].tq > 3) return stbi__err("bad TQ","Corrupt JPEG"); + } + + if (scan != STBI__SCAN_load) return 1; + + if (!stbi__mad3sizes_valid(s->img_x, s->img_y, s->img_n, 0)) return stbi__err("too large", "Image too large to decode"); + + for (i=0; i < s->img_n; ++i) { + if (z->img_comp[i].h > h_max) h_max = z->img_comp[i].h; + if (z->img_comp[i].v > v_max) v_max = z->img_comp[i].v; + } + + // check that plane subsampling factors are integer ratios; our resamplers can't deal with fractional ratios + // and I've never seen a non-corrupted JPEG file actually use them + for (i=0; i < s->img_n; ++i) { + if (h_max % z->img_comp[i].h != 0) return stbi__err("bad H","Corrupt JPEG"); + if (v_max % z->img_comp[i].v != 0) return stbi__err("bad V","Corrupt JPEG"); + } + + // compute interleaved mcu info + z->img_h_max = h_max; + z->img_v_max = v_max; + z->img_mcu_w = h_max * 8; + z->img_mcu_h = v_max * 8; + // these sizes can't be more than 17 bits + z->img_mcu_x = (s->img_x + z->img_mcu_w-1) / z->img_mcu_w; + z->img_mcu_y = (s->img_y + z->img_mcu_h-1) / z->img_mcu_h; + + for (i=0; i < s->img_n; ++i) { + // number of effective pixels (e.g. for non-interleaved MCU) + z->img_comp[i].x = (s->img_x * z->img_comp[i].h + h_max-1) / h_max; + z->img_comp[i].y = (s->img_y * z->img_comp[i].v + v_max-1) / v_max; + // to simplify generation, we'll allocate enough memory to decode + // the bogus oversized data from using interleaved MCUs and their + // big blocks (e.g. a 16x16 iMCU on an image of width 33); we won't + // discard the extra data until colorspace conversion + // + // img_mcu_x, img_mcu_y: <=17 bits; comp[i].h and .v are <=4 (checked earlier) + // so these muls can't overflow with 32-bit ints (which we require) + z->img_comp[i].w2 = z->img_mcu_x * z->img_comp[i].h * 8; + z->img_comp[i].h2 = z->img_mcu_y * z->img_comp[i].v * 8; + z->img_comp[i].coeff = 0; + z->img_comp[i].raw_coeff = 0; + z->img_comp[i].linebuf = NULL; + z->img_comp[i].raw_data = stbi__malloc_mad2(z->img_comp[i].w2, z->img_comp[i].h2, 15); + if (z->img_comp[i].raw_data == NULL) + return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); + // align blocks for idct using mmx/sse + z->img_comp[i].data = (stbi_uc*) (((size_t) z->img_comp[i].raw_data + 15) & ~15); + if (z->progressive) { + // w2, h2 are multiples of 8 (see above) + z->img_comp[i].coeff_w = z->img_comp[i].w2 / 8; + z->img_comp[i].coeff_h = z->img_comp[i].h2 / 8; + z->img_comp[i].raw_coeff = stbi__malloc_mad3(z->img_comp[i].w2, z->img_comp[i].h2, sizeof(short), 15); + if (z->img_comp[i].raw_coeff == NULL) + return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); + z->img_comp[i].coeff = (short*) (((size_t) z->img_comp[i].raw_coeff + 15) & ~15); + } + } + + return 1; +} + +// use comparisons since in some cases we handle more than one case (e.g. SOF) +#define stbi__DNL(x) ((x) == 0xdc) +#define stbi__SOI(x) ((x) == 0xd8) +#define stbi__EOI(x) ((x) == 0xd9) +#define stbi__SOF(x) ((x) == 0xc0 || (x) == 0xc1 || (x) == 0xc2) +#define stbi__SOS(x) ((x) == 0xda) + +#define stbi__SOF_progressive(x) ((x) == 0xc2) + +static int stbi__decode_jpeg_header(stbi__jpeg *z, int scan) +{ + int m; + z->jfif = 0; + z->app14_color_transform = -1; // valid values are 0,1,2 + z->marker = STBI__MARKER_none; // initialize cached marker to empty + m = stbi__get_marker(z); + if (!stbi__SOI(m)) return stbi__err("no SOI","Corrupt JPEG"); + if (scan == STBI__SCAN_type) return 1; + m = stbi__get_marker(z); + while (!stbi__SOF(m)) { + if (!stbi__process_marker(z,m)) return 0; + m = stbi__get_marker(z); + while (m == STBI__MARKER_none) { + // some files have extra padding after their blocks, so ok, we'll scan + if (stbi__at_eof(z->s)) return stbi__err("no SOF", "Corrupt JPEG"); + m = stbi__get_marker(z); + } + } + z->progressive = stbi__SOF_progressive(m); + if (!stbi__process_frame_header(z, scan)) return 0; + return 1; +} + +static stbi_uc stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) +{ + // some JPEGs have junk at end, skip over it but if we find what looks + // like a valid marker, resume there + while (!stbi__at_eof(j->s)) { + stbi_uc x = stbi__get8(j->s); + while (x == 0xff) { // might be a marker + if (stbi__at_eof(j->s)) return STBI__MARKER_none; + x = stbi__get8(j->s); + if (x != 0x00 && x != 0xff) { + // not a stuffed zero or lead-in to another marker, looks + // like an actual marker, return it + return x; + } + // stuffed zero has x=0 now which ends the loop, meaning we go + // back to regular scan loop. + // repeated 0xff keeps trying to read the next byte of the marker. + } + } + return STBI__MARKER_none; +} + +// decode image to YCbCr format +static int stbi__decode_jpeg_image(stbi__jpeg *j) +{ + int m; + for (m = 0; m < 4; m++) { + j->img_comp[m].raw_data = NULL; + j->img_comp[m].raw_coeff = NULL; + } + j->restart_interval = 0; + if (!stbi__decode_jpeg_header(j, STBI__SCAN_load)) return 0; + m = stbi__get_marker(j); + while (!stbi__EOI(m)) { + if (stbi__SOS(m)) { + if (!stbi__process_scan_header(j)) return 0; + if (!stbi__parse_entropy_coded_data(j)) return 0; + if (j->marker == STBI__MARKER_none ) { + j->marker = stbi__skip_jpeg_junk_at_end(j); + // if we reach eof without hitting a marker, stbi__get_marker() below will fail and we'll eventually return 0 + } + m = stbi__get_marker(j); + if (STBI__RESTART(m)) + m = stbi__get_marker(j); + } else if (stbi__DNL(m)) { + int Ld = stbi__get16be(j->s); + stbi__uint32 NL = stbi__get16be(j->s); + if (Ld != 4) return stbi__err("bad DNL len", "Corrupt JPEG"); + if (NL != j->s->img_y) return stbi__err("bad DNL height", "Corrupt JPEG"); + m = stbi__get_marker(j); + } else { + if (!stbi__process_marker(j, m)) return 1; + m = stbi__get_marker(j); + } + } + if (j->progressive) + stbi__jpeg_finish(j); + return 1; +} + +// static jfif-centered resampling (across block boundaries) + +typedef stbi_uc *(*resample_row_func)(stbi_uc *out, stbi_uc *in0, stbi_uc *in1, + int w, int hs); + +#define stbi__div4(x) ((stbi_uc) ((x) >> 2)) + +static stbi_uc *resample_row_1(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + STBI_NOTUSED(out); + STBI_NOTUSED(in_far); + STBI_NOTUSED(w); + STBI_NOTUSED(hs); + return in_near; +} + +static stbi_uc* stbi__resample_row_v_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate two samples vertically for every one in input + int i; + STBI_NOTUSED(hs); + for (i=0; i < w; ++i) + out[i] = stbi__div4(3*in_near[i] + in_far[i] + 2); + return out; +} + +static stbi_uc* stbi__resample_row_h_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate two samples horizontally for every one in input + int i; + stbi_uc *input = in_near; + + if (w == 1) { + // if only one sample, can't do any interpolation + out[0] = out[1] = input[0]; + return out; + } + + out[0] = input[0]; + out[1] = stbi__div4(input[0]*3 + input[1] + 2); + for (i=1; i < w-1; ++i) { + int n = 3*input[i]+2; + out[i*2+0] = stbi__div4(n+input[i-1]); + out[i*2+1] = stbi__div4(n+input[i+1]); + } + out[i*2+0] = stbi__div4(input[w-2]*3 + input[w-1] + 2); + out[i*2+1] = input[w-1]; + + STBI_NOTUSED(in_far); + STBI_NOTUSED(hs); + + return out; +} + +#define stbi__div16(x) ((stbi_uc) ((x) >> 4)) + +static stbi_uc *stbi__resample_row_hv_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate 2x2 samples for every one in input + int i,t0,t1; + if (w == 1) { + out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); + return out; + } + + t1 = 3*in_near[0] + in_far[0]; + out[0] = stbi__div4(t1+2); + for (i=1; i < w; ++i) { + t0 = t1; + t1 = 3*in_near[i]+in_far[i]; + out[i*2-1] = stbi__div16(3*t0 + t1 + 8); + out[i*2 ] = stbi__div16(3*t1 + t0 + 8); + } + out[w*2-1] = stbi__div4(t1+2); + + STBI_NOTUSED(hs); + + return out; +} + +#if defined(STBI_SSE2) || defined(STBI_NEON) +static stbi_uc *stbi__resample_row_hv_2_simd(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate 2x2 samples for every one in input + int i=0,t0,t1; + + if (w == 1) { + out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); + return out; + } + + t1 = 3*in_near[0] + in_far[0]; + // process groups of 8 pixels for as long as we can. + // note we can't handle the last pixel in a row in this loop + // because we need to handle the filter boundary conditions. + for (; i < ((w-1) & ~7); i += 8) { +#if defined(STBI_SSE2) + // load and perform the vertical filtering pass + // this uses 3*x + y = 4*x + (y - x) + __m128i zero = _mm_setzero_si128(); + __m128i farb = _mm_loadl_epi64((__m128i *) (in_far + i)); + __m128i nearb = _mm_loadl_epi64((__m128i *) (in_near + i)); + __m128i farw = _mm_unpacklo_epi8(farb, zero); + __m128i nearw = _mm_unpacklo_epi8(nearb, zero); + __m128i diff = _mm_sub_epi16(farw, nearw); + __m128i nears = _mm_slli_epi16(nearw, 2); + __m128i curr = _mm_add_epi16(nears, diff); // current row + + // horizontal filter works the same based on shifted vers of current + // row. "prev" is current row shifted right by 1 pixel; we need to + // insert the previous pixel value (from t1). + // "next" is current row shifted left by 1 pixel, with first pixel + // of next block of 8 pixels added in. + __m128i prv0 = _mm_slli_si128(curr, 2); + __m128i nxt0 = _mm_srli_si128(curr, 2); + __m128i prev = _mm_insert_epi16(prv0, t1, 0); + __m128i next = _mm_insert_epi16(nxt0, 3*in_near[i+8] + in_far[i+8], 7); + + // horizontal filter, polyphase implementation since it's convenient: + // even pixels = 3*cur + prev = cur*4 + (prev - cur) + // odd pixels = 3*cur + next = cur*4 + (next - cur) + // note the shared term. + __m128i bias = _mm_set1_epi16(8); + __m128i curs = _mm_slli_epi16(curr, 2); + __m128i prvd = _mm_sub_epi16(prev, curr); + __m128i nxtd = _mm_sub_epi16(next, curr); + __m128i curb = _mm_add_epi16(curs, bias); + __m128i even = _mm_add_epi16(prvd, curb); + __m128i odd = _mm_add_epi16(nxtd, curb); + + // interleave even and odd pixels, then undo scaling. + __m128i int0 = _mm_unpacklo_epi16(even, odd); + __m128i int1 = _mm_unpackhi_epi16(even, odd); + __m128i de0 = _mm_srli_epi16(int0, 4); + __m128i de1 = _mm_srli_epi16(int1, 4); + + // pack and write output + __m128i outv = _mm_packus_epi16(de0, de1); + _mm_storeu_si128((__m128i *) (out + i*2), outv); +#elif defined(STBI_NEON) + // load and perform the vertical filtering pass + // this uses 3*x + y = 4*x + (y - x) + uint8x8_t farb = vld1_u8(in_far + i); + uint8x8_t nearb = vld1_u8(in_near + i); + int16x8_t diff = vreinterpretq_s16_u16(vsubl_u8(farb, nearb)); + int16x8_t nears = vreinterpretq_s16_u16(vshll_n_u8(nearb, 2)); + int16x8_t curr = vaddq_s16(nears, diff); // current row + + // horizontal filter works the same based on shifted vers of current + // row. "prev" is current row shifted right by 1 pixel; we need to + // insert the previous pixel value (from t1). + // "next" is current row shifted left by 1 pixel, with first pixel + // of next block of 8 pixels added in. + int16x8_t prv0 = vextq_s16(curr, curr, 7); + int16x8_t nxt0 = vextq_s16(curr, curr, 1); + int16x8_t prev = vsetq_lane_s16(t1, prv0, 0); + int16x8_t next = vsetq_lane_s16(3*in_near[i+8] + in_far[i+8], nxt0, 7); + + // horizontal filter, polyphase implementation since it's convenient: + // even pixels = 3*cur + prev = cur*4 + (prev - cur) + // odd pixels = 3*cur + next = cur*4 + (next - cur) + // note the shared term. + int16x8_t curs = vshlq_n_s16(curr, 2); + int16x8_t prvd = vsubq_s16(prev, curr); + int16x8_t nxtd = vsubq_s16(next, curr); + int16x8_t even = vaddq_s16(curs, prvd); + int16x8_t odd = vaddq_s16(curs, nxtd); + + // undo scaling and round, then store with even/odd phases interleaved + uint8x8x2_t o; + o.val[0] = vqrshrun_n_s16(even, 4); + o.val[1] = vqrshrun_n_s16(odd, 4); + vst2_u8(out + i*2, o); +#endif + + // "previous" value for next iter + t1 = 3*in_near[i+7] + in_far[i+7]; + } + + t0 = t1; + t1 = 3*in_near[i] + in_far[i]; + out[i*2] = stbi__div16(3*t1 + t0 + 8); + + for (++i; i < w; ++i) { + t0 = t1; + t1 = 3*in_near[i]+in_far[i]; + out[i*2-1] = stbi__div16(3*t0 + t1 + 8); + out[i*2 ] = stbi__div16(3*t1 + t0 + 8); + } + out[w*2-1] = stbi__div4(t1+2); + + STBI_NOTUSED(hs); + + return out; +} +#endif + +static stbi_uc *stbi__resample_row_generic(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // resample with nearest-neighbor + int i,j; + STBI_NOTUSED(in_far); + for (i=0; i < w; ++i) + for (j=0; j < hs; ++j) + out[i*hs+j] = in_near[i]; + return out; +} + +// this is a reduced-precision calculation of YCbCr-to-RGB introduced +// to make sure the code produces the same results in both SIMD and scalar +#define stbi__float2fixed(x) (((int) ((x) * 4096.0f + 0.5f)) << 8) +static void stbi__YCbCr_to_RGB_row(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step) +{ + int i; + for (i=0; i < count; ++i) { + int y_fixed = (y[i] << 20) + (1<<19); // rounding + int r,g,b; + int cr = pcr[i] - 128; + int cb = pcb[i] - 128; + r = y_fixed + cr* stbi__float2fixed(1.40200f); + g = y_fixed + (cr*-stbi__float2fixed(0.71414f)) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); + b = y_fixed + cb* stbi__float2fixed(1.77200f); + r >>= 20; + g >>= 20; + b >>= 20; + if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } + if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } + if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } + out[0] = (stbi_uc)r; + out[1] = (stbi_uc)g; + out[2] = (stbi_uc)b; + out[3] = 255; + out += step; + } +} + +#if defined(STBI_SSE2) || defined(STBI_NEON) +static void stbi__YCbCr_to_RGB_simd(stbi_uc *out, stbi_uc const *y, stbi_uc const *pcb, stbi_uc const *pcr, int count, int step) +{ + int i = 0; + +#ifdef STBI_SSE2 + // step == 3 is pretty ugly on the final interleave, and i'm not convinced + // it's useful in practice (you wouldn't use it for textures, for example). + // so just accelerate step == 4 case. + if (step == 4) { + // this is a fairly straightforward implementation and not super-optimized. + __m128i signflip = _mm_set1_epi8(-0x80); + __m128i cr_const0 = _mm_set1_epi16( (short) ( 1.40200f*4096.0f+0.5f)); + __m128i cr_const1 = _mm_set1_epi16( - (short) ( 0.71414f*4096.0f+0.5f)); + __m128i cb_const0 = _mm_set1_epi16( - (short) ( 0.34414f*4096.0f+0.5f)); + __m128i cb_const1 = _mm_set1_epi16( (short) ( 1.77200f*4096.0f+0.5f)); + __m128i y_bias = _mm_set1_epi8((char) (unsigned char) 128); + __m128i xw = _mm_set1_epi16(255); // alpha channel + + for (; i+7 < count; i += 8) { + // load + __m128i y_bytes = _mm_loadl_epi64((__m128i *) (y+i)); + __m128i cr_bytes = _mm_loadl_epi64((__m128i *) (pcr+i)); + __m128i cb_bytes = _mm_loadl_epi64((__m128i *) (pcb+i)); + __m128i cr_biased = _mm_xor_si128(cr_bytes, signflip); // -128 + __m128i cb_biased = _mm_xor_si128(cb_bytes, signflip); // -128 + + // unpack to short (and left-shift cr, cb by 8) + __m128i yw = _mm_unpacklo_epi8(y_bias, y_bytes); + __m128i crw = _mm_unpacklo_epi8(_mm_setzero_si128(), cr_biased); + __m128i cbw = _mm_unpacklo_epi8(_mm_setzero_si128(), cb_biased); + + // color transform + __m128i yws = _mm_srli_epi16(yw, 4); + __m128i cr0 = _mm_mulhi_epi16(cr_const0, crw); + __m128i cb0 = _mm_mulhi_epi16(cb_const0, cbw); + __m128i cb1 = _mm_mulhi_epi16(cbw, cb_const1); + __m128i cr1 = _mm_mulhi_epi16(crw, cr_const1); + __m128i rws = _mm_add_epi16(cr0, yws); + __m128i gwt = _mm_add_epi16(cb0, yws); + __m128i bws = _mm_add_epi16(yws, cb1); + __m128i gws = _mm_add_epi16(gwt, cr1); + + // descale + __m128i rw = _mm_srai_epi16(rws, 4); + __m128i bw = _mm_srai_epi16(bws, 4); + __m128i gw = _mm_srai_epi16(gws, 4); + + // back to byte, set up for transpose + __m128i brb = _mm_packus_epi16(rw, bw); + __m128i gxb = _mm_packus_epi16(gw, xw); + + // transpose to interleave channels + __m128i t0 = _mm_unpacklo_epi8(brb, gxb); + __m128i t1 = _mm_unpackhi_epi8(brb, gxb); + __m128i o0 = _mm_unpacklo_epi16(t0, t1); + __m128i o1 = _mm_unpackhi_epi16(t0, t1); + + // store + _mm_storeu_si128((__m128i *) (out + 0), o0); + _mm_storeu_si128((__m128i *) (out + 16), o1); + out += 32; + } + } +#endif + +#ifdef STBI_NEON + // in this version, step=3 support would be easy to add. but is there demand? + if (step == 4) { + // this is a fairly straightforward implementation and not super-optimized. + uint8x8_t signflip = vdup_n_u8(0x80); + int16x8_t cr_const0 = vdupq_n_s16( (short) ( 1.40200f*4096.0f+0.5f)); + int16x8_t cr_const1 = vdupq_n_s16( - (short) ( 0.71414f*4096.0f+0.5f)); + int16x8_t cb_const0 = vdupq_n_s16( - (short) ( 0.34414f*4096.0f+0.5f)); + int16x8_t cb_const1 = vdupq_n_s16( (short) ( 1.77200f*4096.0f+0.5f)); + + for (; i+7 < count; i += 8) { + // load + uint8x8_t y_bytes = vld1_u8(y + i); + uint8x8_t cr_bytes = vld1_u8(pcr + i); + uint8x8_t cb_bytes = vld1_u8(pcb + i); + int8x8_t cr_biased = vreinterpret_s8_u8(vsub_u8(cr_bytes, signflip)); + int8x8_t cb_biased = vreinterpret_s8_u8(vsub_u8(cb_bytes, signflip)); + + // expand to s16 + int16x8_t yws = vreinterpretq_s16_u16(vshll_n_u8(y_bytes, 4)); + int16x8_t crw = vshll_n_s8(cr_biased, 7); + int16x8_t cbw = vshll_n_s8(cb_biased, 7); + + // color transform + int16x8_t cr0 = vqdmulhq_s16(crw, cr_const0); + int16x8_t cb0 = vqdmulhq_s16(cbw, cb_const0); + int16x8_t cr1 = vqdmulhq_s16(crw, cr_const1); + int16x8_t cb1 = vqdmulhq_s16(cbw, cb_const1); + int16x8_t rws = vaddq_s16(yws, cr0); + int16x8_t gws = vaddq_s16(vaddq_s16(yws, cb0), cr1); + int16x8_t bws = vaddq_s16(yws, cb1); + + // undo scaling, round, convert to byte + uint8x8x4_t o; + o.val[0] = vqrshrun_n_s16(rws, 4); + o.val[1] = vqrshrun_n_s16(gws, 4); + o.val[2] = vqrshrun_n_s16(bws, 4); + o.val[3] = vdup_n_u8(255); + + // store, interleaving r/g/b/a + vst4_u8(out, o); + out += 8*4; + } + } +#endif + + for (; i < count; ++i) { + int y_fixed = (y[i] << 20) + (1<<19); // rounding + int r,g,b; + int cr = pcr[i] - 128; + int cb = pcb[i] - 128; + r = y_fixed + cr* stbi__float2fixed(1.40200f); + g = y_fixed + cr*-stbi__float2fixed(0.71414f) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); + b = y_fixed + cb* stbi__float2fixed(1.77200f); + r >>= 20; + g >>= 20; + b >>= 20; + if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } + if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } + if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } + out[0] = (stbi_uc)r; + out[1] = (stbi_uc)g; + out[2] = (stbi_uc)b; + out[3] = 255; + out += step; + } +} +#endif + +// set up the kernels +static void stbi__setup_jpeg(stbi__jpeg *j) +{ + j->idct_block_kernel = stbi__idct_block; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_row; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2; + +#ifdef STBI_SSE2 + if (stbi__sse2_available()) { + j->idct_block_kernel = stbi__idct_simd; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; + } +#endif + +#ifdef STBI_NEON + j->idct_block_kernel = stbi__idct_simd; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; +#endif +} + +// clean up the temporary component buffers +static void stbi__cleanup_jpeg(stbi__jpeg *j) +{ + stbi__free_jpeg_components(j, j->s->img_n, 0); +} + +typedef struct +{ + resample_row_func resample; + stbi_uc *line0,*line1; + int hs,vs; // expansion factor in each axis + int w_lores; // horizontal pixels pre-expansion + int ystep; // how far through vertical expansion we are + int ypos; // which pre-expansion row we're on +} stbi__resample; + +// fast 0..255 * 0..255 => 0..255 rounded multiplication +static stbi_uc stbi__blinn_8x8(stbi_uc x, stbi_uc y) +{ + unsigned int t = x*y + 128; + return (stbi_uc) ((t + (t >>8)) >> 8); +} + +static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp, int req_comp) +{ + int n, decode_n, is_rgb; + z->s->img_n = 0; // make stbi__cleanup_jpeg safe + + // validate req_comp + if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); + + // load a jpeg image from whichever source, but leave in YCbCr format + if (!stbi__decode_jpeg_image(z)) { stbi__cleanup_jpeg(z); return NULL; } + + // determine actual number of components to generate + n = req_comp ? req_comp : z->s->img_n >= 3 ? 3 : 1; + + is_rgb = z->s->img_n == 3 && (z->rgb == 3 || (z->app14_color_transform == 0 && !z->jfif)); + + if (z->s->img_n == 3 && n < 3 && !is_rgb) + decode_n = 1; + else + decode_n = z->s->img_n; + + // nothing to do if no components requested; check this now to avoid + // accessing uninitialized coutput[0] later + if (decode_n <= 0) { stbi__cleanup_jpeg(z); return NULL; } + + // resample and color-convert + { + int k; + unsigned int i,j; + stbi_uc *output; + stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL }; + + stbi__resample res_comp[4]; + + for (k=0; k < decode_n; ++k) { + stbi__resample *r = &res_comp[k]; + + // allocate line buffer big enough for upsampling off the edges + // with upsample factor of 4 + z->img_comp[k].linebuf = (stbi_uc *) stbi__malloc(z->s->img_x + 3); + if (!z->img_comp[k].linebuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } + + r->hs = z->img_h_max / z->img_comp[k].h; + r->vs = z->img_v_max / z->img_comp[k].v; + r->ystep = r->vs >> 1; + r->w_lores = (z->s->img_x + r->hs-1) / r->hs; + r->ypos = 0; + r->line0 = r->line1 = z->img_comp[k].data; + + if (r->hs == 1 && r->vs == 1) r->resample = resample_row_1; + else if (r->hs == 1 && r->vs == 2) r->resample = stbi__resample_row_v_2; + else if (r->hs == 2 && r->vs == 1) r->resample = stbi__resample_row_h_2; + else if (r->hs == 2 && r->vs == 2) r->resample = z->resample_row_hv_2_kernel; + else r->resample = stbi__resample_row_generic; + } + + // can't error after this so, this is safe + output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1); + if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } + + // now go ahead and resample + for (j=0; j < z->s->img_y; ++j) { + stbi_uc *out = output + n * z->s->img_x * j; + for (k=0; k < decode_n; ++k) { + stbi__resample *r = &res_comp[k]; + int y_bot = r->ystep >= (r->vs >> 1); + coutput[k] = r->resample(z->img_comp[k].linebuf, + y_bot ? r->line1 : r->line0, + y_bot ? r->line0 : r->line1, + r->w_lores, r->hs); + if (++r->ystep >= r->vs) { + r->ystep = 0; + r->line0 = r->line1; + if (++r->ypos < z->img_comp[k].y) + r->line1 += z->img_comp[k].w2; + } + } + if (n >= 3) { + stbi_uc *y = coutput[0]; + if (z->s->img_n == 3) { + if (is_rgb) { + for (i=0; i < z->s->img_x; ++i) { + out[0] = y[i]; + out[1] = coutput[1][i]; + out[2] = coutput[2][i]; + out[3] = 255; + out += n; + } + } else { + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); + } + } else if (z->s->img_n == 4) { + if (z->app14_color_transform == 0) { // CMYK + for (i=0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + out[0] = stbi__blinn_8x8(coutput[0][i], m); + out[1] = stbi__blinn_8x8(coutput[1][i], m); + out[2] = stbi__blinn_8x8(coutput[2][i], m); + out[3] = 255; + out += n; + } + } else if (z->app14_color_transform == 2) { // YCCK + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); + for (i=0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + out[0] = stbi__blinn_8x8(255 - out[0], m); + out[1] = stbi__blinn_8x8(255 - out[1], m); + out[2] = stbi__blinn_8x8(255 - out[2], m); + out += n; + } + } else { // YCbCr + alpha? Ignore the fourth channel for now + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); + } + } else + for (i=0; i < z->s->img_x; ++i) { + out[0] = out[1] = out[2] = y[i]; + out[3] = 255; // not used if n==3 + out += n; + } + } else { + if (is_rgb) { + if (n == 1) + for (i=0; i < z->s->img_x; ++i) + *out++ = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); + else { + for (i=0; i < z->s->img_x; ++i, out += 2) { + out[0] = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); + out[1] = 255; + } + } + } else if (z->s->img_n == 4 && z->app14_color_transform == 0) { + for (i=0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + stbi_uc r = stbi__blinn_8x8(coutput[0][i], m); + stbi_uc g = stbi__blinn_8x8(coutput[1][i], m); + stbi_uc b = stbi__blinn_8x8(coutput[2][i], m); + out[0] = stbi__compute_y(r, g, b); + out[1] = 255; + out += n; + } + } else if (z->s->img_n == 4 && z->app14_color_transform == 2) { + for (i=0; i < z->s->img_x; ++i) { + out[0] = stbi__blinn_8x8(255 - coutput[0][i], coutput[3][i]); + out[1] = 255; + out += n; + } + } else { + stbi_uc *y = coutput[0]; + if (n == 1) + for (i=0; i < z->s->img_x; ++i) out[i] = y[i]; + else + for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; } + } + } + } + stbi__cleanup_jpeg(z); + *out_x = z->s->img_x; + *out_y = z->s->img_y; + if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output + return output; + } +} + +static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + unsigned char* result; + stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg)); + if (!j) return stbi__errpuc("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); + STBI_NOTUSED(ri); + j->s = s; + stbi__setup_jpeg(j); + result = load_jpeg_image(j, x,y,comp,req_comp); + STBI_FREE(j); + return result; +} + +static int stbi__jpeg_test(stbi__context *s) +{ + int r; + stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg)); + if (!j) return stbi__err("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); + j->s = s; + stbi__setup_jpeg(j); + r = stbi__decode_jpeg_header(j, STBI__SCAN_type); + stbi__rewind(s); + STBI_FREE(j); + return r; +} + +static int stbi__jpeg_info_raw(stbi__jpeg *j, int *x, int *y, int *comp) +{ + if (!stbi__decode_jpeg_header(j, STBI__SCAN_header)) { + stbi__rewind( j->s ); + return 0; + } + if (x) *x = j->s->img_x; + if (y) *y = j->s->img_y; + if (comp) *comp = j->s->img_n >= 3 ? 3 : 1; + return 1; +} + +static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp) +{ + int result; + stbi__jpeg* j = (stbi__jpeg*) (stbi__malloc(sizeof(stbi__jpeg))); + if (!j) return stbi__err("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); + j->s = s; + result = stbi__jpeg_info_raw(j, x, y, comp); + STBI_FREE(j); + return result; +} +#endif + +// public domain zlib decode v0.2 Sean Barrett 2006-11-18 +// simple implementation +// - all input must be provided in an upfront buffer +// - all output is written to a single output buffer (can malloc/realloc) +// performance +// - fast huffman + +#ifndef STBI_NO_ZLIB + +// fast-way is faster to check than jpeg huffman, but slow way is slower +#define STBI__ZFAST_BITS 9 // accelerate all cases in default tables +#define STBI__ZFAST_MASK ((1 << STBI__ZFAST_BITS) - 1) +#define STBI__ZNSYMS 288 // number of symbols in literal/length alphabet + +// zlib-style huffman encoding +// (jpegs packs from left, zlib from right, so can't share code) +typedef struct +{ + stbi__uint16 fast[1 << STBI__ZFAST_BITS]; + stbi__uint16 firstcode[16]; + int maxcode[17]; + stbi__uint16 firstsymbol[16]; + stbi_uc size[STBI__ZNSYMS]; + stbi__uint16 value[STBI__ZNSYMS]; +} stbi__zhuffman; + +stbi_inline static int stbi__bitreverse16(int n) +{ + n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); + n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); + n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4); + n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8); + return n; +} + +stbi_inline static int stbi__bit_reverse(int v, int bits) +{ + STBI_ASSERT(bits <= 16); + // to bit reverse n bits, reverse 16 and shift + // e.g. 11 bits, bit reverse and shift away 5 + return stbi__bitreverse16(v) >> (16-bits); +} + +static int stbi__zbuild_huffman(stbi__zhuffman *z, const stbi_uc *sizelist, int num) +{ + int i,k=0; + int code, next_code[16], sizes[17]; + + // DEFLATE spec for generating codes + memset(sizes, 0, sizeof(sizes)); + memset(z->fast, 0, sizeof(z->fast)); + for (i=0; i < num; ++i) + ++sizes[sizelist[i]]; + sizes[0] = 0; + for (i=1; i < 16; ++i) + if (sizes[i] > (1 << i)) + return stbi__err("bad sizes", "Corrupt PNG"); + code = 0; + for (i=1; i < 16; ++i) { + next_code[i] = code; + z->firstcode[i] = (stbi__uint16) code; + z->firstsymbol[i] = (stbi__uint16) k; + code = (code + sizes[i]); + if (sizes[i]) + if (code-1 >= (1 << i)) return stbi__err("bad codelengths","Corrupt PNG"); + z->maxcode[i] = code << (16-i); // preshift for inner loop + code <<= 1; + k += sizes[i]; + } + z->maxcode[16] = 0x10000; // sentinel + for (i=0; i < num; ++i) { + int s = sizelist[i]; + if (s) { + int c = next_code[s] - z->firstcode[s] + z->firstsymbol[s]; + stbi__uint16 fastv = (stbi__uint16) ((s << 9) | i); + z->size [c] = (stbi_uc ) s; + z->value[c] = (stbi__uint16) i; + if (s <= STBI__ZFAST_BITS) { + int j = stbi__bit_reverse(next_code[s],s); + while (j < (1 << STBI__ZFAST_BITS)) { + z->fast[j] = fastv; + j += (1 << s); + } + } + ++next_code[s]; + } + } + return 1; +} + +// zlib-from-memory implementation for PNG reading +// because PNG allows splitting the zlib stream arbitrarily, +// and it's annoying structurally to have PNG call ZLIB call PNG, +// we require PNG read all the IDATs and combine them into a single +// memory buffer + +typedef struct +{ + stbi_uc *zbuffer, *zbuffer_end; + int num_bits; + int hit_zeof_once; + stbi__uint32 code_buffer; + + char *zout; + char *zout_start; + char *zout_end; + int z_expandable; + + stbi__zhuffman z_length, z_distance; +} stbi__zbuf; + +stbi_inline static int stbi__zeof(stbi__zbuf *z) +{ + return (z->zbuffer >= z->zbuffer_end); +} + +stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z) +{ + return stbi__zeof(z) ? 0 : *z->zbuffer++; +} + +static void stbi__fill_bits(stbi__zbuf *z) +{ + do { + if (z->code_buffer >= (1U << z->num_bits)) { + z->zbuffer = z->zbuffer_end; /* treat this as EOF so we fail. */ + return; + } + z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits; + z->num_bits += 8; + } while (z->num_bits <= 24); +} + +stbi_inline static unsigned int stbi__zreceive(stbi__zbuf *z, int n) +{ + unsigned int k; + if (z->num_bits < n) stbi__fill_bits(z); + k = z->code_buffer & ((1 << n) - 1); + z->code_buffer >>= n; + z->num_bits -= n; + return k; +} + +static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) +{ + int b,s,k; + // not resolved by fast table, so compute it the slow way + // use jpeg approach, which requires MSbits at top + k = stbi__bit_reverse(a->code_buffer, 16); + for (s=STBI__ZFAST_BITS+1; ; ++s) + if (k < z->maxcode[s]) + break; + if (s >= 16) return -1; // invalid code! + // code size is s, so: + b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; + if (b >= STBI__ZNSYMS) return -1; // some data was corrupt somewhere! + if (z->size[b] != s) return -1; // was originally an assert, but report failure instead. + a->code_buffer >>= s; + a->num_bits -= s; + return z->value[b]; +} + +stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) +{ + int b,s; + if (a->num_bits < 16) { + if (stbi__zeof(a)) { + if (!a->hit_zeof_once) { + // This is the first time we hit eof, insert 16 extra padding btis + // to allow us to keep going; if we actually consume any of them + // though, that is invalid data. This is caught later. + a->hit_zeof_once = 1; + a->num_bits += 16; // add 16 implicit zero bits + } else { + // We already inserted our extra 16 padding bits and are again + // out, this stream is actually prematurely terminated. + return -1; + } + } else { + stbi__fill_bits(a); + } + } + b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; + if (b) { + s = b >> 9; + a->code_buffer >>= s; + a->num_bits -= s; + return b & 511; + } + return stbi__zhuffman_decode_slowpath(a, z); +} + +static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) // need to make room for n bytes +{ + char *q; + unsigned int cur, limit, old_limit; + z->zout = zout; + if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG"); + cur = (unsigned int) (z->zout - z->zout_start); + limit = old_limit = (unsigned) (z->zout_end - z->zout_start); + if (UINT_MAX - cur < (unsigned) n) return stbi__err("outofmem", "Out of memory"); + while (cur + n > limit) { + if(limit > UINT_MAX / 2) return stbi__err("outofmem", "Out of memory"); + limit *= 2; + } + q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit); + STBI_NOTUSED(old_limit); + if (q == NULL) return stbi__err("outofmem", "Out of memory"); + z->zout_start = q; + z->zout = q + cur; + z->zout_end = q + limit; + return 1; +} + +static const int stbi__zlength_base[31] = { + 3,4,5,6,7,8,9,10,11,13, + 15,17,19,23,27,31,35,43,51,59, + 67,83,99,115,131,163,195,227,258,0,0 }; + +static const int stbi__zlength_extra[31]= +{ 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 }; + +static const int stbi__zdist_base[32] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, +257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0}; + +static const int stbi__zdist_extra[32] = +{ 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; + +static int stbi__parse_huffman_block(stbi__zbuf *a) +{ + char *zout = a->zout; + for(;;) { + int z = stbi__zhuffman_decode(a, &a->z_length); + if (z < 256) { + if (z < 0) return stbi__err("bad huffman code","Corrupt PNG"); // error in huffman codes + if (zout >= a->zout_end) { + if (!stbi__zexpand(a, zout, 1)) return 0; + zout = a->zout; + } + *zout++ = (char) z; + } else { + stbi_uc *p; + int len,dist; + if (z == 256) { + a->zout = zout; + if (a->hit_zeof_once && a->num_bits < 16) { + // The first time we hit zeof, we inserted 16 extra zero bits into our bit + // buffer so the decoder can just do its speculative decoding. But if we + // actually consumed any of those bits (which is the case when num_bits < 16), + // the stream actually read past the end so it is malformed. + return stbi__err("unexpected end","Corrupt PNG"); + } + return 1; + } + if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data + z -= 257; + len = stbi__zlength_base[z]; + if (stbi__zlength_extra[z]) len += stbi__zreceive(a, stbi__zlength_extra[z]); + z = stbi__zhuffman_decode(a, &a->z_distance); + if (z < 0 || z >= 30) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, distance codes 30 and 31 must not appear in compressed data + dist = stbi__zdist_base[z]; + if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]); + if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG"); + if (len > a->zout_end - zout) { + if (!stbi__zexpand(a, zout, len)) return 0; + zout = a->zout; + } + p = (stbi_uc *) (zout - dist); + if (dist == 1) { // run of one byte; common in images. + stbi_uc v = *p; + if (len) { do *zout++ = v; while (--len); } + } else { + if (len) { do *zout++ = *p++; while (--len); } + } + } + } +} + +static int stbi__compute_huffman_codes(stbi__zbuf *a) +{ + static const stbi_uc length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; + stbi__zhuffman z_codelength; + stbi_uc lencodes[286+32+137];//padding for maximum single op + stbi_uc codelength_sizes[19]; + int i,n; + + int hlit = stbi__zreceive(a,5) + 257; + int hdist = stbi__zreceive(a,5) + 1; + int hclen = stbi__zreceive(a,4) + 4; + int ntot = hlit + hdist; + + memset(codelength_sizes, 0, sizeof(codelength_sizes)); + for (i=0; i < hclen; ++i) { + int s = stbi__zreceive(a,3); + codelength_sizes[length_dezigzag[i]] = (stbi_uc) s; + } + if (!stbi__zbuild_huffman(&z_codelength, codelength_sizes, 19)) return 0; + + n = 0; + while (n < ntot) { + int c = stbi__zhuffman_decode(a, &z_codelength); + if (c < 0 || c >= 19) return stbi__err("bad codelengths", "Corrupt PNG"); + if (c < 16) + lencodes[n++] = (stbi_uc) c; + else { + stbi_uc fill = 0; + if (c == 16) { + c = stbi__zreceive(a,2)+3; + if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG"); + fill = lencodes[n-1]; + } else if (c == 17) { + c = stbi__zreceive(a,3)+3; + } else if (c == 18) { + c = stbi__zreceive(a,7)+11; + } else { + return stbi__err("bad codelengths", "Corrupt PNG"); + } + if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG"); + memset(lencodes+n, fill, c); + n += c; + } + } + if (n != ntot) return stbi__err("bad codelengths","Corrupt PNG"); + if (!stbi__zbuild_huffman(&a->z_length, lencodes, hlit)) return 0; + if (!stbi__zbuild_huffman(&a->z_distance, lencodes+hlit, hdist)) return 0; + return 1; +} + +static int stbi__parse_uncompressed_block(stbi__zbuf *a) +{ + stbi_uc header[4]; + int len,nlen,k; + if (a->num_bits & 7) + stbi__zreceive(a, a->num_bits & 7); // discard + // drain the bit-packed data into header + k = 0; + while (a->num_bits > 0) { + header[k++] = (stbi_uc) (a->code_buffer & 255); // suppress MSVC run-time check + a->code_buffer >>= 8; + a->num_bits -= 8; + } + if (a->num_bits < 0) return stbi__err("zlib corrupt","Corrupt PNG"); + // now fill header the normal way + while (k < 4) + header[k++] = stbi__zget8(a); + len = header[1] * 256 + header[0]; + nlen = header[3] * 256 + header[2]; + if (nlen != (len ^ 0xffff)) return stbi__err("zlib corrupt","Corrupt PNG"); + if (a->zbuffer + len > a->zbuffer_end) return stbi__err("read past buffer","Corrupt PNG"); + if (a->zout + len > a->zout_end) + if (!stbi__zexpand(a, a->zout, len)) return 0; + memcpy(a->zout, a->zbuffer, len); + a->zbuffer += len; + a->zout += len; + return 1; +} + +static int stbi__parse_zlib_header(stbi__zbuf *a) +{ + int cmf = stbi__zget8(a); + int cm = cmf & 15; + /* int cinfo = cmf >> 4; */ + int flg = stbi__zget8(a); + if (stbi__zeof(a)) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec + if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec + if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png + if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png + // window = 1 << (8 + cinfo)... but who cares, we fully buffer output + return 1; +} + +static const stbi_uc stbi__zdefault_length[STBI__ZNSYMS] = +{ + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8 +}; +static const stbi_uc stbi__zdefault_distance[32] = +{ + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 +}; +/* +Init algorithm: +{ + int i; // use <= to match clearly with spec + for (i=0; i <= 143; ++i) stbi__zdefault_length[i] = 8; + for ( ; i <= 255; ++i) stbi__zdefault_length[i] = 9; + for ( ; i <= 279; ++i) stbi__zdefault_length[i] = 7; + for ( ; i <= 287; ++i) stbi__zdefault_length[i] = 8; + + for (i=0; i <= 31; ++i) stbi__zdefault_distance[i] = 5; +} +*/ + +static int stbi__parse_zlib(stbi__zbuf *a, int parse_header) +{ + int final, type; + if (parse_header) + if (!stbi__parse_zlib_header(a)) return 0; + a->num_bits = 0; + a->code_buffer = 0; + a->hit_zeof_once = 0; + do { + final = stbi__zreceive(a,1); + type = stbi__zreceive(a,2); + if (type == 0) { + if (!stbi__parse_uncompressed_block(a)) return 0; + } else if (type == 3) { + return 0; + } else { + if (type == 1) { + // use fixed code lengths + if (!stbi__zbuild_huffman(&a->z_length , stbi__zdefault_length , STBI__ZNSYMS)) return 0; + if (!stbi__zbuild_huffman(&a->z_distance, stbi__zdefault_distance, 32)) return 0; + } else { + if (!stbi__compute_huffman_codes(a)) return 0; + } + if (!stbi__parse_huffman_block(a)) return 0; + } + } while (!final); + return 1; +} + +static int stbi__do_zlib(stbi__zbuf *a, char *obuf, int olen, int exp, int parse_header) +{ + a->zout_start = obuf; + a->zout = obuf; + a->zout_end = obuf + olen; + a->z_expandable = exp; + + return stbi__parse_zlib(a, parse_header); +} + +STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen) +{ + stbi__zbuf a; + char *p = (char *) stbi__malloc(initial_size); + if (p == NULL) return NULL; + a.zbuffer = (stbi_uc *) buffer; + a.zbuffer_end = (stbi_uc *) buffer + len; + if (stbi__do_zlib(&a, p, initial_size, 1, 1)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF char *stbi_zlib_decode_malloc(char const *buffer, int len, int *outlen) +{ + return stbi_zlib_decode_malloc_guesssize(buffer, len, 16384, outlen); +} + +STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header) +{ + stbi__zbuf a; + char *p = (char *) stbi__malloc(initial_size); + if (p == NULL) return NULL; + a.zbuffer = (stbi_uc *) buffer; + a.zbuffer_end = (stbi_uc *) buffer + len; + if (stbi__do_zlib(&a, p, initial_size, 1, parse_header)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, char const *ibuffer, int ilen) +{ + stbi__zbuf a; + a.zbuffer = (stbi_uc *) ibuffer; + a.zbuffer_end = (stbi_uc *) ibuffer + ilen; + if (stbi__do_zlib(&a, obuffer, olen, 0, 1)) + return (int) (a.zout - a.zout_start); + else + return -1; +} + +STBIDEF char *stbi_zlib_decode_noheader_malloc(char const *buffer, int len, int *outlen) +{ + stbi__zbuf a; + char *p = (char *) stbi__malloc(16384); + if (p == NULL) return NULL; + a.zbuffer = (stbi_uc *) buffer; + a.zbuffer_end = (stbi_uc *) buffer+len; + if (stbi__do_zlib(&a, p, 16384, 1, 0)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen) +{ + stbi__zbuf a; + a.zbuffer = (stbi_uc *) ibuffer; + a.zbuffer_end = (stbi_uc *) ibuffer + ilen; + if (stbi__do_zlib(&a, obuffer, olen, 0, 0)) + return (int) (a.zout - a.zout_start); + else + return -1; +} +#endif + +// public domain "baseline" PNG decoder v0.10 Sean Barrett 2006-11-18 +// simple implementation +// - only 8-bit samples +// - no CRC checking +// - allocates lots of intermediate memory +// - avoids problem of streaming data between subsystems +// - avoids explicit window management +// performance +// - uses stb_zlib, a PD zlib implementation with fast huffman decoding + +#ifndef STBI_NO_PNG +typedef struct +{ + stbi__uint32 length; + stbi__uint32 type; +} stbi__pngchunk; + +static stbi__pngchunk stbi__get_chunk_header(stbi__context *s) +{ + stbi__pngchunk c; + c.length = stbi__get32be(s); + c.type = stbi__get32be(s); + return c; +} + +static int stbi__check_png_header(stbi__context *s) +{ + static const stbi_uc png_sig[8] = { 137,80,78,71,13,10,26,10 }; + int i; + for (i=0; i < 8; ++i) + if (stbi__get8(s) != png_sig[i]) return stbi__err("bad png sig","Not a PNG"); + return 1; +} + +typedef struct +{ + stbi__context *s; + stbi_uc *idata, *expanded, *out; + int depth; +} stbi__png; + + +enum { + STBI__F_none=0, + STBI__F_sub=1, + STBI__F_up=2, + STBI__F_avg=3, + STBI__F_paeth=4, + // synthetic filter used for first scanline to avoid needing a dummy row of 0s + STBI__F_avg_first +}; + +static stbi_uc first_row_filter[5] = +{ + STBI__F_none, + STBI__F_sub, + STBI__F_none, + STBI__F_avg_first, + STBI__F_sub // Paeth with b=c=0 turns out to be equivalent to sub +}; + +static int stbi__paeth(int a, int b, int c) +{ + // This formulation looks very different from the reference in the PNG spec, but is + // actually equivalent and has favorable data dependencies and admits straightforward + // generation of branch-free code, which helps performance significantly. + int thresh = c*3 - (a + b); + int lo = a < b ? a : b; + int hi = a < b ? b : a; + int t0 = (hi <= thresh) ? lo : c; + int t1 = (thresh <= lo) ? hi : t0; + return t1; +} + +static const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 }; + +// adds an extra all-255 alpha channel +// dest == src is legal +// img_n must be 1 or 3 +static void stbi__create_png_alpha_expand8(stbi_uc *dest, stbi_uc *src, stbi__uint32 x, int img_n) +{ + int i; + // must process data backwards since we allow dest==src + if (img_n == 1) { + for (i=x-1; i >= 0; --i) { + dest[i*2+1] = 255; + dest[i*2+0] = src[i]; + } + } else { + STBI_ASSERT(img_n == 3); + for (i=x-1; i >= 0; --i) { + dest[i*4+3] = 255; + dest[i*4+2] = src[i*3+2]; + dest[i*4+1] = src[i*3+1]; + dest[i*4+0] = src[i*3+0]; + } + } +} + +// create the png data from post-deflated data +static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color) +{ + int bytes = (depth == 16 ? 2 : 1); + stbi__context *s = a->s; + stbi__uint32 i,j,stride = x*out_n*bytes; + stbi__uint32 img_len, img_width_bytes; + stbi_uc *filter_buf; + int all_ok = 1; + int k; + int img_n = s->img_n; // copy it into a local for later + + int output_bytes = out_n*bytes; + int filter_bytes = img_n*bytes; + int width = x; + + STBI_ASSERT(out_n == s->img_n || out_n == s->img_n+1); + a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into + if (!a->out) return stbi__err("outofmem", "Out of memory"); + + // note: error exits here don't need to clean up a->out individually, + // stbi__do_png always does on error. + if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err("too large", "Corrupt PNG"); + img_width_bytes = (((img_n * x * depth) + 7) >> 3); + if (!stbi__mad2sizes_valid(img_width_bytes, y, img_width_bytes)) return stbi__err("too large", "Corrupt PNG"); + img_len = (img_width_bytes + 1) * y; + + // we used to check for exact match between raw_len and img_len on non-interlaced PNGs, + // but issue #276 reported a PNG in the wild that had extra data at the end (all zeros), + // so just check for raw_len < img_len always. + if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG"); + + // Allocate two scan lines worth of filter workspace buffer. + filter_buf = (stbi_uc *) stbi__malloc_mad2(img_width_bytes, 2, 0); + if (!filter_buf) return stbi__err("outofmem", "Out of memory"); + + // Filtering for low-bit-depth images + if (depth < 8) { + filter_bytes = 1; + width = img_width_bytes; + } + + for (j=0; j < y; ++j) { + // cur/prior filter buffers alternate + stbi_uc *cur = filter_buf + (j & 1)*img_width_bytes; + stbi_uc *prior = filter_buf + (~j & 1)*img_width_bytes; + stbi_uc *dest = a->out + stride*j; + int nk = width * filter_bytes; + int filter = *raw++; + + // check filter type + if (filter > 4) { + all_ok = stbi__err("invalid filter","Corrupt PNG"); + break; + } + + // if first row, use special filter that doesn't sample previous row + if (j == 0) filter = first_row_filter[filter]; + + // perform actual filtering + switch (filter) { + case STBI__F_none: + memcpy(cur, raw, nk); + break; + case STBI__F_sub: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); + break; + case STBI__F_up: + for (k = 0; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); + break; + case STBI__F_avg: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); + break; + case STBI__F_paeth: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); // prior[k] == stbi__paeth(0,prior[k],0) + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes], prior[k], prior[k-filter_bytes])); + break; + case STBI__F_avg_first: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); + break; + } + + raw += nk; + + // expand decoded bits in cur to dest, also adding an extra alpha channel if desired + if (depth < 8) { + stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale grayscale values to 0..255 range + stbi_uc *in = cur; + stbi_uc *out = dest; + stbi_uc inb = 0; + stbi__uint32 nsmp = x*img_n; + + // expand bits to bytes first + if (depth == 4) { + for (i=0; i < nsmp; ++i) { + if ((i & 1) == 0) inb = *in++; + *out++ = scale * (inb >> 4); + inb <<= 4; + } + } else if (depth == 2) { + for (i=0; i < nsmp; ++i) { + if ((i & 3) == 0) inb = *in++; + *out++ = scale * (inb >> 6); + inb <<= 2; + } + } else { + STBI_ASSERT(depth == 1); + for (i=0; i < nsmp; ++i) { + if ((i & 7) == 0) inb = *in++; + *out++ = scale * (inb >> 7); + inb <<= 1; + } + } + + // insert alpha=255 values if desired + if (img_n != out_n) + stbi__create_png_alpha_expand8(dest, dest, x, img_n); + } else if (depth == 8) { + if (img_n == out_n) + memcpy(dest, cur, x*img_n); + else + stbi__create_png_alpha_expand8(dest, cur, x, img_n); + } else if (depth == 16) { + // convert the image data from big-endian to platform-native + stbi__uint16 *dest16 = (stbi__uint16*)dest; + stbi__uint32 nsmp = x*img_n; + + if (img_n == out_n) { + for (i = 0; i < nsmp; ++i, ++dest16, cur += 2) + *dest16 = (cur[0] << 8) | cur[1]; + } else { + STBI_ASSERT(img_n+1 == out_n); + if (img_n == 1) { + for (i = 0; i < x; ++i, dest16 += 2, cur += 2) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = 0xffff; + } + } else { + STBI_ASSERT(img_n == 3); + for (i = 0; i < x; ++i, dest16 += 4, cur += 6) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = (cur[2] << 8) | cur[3]; + dest16[2] = (cur[4] << 8) | cur[5]; + dest16[3] = 0xffff; + } + } + } + } + } + + STBI_FREE(filter_buf); + if (!all_ok) return 0; + + return 1; +} + +static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint32 image_data_len, int out_n, int depth, int color, int interlaced) +{ + int bytes = (depth == 16 ? 2 : 1); + int out_bytes = out_n * bytes; + stbi_uc *final; + int p; + if (!interlaced) + return stbi__create_png_image_raw(a, image_data, image_data_len, out_n, a->s->img_x, a->s->img_y, depth, color); + + // de-interlacing + final = (stbi_uc *) stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0); + if (!final) return stbi__err("outofmem", "Out of memory"); + for (p=0; p < 7; ++p) { + int xorig[] = { 0,4,0,2,0,1,0 }; + int yorig[] = { 0,0,4,0,2,0,1 }; + int xspc[] = { 8,8,4,4,2,2,1 }; + int yspc[] = { 8,8,8,4,4,2,2 }; + int i,j,x,y; + // pass1_x[4] = 0, pass1_x[5] = 1, pass1_x[12] = 1 + x = (a->s->img_x - xorig[p] + xspc[p]-1) / xspc[p]; + y = (a->s->img_y - yorig[p] + yspc[p]-1) / yspc[p]; + if (x && y) { + stbi__uint32 img_len = ((((a->s->img_n * x * depth) + 7) >> 3) + 1) * y; + if (!stbi__create_png_image_raw(a, image_data, image_data_len, out_n, x, y, depth, color)) { + STBI_FREE(final); + return 0; + } + for (j=0; j < y; ++j) { + for (i=0; i < x; ++i) { + int out_y = j*yspc[p]+yorig[p]; + int out_x = i*xspc[p]+xorig[p]; + memcpy(final + out_y*a->s->img_x*out_bytes + out_x*out_bytes, + a->out + (j*x+i)*out_bytes, out_bytes); + } + } + STBI_FREE(a->out); + image_data += img_len; + image_data_len -= img_len; + } + } + a->out = final; + + return 1; +} + +static int stbi__compute_transparency(stbi__png *z, stbi_uc tc[3], int out_n) +{ + stbi__context *s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi_uc *p = z->out; + + // compute color-based transparency, assuming we've + // already got 255 as the alpha value in the output + STBI_ASSERT(out_n == 2 || out_n == 4); + + if (out_n == 2) { + for (i=0; i < pixel_count; ++i) { + p[1] = (p[0] == tc[0] ? 0 : 255); + p += 2; + } + } else { + for (i=0; i < pixel_count; ++i) { + if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) + p[3] = 0; + p += 4; + } + } + return 1; +} + +static int stbi__compute_transparency16(stbi__png *z, stbi__uint16 tc[3], int out_n) +{ + stbi__context *s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi__uint16 *p = (stbi__uint16*) z->out; + + // compute color-based transparency, assuming we've + // already got 65535 as the alpha value in the output + STBI_ASSERT(out_n == 2 || out_n == 4); + + if (out_n == 2) { + for (i = 0; i < pixel_count; ++i) { + p[1] = (p[0] == tc[0] ? 0 : 65535); + p += 2; + } + } else { + for (i = 0; i < pixel_count; ++i) { + if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) + p[3] = 0; + p += 4; + } + } + return 1; +} + +static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int pal_img_n) +{ + stbi__uint32 i, pixel_count = a->s->img_x * a->s->img_y; + stbi_uc *p, *temp_out, *orig = a->out; + + p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0); + if (p == NULL) return stbi__err("outofmem", "Out of memory"); + + // between here and free(out) below, exitting would leak + temp_out = p; + + if (pal_img_n == 3) { + for (i=0; i < pixel_count; ++i) { + int n = orig[i]*4; + p[0] = palette[n ]; + p[1] = palette[n+1]; + p[2] = palette[n+2]; + p += 3; + } + } else { + for (i=0; i < pixel_count; ++i) { + int n = orig[i]*4; + p[0] = palette[n ]; + p[1] = palette[n+1]; + p[2] = palette[n+2]; + p[3] = palette[n+3]; + p += 4; + } + } + STBI_FREE(a->out); + a->out = temp_out; + + STBI_NOTUSED(len); + + return 1; +} + +static int stbi__unpremultiply_on_load_global = 0; +static int stbi__de_iphone_flag_global = 0; + +STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply) +{ + stbi__unpremultiply_on_load_global = flag_true_if_should_unpremultiply; +} + +STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert) +{ + stbi__de_iphone_flag_global = flag_true_if_should_convert; +} + +#ifndef STBI_THREAD_LOCAL +#define stbi__unpremultiply_on_load stbi__unpremultiply_on_load_global +#define stbi__de_iphone_flag stbi__de_iphone_flag_global +#else +static STBI_THREAD_LOCAL int stbi__unpremultiply_on_load_local, stbi__unpremultiply_on_load_set; +static STBI_THREAD_LOCAL int stbi__de_iphone_flag_local, stbi__de_iphone_flag_set; + +STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply) +{ + stbi__unpremultiply_on_load_local = flag_true_if_should_unpremultiply; + stbi__unpremultiply_on_load_set = 1; +} + +STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert) +{ + stbi__de_iphone_flag_local = flag_true_if_should_convert; + stbi__de_iphone_flag_set = 1; +} + +#define stbi__unpremultiply_on_load (stbi__unpremultiply_on_load_set \ + ? stbi__unpremultiply_on_load_local \ + : stbi__unpremultiply_on_load_global) +#define stbi__de_iphone_flag (stbi__de_iphone_flag_set \ + ? stbi__de_iphone_flag_local \ + : stbi__de_iphone_flag_global) +#endif // STBI_THREAD_LOCAL + +static void stbi__de_iphone(stbi__png *z) +{ + stbi__context *s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi_uc *p = z->out; + + if (s->img_out_n == 3) { // convert bgr to rgb + for (i=0; i < pixel_count; ++i) { + stbi_uc t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 3; + } + } else { + STBI_ASSERT(s->img_out_n == 4); + if (stbi__unpremultiply_on_load) { + // convert bgr to rgb and unpremultiply + for (i=0; i < pixel_count; ++i) { + stbi_uc a = p[3]; + stbi_uc t = p[0]; + if (a) { + stbi_uc half = a / 2; + p[0] = (p[2] * 255 + half) / a; + p[1] = (p[1] * 255 + half) / a; + p[2] = ( t * 255 + half) / a; + } else { + p[0] = p[2]; + p[2] = t; + } + p += 4; + } + } else { + // convert bgr to rgb + for (i=0; i < pixel_count; ++i) { + stbi_uc t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 4; + } + } + } +} + +#define STBI__PNG_TYPE(a,b,c,d) (((unsigned) (a) << 24) + ((unsigned) (b) << 16) + ((unsigned) (c) << 8) + (unsigned) (d)) + +static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) +{ + stbi_uc palette[1024], pal_img_n=0; + stbi_uc has_trans=0, tc[3]={0}; + stbi__uint16 tc16[3]; + stbi__uint32 ioff=0, idata_limit=0, i, pal_len=0; + int first=1,k,interlace=0, color=0, is_iphone=0; + stbi__context *s = z->s; + + z->expanded = NULL; + z->idata = NULL; + z->out = NULL; + + if (!stbi__check_png_header(s)) return 0; + + if (scan == STBI__SCAN_type) return 1; + + for (;;) { + stbi__pngchunk c = stbi__get_chunk_header(s); + switch (c.type) { + case STBI__PNG_TYPE('C','g','B','I'): + is_iphone = 1; + stbi__skip(s, c.length); + break; + case STBI__PNG_TYPE('I','H','D','R'): { + int comp,filter; + if (!first) return stbi__err("multiple IHDR","Corrupt PNG"); + first = 0; + if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG"); + s->img_x = stbi__get32be(s); + s->img_y = stbi__get32be(s); + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + z->depth = stbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only"); + color = stbi__get8(s); if (color > 6) return stbi__err("bad ctype","Corrupt PNG"); + if (color == 3 && z->depth == 16) return stbi__err("bad ctype","Corrupt PNG"); + if (color == 3) pal_img_n = 3; else if (color & 1) return stbi__err("bad ctype","Corrupt PNG"); + comp = stbi__get8(s); if (comp) return stbi__err("bad comp method","Corrupt PNG"); + filter= stbi__get8(s); if (filter) return stbi__err("bad filter method","Corrupt PNG"); + interlace = stbi__get8(s); if (interlace>1) return stbi__err("bad interlace method","Corrupt PNG"); + if (!s->img_x || !s->img_y) return stbi__err("0-pixel image","Corrupt PNG"); + if (!pal_img_n) { + s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0); + if ((1 << 30) / s->img_x / s->img_n < s->img_y) return stbi__err("too large", "Image too large to decode"); + } else { + // if paletted, then pal_n is our final components, and + // img_n is # components to decompress/filter. + s->img_n = 1; + if ((1 << 30) / s->img_x / 4 < s->img_y) return stbi__err("too large","Corrupt PNG"); + } + // even with SCAN_header, have to scan to see if we have a tRNS + break; + } + + case STBI__PNG_TYPE('P','L','T','E'): { + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (c.length > 256*3) return stbi__err("invalid PLTE","Corrupt PNG"); + pal_len = c.length / 3; + if (pal_len * 3 != c.length) return stbi__err("invalid PLTE","Corrupt PNG"); + for (i=0; i < pal_len; ++i) { + palette[i*4+0] = stbi__get8(s); + palette[i*4+1] = stbi__get8(s); + palette[i*4+2] = stbi__get8(s); + palette[i*4+3] = 255; + } + break; + } + + case STBI__PNG_TYPE('t','R','N','S'): { + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (z->idata) return stbi__err("tRNS after IDAT","Corrupt PNG"); + if (pal_img_n) { + if (scan == STBI__SCAN_header) { s->img_n = 4; return 1; } + if (pal_len == 0) return stbi__err("tRNS before PLTE","Corrupt PNG"); + if (c.length > pal_len) return stbi__err("bad tRNS len","Corrupt PNG"); + pal_img_n = 4; + for (i=0; i < c.length; ++i) + palette[i*4+3] = stbi__get8(s); + } else { + if (!(s->img_n & 1)) return stbi__err("tRNS with alpha","Corrupt PNG"); + if (c.length != (stbi__uint32) s->img_n*2) return stbi__err("bad tRNS len","Corrupt PNG"); + has_trans = 1; + // non-paletted with tRNS = constant alpha. if header-scanning, we can stop now. + if (scan == STBI__SCAN_header) { ++s->img_n; return 1; } + if (z->depth == 16) { + for (k = 0; k < s->img_n && k < 3; ++k) // extra loop test to suppress false GCC warning + tc16[k] = (stbi__uint16)stbi__get16be(s); // copy the values as-is + } else { + for (k = 0; k < s->img_n && k < 3; ++k) + tc[k] = (stbi_uc)(stbi__get16be(s) & 255) * stbi__depth_scale_table[z->depth]; // non 8-bit images will be larger + } + } + break; + } + + case STBI__PNG_TYPE('I','D','A','T'): { + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (pal_img_n && !pal_len) return stbi__err("no PLTE","Corrupt PNG"); + if (scan == STBI__SCAN_header) { + // header scan definitely stops at first IDAT + if (pal_img_n) + s->img_n = pal_img_n; + return 1; + } + if (c.length > (1u << 30)) return stbi__err("IDAT size limit", "IDAT section larger than 2^30 bytes"); + if ((int)(ioff + c.length) < (int)ioff) return 0; + if (ioff + c.length > idata_limit) { + stbi__uint32 idata_limit_old = idata_limit; + stbi_uc *p; + if (idata_limit == 0) idata_limit = c.length > 4096 ? c.length : 4096; + while (ioff + c.length > idata_limit) + idata_limit *= 2; + STBI_NOTUSED(idata_limit_old); + p = (stbi_uc *) STBI_REALLOC_SIZED(z->idata, idata_limit_old, idata_limit); if (p == NULL) return stbi__err("outofmem", "Out of memory"); + z->idata = p; + } + if (!stbi__getn(s, z->idata+ioff,c.length)) return stbi__err("outofdata","Corrupt PNG"); + ioff += c.length; + break; + } + + case STBI__PNG_TYPE('I','E','N','D'): { + stbi__uint32 raw_len, bpl; + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (scan != STBI__SCAN_load) return 1; + if (z->idata == NULL) return stbi__err("no IDAT","Corrupt PNG"); + // initial guess for decoded data size to avoid unnecessary reallocs + bpl = (s->img_x * z->depth + 7) / 8; // bytes per line, per component + raw_len = bpl * s->img_y * s->img_n /* pixels */ + s->img_y /* filter mode per row */; + z->expanded = (stbi_uc *) stbi_zlib_decode_malloc_guesssize_headerflag((char *) z->idata, ioff, raw_len, (int *) &raw_len, !is_iphone); + if (z->expanded == NULL) return 0; // zlib should set error + STBI_FREE(z->idata); z->idata = NULL; + if ((req_comp == s->img_n+1 && req_comp != 3 && !pal_img_n) || has_trans) + s->img_out_n = s->img_n+1; + else + s->img_out_n = s->img_n; + if (!stbi__create_png_image(z, z->expanded, raw_len, s->img_out_n, z->depth, color, interlace)) return 0; + if (has_trans) { + if (z->depth == 16) { + if (!stbi__compute_transparency16(z, tc16, s->img_out_n)) return 0; + } else { + if (!stbi__compute_transparency(z, tc, s->img_out_n)) return 0; + } + } + if (is_iphone && stbi__de_iphone_flag && s->img_out_n > 2) + stbi__de_iphone(z); + if (pal_img_n) { + // pal_img_n == 3 or 4 + s->img_n = pal_img_n; // record the actual colors we had + s->img_out_n = pal_img_n; + if (req_comp >= 3) s->img_out_n = req_comp; + if (!stbi__expand_png_palette(z, palette, pal_len, s->img_out_n)) + return 0; + } else if (has_trans) { + // non-paletted image with tRNS -> source image has (constant) alpha + ++s->img_n; + } + STBI_FREE(z->expanded); z->expanded = NULL; + // end of PNG chunk, read and skip CRC + stbi__get32be(s); + return 1; + } + + default: + // if critical, fail + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if ((c.type & (1 << 29)) == 0) { + #ifndef STBI_NO_FAILURE_STRINGS + // not threadsafe + static char invalid_chunk[] = "XXXX PNG chunk not known"; + invalid_chunk[0] = STBI__BYTECAST(c.type >> 24); + invalid_chunk[1] = STBI__BYTECAST(c.type >> 16); + invalid_chunk[2] = STBI__BYTECAST(c.type >> 8); + invalid_chunk[3] = STBI__BYTECAST(c.type >> 0); + #endif + return stbi__err(invalid_chunk, "PNG not supported: unknown PNG chunk type"); + } + stbi__skip(s, c.length); + break; + } + // end of PNG chunk, read and skip CRC + stbi__get32be(s); + } +} + +static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, stbi__result_info *ri) +{ + void *result=NULL; + if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); + if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { + if (p->depth <= 8) + ri->bits_per_channel = 8; + else if (p->depth == 16) + ri->bits_per_channel = 16; + else + return stbi__errpuc("bad bits_per_channel", "PNG not supported: unsupported color depth"); + result = p->out; + p->out = NULL; + if (req_comp && req_comp != p->s->img_out_n) { + if (ri->bits_per_channel == 8) + result = stbi__convert_format((unsigned char *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); + else + result = stbi__convert_format16((stbi__uint16 *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); + p->s->img_out_n = req_comp; + if (result == NULL) return result; + } + *x = p->s->img_x; + *y = p->s->img_y; + if (n) *n = p->s->img_n; + } + STBI_FREE(p->out); p->out = NULL; + STBI_FREE(p->expanded); p->expanded = NULL; + STBI_FREE(p->idata); p->idata = NULL; + + return result; +} + +static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi__png p; + p.s = s; + return stbi__do_png(&p, x,y,comp,req_comp, ri); +} + +static int stbi__png_test(stbi__context *s) +{ + int r; + r = stbi__check_png_header(s); + stbi__rewind(s); + return r; +} + +static int stbi__png_info_raw(stbi__png *p, int *x, int *y, int *comp) +{ + if (!stbi__parse_png_file(p, STBI__SCAN_header, 0)) { + stbi__rewind( p->s ); + return 0; + } + if (x) *x = p->s->img_x; + if (y) *y = p->s->img_y; + if (comp) *comp = p->s->img_n; + return 1; +} + +static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp) +{ + stbi__png p; + p.s = s; + return stbi__png_info_raw(&p, x, y, comp); +} + +static int stbi__png_is16(stbi__context *s) +{ + stbi__png p; + p.s = s; + if (!stbi__png_info_raw(&p, NULL, NULL, NULL)) + return 0; + if (p.depth != 16) { + stbi__rewind(p.s); + return 0; + } + return 1; +} +#endif + +// Microsoft/Windows BMP image + +#ifndef STBI_NO_BMP +static int stbi__bmp_test_raw(stbi__context *s) +{ + int r; + int sz; + if (stbi__get8(s) != 'B') return 0; + if (stbi__get8(s) != 'M') return 0; + stbi__get32le(s); // discard filesize + stbi__get16le(s); // discard reserved + stbi__get16le(s); // discard reserved + stbi__get32le(s); // discard data offset + sz = stbi__get32le(s); + r = (sz == 12 || sz == 40 || sz == 56 || sz == 108 || sz == 124); + return r; +} + +static int stbi__bmp_test(stbi__context *s) +{ + int r = stbi__bmp_test_raw(s); + stbi__rewind(s); + return r; +} + + +// returns 0..31 for the highest set bit +static int stbi__high_bit(unsigned int z) +{ + int n=0; + if (z == 0) return -1; + if (z >= 0x10000) { n += 16; z >>= 16; } + if (z >= 0x00100) { n += 8; z >>= 8; } + if (z >= 0x00010) { n += 4; z >>= 4; } + if (z >= 0x00004) { n += 2; z >>= 2; } + if (z >= 0x00002) { n += 1;/* >>= 1;*/ } + return n; +} + +static int stbi__bitcount(unsigned int a) +{ + a = (a & 0x55555555) + ((a >> 1) & 0x55555555); // max 2 + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); // max 4 + a = (a + (a >> 4)) & 0x0f0f0f0f; // max 8 per 4, now 8 bits + a = (a + (a >> 8)); // max 16 per 8 bits + a = (a + (a >> 16)); // max 32 per 8 bits + return a & 0xff; +} + +// extract an arbitrarily-aligned N-bit value (N=bits) +// from v, and then make it 8-bits long and fractionally +// extend it to full full range. +static int stbi__shiftsigned(unsigned int v, int shift, int bits) +{ + static unsigned int mul_table[9] = { + 0, + 0xff/*0b11111111*/, 0x55/*0b01010101*/, 0x49/*0b01001001*/, 0x11/*0b00010001*/, + 0x21/*0b00100001*/, 0x41/*0b01000001*/, 0x81/*0b10000001*/, 0x01/*0b00000001*/, + }; + static unsigned int shift_table[9] = { + 0, 0,0,1,0,2,4,6,0, + }; + if (shift < 0) + v <<= -shift; + else + v >>= shift; + STBI_ASSERT(v < 256); + v >>= (8-bits); + STBI_ASSERT(bits >= 0 && bits <= 8); + return (int) ((unsigned) v * mul_table[bits]) >> shift_table[bits]; +} + +typedef struct +{ + int bpp, offset, hsz; + unsigned int mr,mg,mb,ma, all_a; + int extra_read; +} stbi__bmp_data; + +static int stbi__bmp_set_mask_defaults(stbi__bmp_data *info, int compress) +{ + // BI_BITFIELDS specifies masks explicitly, don't override + if (compress == 3) + return 1; + + if (compress == 0) { + if (info->bpp == 16) { + info->mr = 31u << 10; + info->mg = 31u << 5; + info->mb = 31u << 0; + } else if (info->bpp == 32) { + info->mr = 0xffu << 16; + info->mg = 0xffu << 8; + info->mb = 0xffu << 0; + info->ma = 0xffu << 24; + info->all_a = 0; // if all_a is 0 at end, then we loaded alpha channel but it was all 0 + } else { + // otherwise, use defaults, which is all-0 + info->mr = info->mg = info->mb = info->ma = 0; + } + return 1; + } + return 0; // error +} + +static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) +{ + int hsz; + if (stbi__get8(s) != 'B' || stbi__get8(s) != 'M') return stbi__errpuc("not BMP", "Corrupt BMP"); + stbi__get32le(s); // discard filesize + stbi__get16le(s); // discard reserved + stbi__get16le(s); // discard reserved + info->offset = stbi__get32le(s); + info->hsz = hsz = stbi__get32le(s); + info->mr = info->mg = info->mb = info->ma = 0; + info->extra_read = 14; + + if (info->offset < 0) return stbi__errpuc("bad BMP", "bad BMP"); + + if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc("unknown BMP", "BMP type not supported: unknown"); + if (hsz == 12) { + s->img_x = stbi__get16le(s); + s->img_y = stbi__get16le(s); + } else { + s->img_x = stbi__get32le(s); + s->img_y = stbi__get32le(s); + } + if (stbi__get16le(s) != 1) return stbi__errpuc("bad BMP", "bad BMP"); + info->bpp = stbi__get16le(s); + if (hsz != 12) { + int compress = stbi__get32le(s); + if (compress == 1 || compress == 2) return stbi__errpuc("BMP RLE", "BMP type not supported: RLE"); + if (compress >= 4) return stbi__errpuc("BMP JPEG/PNG", "BMP type not supported: unsupported compression"); // this includes PNG/JPEG modes + if (compress == 3 && info->bpp != 16 && info->bpp != 32) return stbi__errpuc("bad BMP", "bad BMP"); // bitfields requires 16 or 32 bits/pixel + stbi__get32le(s); // discard sizeof + stbi__get32le(s); // discard hres + stbi__get32le(s); // discard vres + stbi__get32le(s); // discard colorsused + stbi__get32le(s); // discard max important + if (hsz == 40 || hsz == 56) { + if (hsz == 56) { + stbi__get32le(s); + stbi__get32le(s); + stbi__get32le(s); + stbi__get32le(s); + } + if (info->bpp == 16 || info->bpp == 32) { + if (compress == 0) { + stbi__bmp_set_mask_defaults(info, compress); + } else if (compress == 3) { + info->mr = stbi__get32le(s); + info->mg = stbi__get32le(s); + info->mb = stbi__get32le(s); + info->extra_read += 12; + // not documented, but generated by photoshop and handled by mspaint + if (info->mr == info->mg && info->mg == info->mb) { + // ?!?!? + return stbi__errpuc("bad BMP", "bad BMP"); + } + } else + return stbi__errpuc("bad BMP", "bad BMP"); + } + } else { + // V4/V5 header + int i; + if (hsz != 108 && hsz != 124) + return stbi__errpuc("bad BMP", "bad BMP"); + info->mr = stbi__get32le(s); + info->mg = stbi__get32le(s); + info->mb = stbi__get32le(s); + info->ma = stbi__get32le(s); + if (compress != 3) // override mr/mg/mb unless in BI_BITFIELDS mode, as per docs + stbi__bmp_set_mask_defaults(info, compress); + stbi__get32le(s); // discard color space + for (i=0; i < 12; ++i) + stbi__get32le(s); // discard color space parameters + if (hsz == 124) { + stbi__get32le(s); // discard rendering intent + stbi__get32le(s); // discard offset of profile data + stbi__get32le(s); // discard size of profile data + stbi__get32le(s); // discard reserved + } + } + } + return (void *) 1; +} + + +static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi_uc *out; + unsigned int mr=0,mg=0,mb=0,ma=0, all_a; + stbi_uc pal[256][4]; + int psize=0,i,j,width; + int flip_vertically, pad, target; + stbi__bmp_data info; + STBI_NOTUSED(ri); + + info.all_a = 255; + if (stbi__bmp_parse_header(s, &info) == NULL) + return NULL; // error code already set + + flip_vertically = ((int) s->img_y) > 0; + s->img_y = abs((int) s->img_y); + + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + mr = info.mr; + mg = info.mg; + mb = info.mb; + ma = info.ma; + all_a = info.all_a; + + if (info.hsz == 12) { + if (info.bpp < 24) + psize = (info.offset - info.extra_read - 24) / 3; + } else { + if (info.bpp < 16) + psize = (info.offset - info.extra_read - info.hsz) >> 2; + } + if (psize == 0) { + // accept some number of extra bytes after the header, but if the offset points either to before + // the header ends or implies a large amount of extra data, reject the file as malformed + int bytes_read_so_far = s->callback_already_read + (int)(s->img_buffer - s->img_buffer_original); + int header_limit = 1024; // max we actually read is below 256 bytes currently. + int extra_data_limit = 256*4; // what ordinarily goes here is a palette; 256 entries*4 bytes is its max size. + if (bytes_read_so_far <= 0 || bytes_read_so_far > header_limit) { + return stbi__errpuc("bad header", "Corrupt BMP"); + } + // we established that bytes_read_so_far is positive and sensible. + // the first half of this test rejects offsets that are either too small positives, or + // negative, and guarantees that info.offset >= bytes_read_so_far > 0. this in turn + // ensures the number computed in the second half of the test can't overflow. + if (info.offset < bytes_read_so_far || info.offset - bytes_read_so_far > extra_data_limit) { + return stbi__errpuc("bad offset", "Corrupt BMP"); + } else { + stbi__skip(s, info.offset - bytes_read_so_far); + } + } + + if (info.bpp == 24 && ma == 0xff000000) + s->img_n = 3; + else + s->img_n = ma ? 4 : 3; + if (req_comp && req_comp >= 3) // we can directly decode 3 or 4 + target = req_comp; + else + target = s->img_n; // if they want monochrome, we'll post-convert + + // sanity-check size + if (!stbi__mad3sizes_valid(target, s->img_x, s->img_y, 0)) + return stbi__errpuc("too large", "Corrupt BMP"); + + out = (stbi_uc *) stbi__malloc_mad3(target, s->img_x, s->img_y, 0); + if (!out) return stbi__errpuc("outofmem", "Out of memory"); + if (info.bpp < 16) { + int z=0; + if (psize == 0 || psize > 256) { STBI_FREE(out); return stbi__errpuc("invalid", "Corrupt BMP"); } + for (i=0; i < psize; ++i) { + pal[i][2] = stbi__get8(s); + pal[i][1] = stbi__get8(s); + pal[i][0] = stbi__get8(s); + if (info.hsz != 12) stbi__get8(s); + pal[i][3] = 255; + } + stbi__skip(s, info.offset - info.extra_read - info.hsz - psize * (info.hsz == 12 ? 3 : 4)); + if (info.bpp == 1) width = (s->img_x + 7) >> 3; + else if (info.bpp == 4) width = (s->img_x + 1) >> 1; + else if (info.bpp == 8) width = s->img_x; + else { STBI_FREE(out); return stbi__errpuc("bad bpp", "Corrupt BMP"); } + pad = (-width)&3; + if (info.bpp == 1) { + for (j=0; j < (int) s->img_y; ++j) { + int bit_offset = 7, v = stbi__get8(s); + for (i=0; i < (int) s->img_x; ++i) { + int color = (v>>bit_offset)&0x1; + out[z++] = pal[color][0]; + out[z++] = pal[color][1]; + out[z++] = pal[color][2]; + if (target == 4) out[z++] = 255; + if (i+1 == (int) s->img_x) break; + if((--bit_offset) < 0) { + bit_offset = 7; + v = stbi__get8(s); + } + } + stbi__skip(s, pad); + } + } else { + for (j=0; j < (int) s->img_y; ++j) { + for (i=0; i < (int) s->img_x; i += 2) { + int v=stbi__get8(s),v2=0; + if (info.bpp == 4) { + v2 = v & 15; + v >>= 4; + } + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) out[z++] = 255; + if (i+1 == (int) s->img_x) break; + v = (info.bpp == 8) ? stbi__get8(s) : v2; + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) out[z++] = 255; + } + stbi__skip(s, pad); + } + } + } else { + int rshift=0,gshift=0,bshift=0,ashift=0,rcount=0,gcount=0,bcount=0,acount=0; + int z = 0; + int easy=0; + stbi__skip(s, info.offset - info.extra_read - info.hsz); + if (info.bpp == 24) width = 3 * s->img_x; + else if (info.bpp == 16) width = 2*s->img_x; + else /* bpp = 32 and pad = 0 */ width=0; + pad = (-width) & 3; + if (info.bpp == 24) { + easy = 1; + } else if (info.bpp == 32) { + if (mb == 0xff && mg == 0xff00 && mr == 0x00ff0000 && ma == 0xff000000) + easy = 2; + } + if (!easy) { + if (!mr || !mg || !mb) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); } + // right shift amt to put high bit in position #7 + rshift = stbi__high_bit(mr)-7; rcount = stbi__bitcount(mr); + gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg); + bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb); + ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma); + if (rcount > 8 || gcount > 8 || bcount > 8 || acount > 8) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); } + } + for (j=0; j < (int) s->img_y; ++j) { + if (easy) { + for (i=0; i < (int) s->img_x; ++i) { + unsigned char a; + out[z+2] = stbi__get8(s); + out[z+1] = stbi__get8(s); + out[z+0] = stbi__get8(s); + z += 3; + a = (easy == 2 ? stbi__get8(s) : 255); + all_a |= a; + if (target == 4) out[z++] = a; + } + } else { + int bpp = info.bpp; + for (i=0; i < (int) s->img_x; ++i) { + stbi__uint32 v = (bpp == 16 ? (stbi__uint32) stbi__get16le(s) : stbi__get32le(s)); + unsigned int a; + out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mr, rshift, rcount)); + out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mg, gshift, gcount)); + out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mb, bshift, bcount)); + a = (ma ? stbi__shiftsigned(v & ma, ashift, acount) : 255); + all_a |= a; + if (target == 4) out[z++] = STBI__BYTECAST(a); + } + } + stbi__skip(s, pad); + } + } + + // if alpha channel is all 0s, replace with all 255s + if (target == 4 && all_a == 0) + for (i=4*s->img_x*s->img_y-1; i >= 0; i -= 4) + out[i] = 255; + + if (flip_vertically) { + stbi_uc t; + for (j=0; j < (int) s->img_y>>1; ++j) { + stbi_uc *p1 = out + j *s->img_x*target; + stbi_uc *p2 = out + (s->img_y-1-j)*s->img_x*target; + for (i=0; i < (int) s->img_x*target; ++i) { + t = p1[i]; p1[i] = p2[i]; p2[i] = t; + } + } + } + + if (req_comp && req_comp != target) { + out = stbi__convert_format(out, target, req_comp, s->img_x, s->img_y); + if (out == NULL) return out; // stbi__convert_format frees input on failure + } + + *x = s->img_x; + *y = s->img_y; + if (comp) *comp = s->img_n; + return out; +} +#endif + +// Targa Truevision - TGA +// by Jonathan Dummer +#ifndef STBI_NO_TGA +// returns STBI_rgb or whatever, 0 on error +static int stbi__tga_get_comp(int bits_per_pixel, int is_grey, int* is_rgb16) +{ + // only RGB or RGBA (incl. 16bit) or grey allowed + if (is_rgb16) *is_rgb16 = 0; + switch(bits_per_pixel) { + case 8: return STBI_grey; + case 16: if(is_grey) return STBI_grey_alpha; + // fallthrough + case 15: if(is_rgb16) *is_rgb16 = 1; + return STBI_rgb; + case 24: // fallthrough + case 32: return bits_per_pixel/8; + default: return 0; + } +} + +static int stbi__tga_info(stbi__context *s, int *x, int *y, int *comp) +{ + int tga_w, tga_h, tga_comp, tga_image_type, tga_bits_per_pixel, tga_colormap_bpp; + int sz, tga_colormap_type; + stbi__get8(s); // discard Offset + tga_colormap_type = stbi__get8(s); // colormap type + if( tga_colormap_type > 1 ) { + stbi__rewind(s); + return 0; // only RGB or indexed allowed + } + tga_image_type = stbi__get8(s); // image type + if ( tga_colormap_type == 1 ) { // colormapped (paletted) image + if (tga_image_type != 1 && tga_image_type != 9) { + stbi__rewind(s); + return 0; + } + stbi__skip(s,4); // skip index of first colormap entry and number of entries + sz = stbi__get8(s); // check bits per palette color entry + if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) { + stbi__rewind(s); + return 0; + } + stbi__skip(s,4); // skip image x and y origin + tga_colormap_bpp = sz; + } else { // "normal" image w/o colormap - only RGB or grey allowed, +/- RLE + if ( (tga_image_type != 2) && (tga_image_type != 3) && (tga_image_type != 10) && (tga_image_type != 11) ) { + stbi__rewind(s); + return 0; // only RGB or grey allowed, +/- RLE + } + stbi__skip(s,9); // skip colormap specification and image x/y origin + tga_colormap_bpp = 0; + } + tga_w = stbi__get16le(s); + if( tga_w < 1 ) { + stbi__rewind(s); + return 0; // test width + } + tga_h = stbi__get16le(s); + if( tga_h < 1 ) { + stbi__rewind(s); + return 0; // test height + } + tga_bits_per_pixel = stbi__get8(s); // bits per pixel + stbi__get8(s); // ignore alpha bits + if (tga_colormap_bpp != 0) { + if((tga_bits_per_pixel != 8) && (tga_bits_per_pixel != 16)) { + // when using a colormap, tga_bits_per_pixel is the size of the indexes + // I don't think anything but 8 or 16bit indexes makes sense + stbi__rewind(s); + return 0; + } + tga_comp = stbi__tga_get_comp(tga_colormap_bpp, 0, NULL); + } else { + tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3) || (tga_image_type == 11), NULL); + } + if(!tga_comp) { + stbi__rewind(s); + return 0; + } + if (x) *x = tga_w; + if (y) *y = tga_h; + if (comp) *comp = tga_comp; + return 1; // seems to have passed everything +} + +static int stbi__tga_test(stbi__context *s) +{ + int res = 0; + int sz, tga_color_type; + stbi__get8(s); // discard Offset + tga_color_type = stbi__get8(s); // color type + if ( tga_color_type > 1 ) goto errorEnd; // only RGB or indexed allowed + sz = stbi__get8(s); // image type + if ( tga_color_type == 1 ) { // colormapped (paletted) image + if (sz != 1 && sz != 9) goto errorEnd; // colortype 1 demands image type 1 or 9 + stbi__skip(s,4); // skip index of first colormap entry and number of entries + sz = stbi__get8(s); // check bits per palette color entry + if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd; + stbi__skip(s,4); // skip image x and y origin + } else { // "normal" image w/o colormap + if ( (sz != 2) && (sz != 3) && (sz != 10) && (sz != 11) ) goto errorEnd; // only RGB or grey allowed, +/- RLE + stbi__skip(s,9); // skip colormap specification and image x/y origin + } + if ( stbi__get16le(s) < 1 ) goto errorEnd; // test width + if ( stbi__get16le(s) < 1 ) goto errorEnd; // test height + sz = stbi__get8(s); // bits per pixel + if ( (tga_color_type == 1) && (sz != 8) && (sz != 16) ) goto errorEnd; // for colormapped images, bpp is size of an index + if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd; + + res = 1; // if we got this far, everything's good and we can return 1 instead of 0 + +errorEnd: + stbi__rewind(s); + return res; +} + +// read 16bit value and convert to 24bit RGB +static void stbi__tga_read_rgb16(stbi__context *s, stbi_uc* out) +{ + stbi__uint16 px = (stbi__uint16)stbi__get16le(s); + stbi__uint16 fiveBitMask = 31; + // we have 3 channels with 5bits each + int r = (px >> 10) & fiveBitMask; + int g = (px >> 5) & fiveBitMask; + int b = px & fiveBitMask; + // Note that this saves the data in RGB(A) order, so it doesn't need to be swapped later + out[0] = (stbi_uc)((r * 255)/31); + out[1] = (stbi_uc)((g * 255)/31); + out[2] = (stbi_uc)((b * 255)/31); + + // some people claim that the most significant bit might be used for alpha + // (possibly if an alpha-bit is set in the "image descriptor byte") + // but that only made 16bit test images completely translucent.. + // so let's treat all 15 and 16bit TGAs as RGB with no alpha. +} + +static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + // read in the TGA header stuff + int tga_offset = stbi__get8(s); + int tga_indexed = stbi__get8(s); + int tga_image_type = stbi__get8(s); + int tga_is_RLE = 0; + int tga_palette_start = stbi__get16le(s); + int tga_palette_len = stbi__get16le(s); + int tga_palette_bits = stbi__get8(s); + int tga_x_origin = stbi__get16le(s); + int tga_y_origin = stbi__get16le(s); + int tga_width = stbi__get16le(s); + int tga_height = stbi__get16le(s); + int tga_bits_per_pixel = stbi__get8(s); + int tga_comp, tga_rgb16=0; + int tga_inverted = stbi__get8(s); + // int tga_alpha_bits = tga_inverted & 15; // the 4 lowest bits - unused (useless?) + // image data + unsigned char *tga_data; + unsigned char *tga_palette = NULL; + int i, j; + unsigned char raw_data[4] = {0}; + int RLE_count = 0; + int RLE_repeating = 0; + int read_next_pixel = 1; + STBI_NOTUSED(ri); + STBI_NOTUSED(tga_x_origin); // @TODO + STBI_NOTUSED(tga_y_origin); // @TODO + + if (tga_height > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (tga_width > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + // do a tiny bit of precessing + if ( tga_image_type >= 8 ) + { + tga_image_type -= 8; + tga_is_RLE = 1; + } + tga_inverted = 1 - ((tga_inverted >> 5) & 1); + + // If I'm paletted, then I'll use the number of bits from the palette + if ( tga_indexed ) tga_comp = stbi__tga_get_comp(tga_palette_bits, 0, &tga_rgb16); + else tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3), &tga_rgb16); + + if(!tga_comp) // shouldn't really happen, stbi__tga_test() should have ensured basic consistency + return stbi__errpuc("bad format", "Can't find out TGA pixelformat"); + + // tga info + *x = tga_width; + *y = tga_height; + if (comp) *comp = tga_comp; + + if (!stbi__mad3sizes_valid(tga_width, tga_height, tga_comp, 0)) + return stbi__errpuc("too large", "Corrupt TGA"); + + tga_data = (unsigned char*)stbi__malloc_mad3(tga_width, tga_height, tga_comp, 0); + if (!tga_data) return stbi__errpuc("outofmem", "Out of memory"); + + // skip to the data's starting position (offset usually = 0) + stbi__skip(s, tga_offset ); + + if ( !tga_indexed && !tga_is_RLE && !tga_rgb16 ) { + for (i=0; i < tga_height; ++i) { + int row = tga_inverted ? tga_height -i - 1 : i; + stbi_uc *tga_row = tga_data + row*tga_width*tga_comp; + stbi__getn(s, tga_row, tga_width * tga_comp); + } + } else { + // do I need to load a palette? + if ( tga_indexed) + { + if (tga_palette_len == 0) { /* you have to have at least one entry! */ + STBI_FREE(tga_data); + return stbi__errpuc("bad palette", "Corrupt TGA"); + } + + // any data to skip? (offset usually = 0) + stbi__skip(s, tga_palette_start ); + // load the palette + tga_palette = (unsigned char*)stbi__malloc_mad2(tga_palette_len, tga_comp, 0); + if (!tga_palette) { + STBI_FREE(tga_data); + return stbi__errpuc("outofmem", "Out of memory"); + } + if (tga_rgb16) { + stbi_uc *pal_entry = tga_palette; + STBI_ASSERT(tga_comp == STBI_rgb); + for (i=0; i < tga_palette_len; ++i) { + stbi__tga_read_rgb16(s, pal_entry); + pal_entry += tga_comp; + } + } else if (!stbi__getn(s, tga_palette, tga_palette_len * tga_comp)) { + STBI_FREE(tga_data); + STBI_FREE(tga_palette); + return stbi__errpuc("bad palette", "Corrupt TGA"); + } + } + // load the data + for (i=0; i < tga_width * tga_height; ++i) + { + // if I'm in RLE mode, do I need to get a RLE stbi__pngchunk? + if ( tga_is_RLE ) + { + if ( RLE_count == 0 ) + { + // yep, get the next byte as a RLE command + int RLE_cmd = stbi__get8(s); + RLE_count = 1 + (RLE_cmd & 127); + RLE_repeating = RLE_cmd >> 7; + read_next_pixel = 1; + } else if ( !RLE_repeating ) + { + read_next_pixel = 1; + } + } else + { + read_next_pixel = 1; + } + // OK, if I need to read a pixel, do it now + if ( read_next_pixel ) + { + // load however much data we did have + if ( tga_indexed ) + { + // read in index, then perform the lookup + int pal_idx = (tga_bits_per_pixel == 8) ? stbi__get8(s) : stbi__get16le(s); + if ( pal_idx >= tga_palette_len ) { + // invalid index + pal_idx = 0; + } + pal_idx *= tga_comp; + for (j = 0; j < tga_comp; ++j) { + raw_data[j] = tga_palette[pal_idx+j]; + } + } else if(tga_rgb16) { + STBI_ASSERT(tga_comp == STBI_rgb); + stbi__tga_read_rgb16(s, raw_data); + } else { + // read in the data raw + for (j = 0; j < tga_comp; ++j) { + raw_data[j] = stbi__get8(s); + } + } + // clear the reading flag for the next pixel + read_next_pixel = 0; + } // end of reading a pixel + + // copy data + for (j = 0; j < tga_comp; ++j) + tga_data[i*tga_comp+j] = raw_data[j]; + + // in case we're in RLE mode, keep counting down + --RLE_count; + } + // do I need to invert the image? + if ( tga_inverted ) + { + for (j = 0; j*2 < tga_height; ++j) + { + int index1 = j * tga_width * tga_comp; + int index2 = (tga_height - 1 - j) * tga_width * tga_comp; + for (i = tga_width * tga_comp; i > 0; --i) + { + unsigned char temp = tga_data[index1]; + tga_data[index1] = tga_data[index2]; + tga_data[index2] = temp; + ++index1; + ++index2; + } + } + } + // clear my palette, if I had one + if ( tga_palette != NULL ) + { + STBI_FREE( tga_palette ); + } + } + + // swap RGB - if the source data was RGB16, it already is in the right order + if (tga_comp >= 3 && !tga_rgb16) + { + unsigned char* tga_pixel = tga_data; + for (i=0; i < tga_width * tga_height; ++i) + { + unsigned char temp = tga_pixel[0]; + tga_pixel[0] = tga_pixel[2]; + tga_pixel[2] = temp; + tga_pixel += tga_comp; + } + } + + // convert to target component count + if (req_comp && req_comp != tga_comp) + tga_data = stbi__convert_format(tga_data, tga_comp, req_comp, tga_width, tga_height); + + // the things I do to get rid of an error message, and yet keep + // Microsoft's C compilers happy... [8^( + tga_palette_start = tga_palette_len = tga_palette_bits = + tga_x_origin = tga_y_origin = 0; + STBI_NOTUSED(tga_palette_start); + // OK, done + return tga_data; +} +#endif + +// ************************************************************************************************* +// Photoshop PSD loader -- PD by Thatcher Ulrich, integration by Nicolas Schulz, tweaked by STB + +#ifndef STBI_NO_PSD +static int stbi__psd_test(stbi__context *s) +{ + int r = (stbi__get32be(s) == 0x38425053); + stbi__rewind(s); + return r; +} + +static int stbi__psd_decode_rle(stbi__context *s, stbi_uc *p, int pixelCount) +{ + int count, nleft, len; + + count = 0; + while ((nleft = pixelCount - count) > 0) { + len = stbi__get8(s); + if (len == 128) { + // No-op. + } else if (len < 128) { + // Copy next len+1 bytes literally. + len++; + if (len > nleft) return 0; // corrupt data + count += len; + while (len) { + *p = stbi__get8(s); + p += 4; + len--; + } + } else if (len > 128) { + stbi_uc val; + // Next -len+1 bytes in the dest are replicated from next source byte. + // (Interpret len as a negative 8-bit int.) + len = 257 - len; + if (len > nleft) return 0; // corrupt data + val = stbi__get8(s); + count += len; + while (len) { + *p = val; + p += 4; + len--; + } + } + } + + return 1; +} + +static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) +{ + int pixelCount; + int channelCount, compression; + int channel, i; + int bitdepth; + int w,h; + stbi_uc *out; + STBI_NOTUSED(ri); + + // Check identifier + if (stbi__get32be(s) != 0x38425053) // "8BPS" + return stbi__errpuc("not PSD", "Corrupt PSD image"); + + // Check file type version. + if (stbi__get16be(s) != 1) + return stbi__errpuc("wrong version", "Unsupported version of PSD image"); + + // Skip 6 reserved bytes. + stbi__skip(s, 6 ); + + // Read the number of channels (R, G, B, A, etc). + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) + return stbi__errpuc("wrong channel count", "Unsupported number of channels in PSD image"); + + // Read the rows and columns of the image. + h = stbi__get32be(s); + w = stbi__get32be(s); + + if (h > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (w > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + // Make sure the depth is 8 bits. + bitdepth = stbi__get16be(s); + if (bitdepth != 8 && bitdepth != 16) + return stbi__errpuc("unsupported bit depth", "PSD bit depth is not 8 or 16 bit"); + + // Make sure the color mode is RGB. + // Valid options are: + // 0: Bitmap + // 1: Grayscale + // 2: Indexed color + // 3: RGB color + // 4: CMYK color + // 7: Multichannel + // 8: Duotone + // 9: Lab color + if (stbi__get16be(s) != 3) + return stbi__errpuc("wrong color format", "PSD is not in RGB color format"); + + // Skip the Mode Data. (It's the palette for indexed color; other info for other modes.) + stbi__skip(s,stbi__get32be(s) ); + + // Skip the image resources. (resolution, pen tool paths, etc) + stbi__skip(s, stbi__get32be(s) ); + + // Skip the reserved data. + stbi__skip(s, stbi__get32be(s) ); + + // Find out if the data is compressed. + // Known values: + // 0: no compression + // 1: RLE compressed + compression = stbi__get16be(s); + if (compression > 1) + return stbi__errpuc("bad compression", "PSD has an unknown compression format"); + + // Check size + if (!stbi__mad3sizes_valid(4, w, h, 0)) + return stbi__errpuc("too large", "Corrupt PSD"); + + // Create the destination image. + + if (!compression && bitdepth == 16 && bpc == 16) { + out = (stbi_uc *) stbi__malloc_mad3(8, w, h, 0); + ri->bits_per_channel = 16; + } else + out = (stbi_uc *) stbi__malloc(4 * w*h); + + if (!out) return stbi__errpuc("outofmem", "Out of memory"); + pixelCount = w*h; + + // Initialize the data to zero. + //memset( out, 0, pixelCount * 4 ); + + // Finally, the image data. + if (compression) { + // RLE as used by .PSD and .TIFF + // Loop until you get the number of unpacked bytes you are expecting: + // Read the next source byte into n. + // If n is between 0 and 127 inclusive, copy the next n+1 bytes literally. + // Else if n is between -127 and -1 inclusive, copy the next byte -n+1 times. + // Else if n is 128, noop. + // Endloop + + // The RLE-compressed data is preceded by a 2-byte data count for each row in the data, + // which we're going to just skip. + stbi__skip(s, h * channelCount * 2 ); + + // Read the RLE data by channel. + for (channel = 0; channel < 4; channel++) { + stbi_uc *p; + + p = out+channel; + if (channel >= channelCount) { + // Fill this channel with default data. + for (i = 0; i < pixelCount; i++, p += 4) + *p = (channel == 3 ? 255 : 0); + } else { + // Read the RLE data. + if (!stbi__psd_decode_rle(s, p, pixelCount)) { + STBI_FREE(out); + return stbi__errpuc("corrupt", "bad RLE data"); + } + } + } + + } else { + // We're at the raw image data. It's each channel in order (Red, Green, Blue, Alpha, ...) + // where each channel consists of an 8-bit (or 16-bit) value for each pixel in the image. + + // Read the data by channel. + for (channel = 0; channel < 4; channel++) { + if (channel >= channelCount) { + // Fill this channel with default data. + if (bitdepth == 16 && bpc == 16) { + stbi__uint16 *q = ((stbi__uint16 *) out) + channel; + stbi__uint16 val = channel == 3 ? 65535 : 0; + for (i = 0; i < pixelCount; i++, q += 4) + *q = val; + } else { + stbi_uc *p = out+channel; + stbi_uc val = channel == 3 ? 255 : 0; + for (i = 0; i < pixelCount; i++, p += 4) + *p = val; + } + } else { + if (ri->bits_per_channel == 16) { // output bpc + stbi__uint16 *q = ((stbi__uint16 *) out) + channel; + for (i = 0; i < pixelCount; i++, q += 4) + *q = (stbi__uint16) stbi__get16be(s); + } else { + stbi_uc *p = out+channel; + if (bitdepth == 16) { // input bpc + for (i = 0; i < pixelCount; i++, p += 4) + *p = (stbi_uc) (stbi__get16be(s) >> 8); + } else { + for (i = 0; i < pixelCount; i++, p += 4) + *p = stbi__get8(s); + } + } + } + } + } + + // remove weird white matte from PSD + if (channelCount >= 4) { + if (ri->bits_per_channel == 16) { + for (i=0; i < w*h; ++i) { + stbi__uint16 *pixel = (stbi__uint16 *) out + 4*i; + if (pixel[3] != 0 && pixel[3] != 65535) { + float a = pixel[3] / 65535.0f; + float ra = 1.0f / a; + float inv_a = 65535.0f * (1 - ra); + pixel[0] = (stbi__uint16) (pixel[0]*ra + inv_a); + pixel[1] = (stbi__uint16) (pixel[1]*ra + inv_a); + pixel[2] = (stbi__uint16) (pixel[2]*ra + inv_a); + } + } + } else { + for (i=0; i < w*h; ++i) { + unsigned char *pixel = out + 4*i; + if (pixel[3] != 0 && pixel[3] != 255) { + float a = pixel[3] / 255.0f; + float ra = 1.0f / a; + float inv_a = 255.0f * (1 - ra); + pixel[0] = (unsigned char) (pixel[0]*ra + inv_a); + pixel[1] = (unsigned char) (pixel[1]*ra + inv_a); + pixel[2] = (unsigned char) (pixel[2]*ra + inv_a); + } + } + } + } + + // convert to desired output format + if (req_comp && req_comp != 4) { + if (ri->bits_per_channel == 16) + out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, 4, req_comp, w, h); + else + out = stbi__convert_format(out, 4, req_comp, w, h); + if (out == NULL) return out; // stbi__convert_format frees input on failure + } + + if (comp) *comp = 4; + *y = h; + *x = w; + + return out; +} +#endif + +// ************************************************************************************************* +// Softimage PIC loader +// by Tom Seddon +// +// See http://softimage.wiki.softimage.com/index.php/INFO:_PIC_file_format +// See http://ozviz.wasp.uwa.edu.au/~pbourke/dataformats/softimagepic/ + +#ifndef STBI_NO_PIC +static int stbi__pic_is4(stbi__context *s,const char *str) +{ + int i; + for (i=0; i<4; ++i) + if (stbi__get8(s) != (stbi_uc)str[i]) + return 0; + + return 1; +} + +static int stbi__pic_test_core(stbi__context *s) +{ + int i; + + if (!stbi__pic_is4(s,"\x53\x80\xF6\x34")) + return 0; + + for(i=0;i<84;++i) + stbi__get8(s); + + if (!stbi__pic_is4(s,"PICT")) + return 0; + + return 1; +} + +typedef struct +{ + stbi_uc size,type,channel; +} stbi__pic_packet; + +static stbi_uc *stbi__readval(stbi__context *s, int channel, stbi_uc *dest) +{ + int mask=0x80, i; + + for (i=0; i<4; ++i, mask>>=1) { + if (channel & mask) { + if (stbi__at_eof(s)) return stbi__errpuc("bad file","PIC file too short"); + dest[i]=stbi__get8(s); + } + } + + return dest; +} + +static void stbi__copyval(int channel,stbi_uc *dest,const stbi_uc *src) +{ + int mask=0x80,i; + + for (i=0;i<4; ++i, mask>>=1) + if (channel&mask) + dest[i]=src[i]; +} + +static stbi_uc *stbi__pic_load_core(stbi__context *s,int width,int height,int *comp, stbi_uc *result) +{ + int act_comp=0,num_packets=0,y,chained; + stbi__pic_packet packets[10]; + + // this will (should...) cater for even some bizarre stuff like having data + // for the same channel in multiple packets. + do { + stbi__pic_packet *packet; + + if (num_packets==sizeof(packets)/sizeof(packets[0])) + return stbi__errpuc("bad format","too many packets"); + + packet = &packets[num_packets++]; + + chained = stbi__get8(s); + packet->size = stbi__get8(s); + packet->type = stbi__get8(s); + packet->channel = stbi__get8(s); + + act_comp |= packet->channel; + + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (reading packets)"); + if (packet->size != 8) return stbi__errpuc("bad format","packet isn't 8bpp"); + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); // has alpha channel? + + for(y=0; y<height; ++y) { + int packet_idx; + + for(packet_idx=0; packet_idx < num_packets; ++packet_idx) { + stbi__pic_packet *packet = &packets[packet_idx]; + stbi_uc *dest = result+y*width*4; + + switch (packet->type) { + default: + return stbi__errpuc("bad format","packet has bad compression type"); + + case 0: {//uncompressed + int x; + + for(x=0;x<width;++x, dest+=4) + if (!stbi__readval(s,packet->channel,dest)) + return 0; + break; + } + + case 1://Pure RLE + { + int left=width, i; + + while (left>0) { + stbi_uc count,value[4]; + + count=stbi__get8(s); + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pure read count)"); + + if (count > left) + count = (stbi_uc) left; + + if (!stbi__readval(s,packet->channel,value)) return 0; + + for(i=0; i<count; ++i,dest+=4) + stbi__copyval(packet->channel,dest,value); + left -= count; + } + } + break; + + case 2: {//Mixed RLE + int left=width; + while (left>0) { + int count = stbi__get8(s), i; + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (mixed read count)"); + + if (count >= 128) { // Repeated + stbi_uc value[4]; + + if (count==128) + count = stbi__get16be(s); + else + count -= 127; + if (count > left) + return stbi__errpuc("bad file","scanline overrun"); + + if (!stbi__readval(s,packet->channel,value)) + return 0; + + for(i=0;i<count;++i, dest += 4) + stbi__copyval(packet->channel,dest,value); + } else { // Raw + ++count; + if (count>left) return stbi__errpuc("bad file","scanline overrun"); + + for(i=0;i<count;++i, dest+=4) + if (!stbi__readval(s,packet->channel,dest)) + return 0; + } + left-=count; + } + break; + } + } + } + } + + return result; +} + +static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_comp, stbi__result_info *ri) +{ + stbi_uc *result; + int i, x,y, internal_comp; + STBI_NOTUSED(ri); + + if (!comp) comp = &internal_comp; + + for (i=0; i<92; ++i) + stbi__get8(s); + + x = stbi__get16be(s); + y = stbi__get16be(s); + + if (y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pic header)"); + if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc("too large", "PIC image too large to decode"); + + stbi__get32be(s); //skip `ratio' + stbi__get16be(s); //skip `fields' + stbi__get16be(s); //skip `pad' + + // intermediate buffer is RGBA + result = (stbi_uc *) stbi__malloc_mad3(x, y, 4, 0); + if (!result) return stbi__errpuc("outofmem", "Out of memory"); + memset(result, 0xff, x*y*4); + + if (!stbi__pic_load_core(s,x,y,comp, result)) { + STBI_FREE(result); + result=0; + } + *px = x; + *py = y; + if (req_comp == 0) req_comp = *comp; + result=stbi__convert_format(result,4,req_comp,x,y); + + return result; +} + +static int stbi__pic_test(stbi__context *s) +{ + int r = stbi__pic_test_core(s); + stbi__rewind(s); + return r; +} +#endif + +// ************************************************************************************************* +// GIF loader -- public domain by Jean-Marc Lienher -- simplified/shrunk by stb + +#ifndef STBI_NO_GIF +typedef struct +{ + stbi__int16 prefix; + stbi_uc first; + stbi_uc suffix; +} stbi__gif_lzw; + +typedef struct +{ + int w,h; + stbi_uc *out; // output buffer (always 4 components) + stbi_uc *background; // The current "background" as far as a gif is concerned + stbi_uc *history; + int flags, bgindex, ratio, transparent, eflags; + stbi_uc pal[256][4]; + stbi_uc lpal[256][4]; + stbi__gif_lzw codes[8192]; + stbi_uc *color_table; + int parse, step; + int lflags; + int start_x, start_y; + int max_x, max_y; + int cur_x, cur_y; + int line_size; + int delay; +} stbi__gif; + +static int stbi__gif_test_raw(stbi__context *s) +{ + int sz; + if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') return 0; + sz = stbi__get8(s); + if (sz != '9' && sz != '7') return 0; + if (stbi__get8(s) != 'a') return 0; + return 1; +} + +static int stbi__gif_test(stbi__context *s) +{ + int r = stbi__gif_test_raw(s); + stbi__rewind(s); + return r; +} + +static void stbi__gif_parse_colortable(stbi__context *s, stbi_uc pal[256][4], int num_entries, int transp) +{ + int i; + for (i=0; i < num_entries; ++i) { + pal[i][2] = stbi__get8(s); + pal[i][1] = stbi__get8(s); + pal[i][0] = stbi__get8(s); + pal[i][3] = transp == i ? 0 : 255; + } +} + +static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_info) +{ + stbi_uc version; + if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') + return stbi__err("not GIF", "Corrupt GIF"); + + version = stbi__get8(s); + if (version != '7' && version != '9') return stbi__err("not GIF", "Corrupt GIF"); + if (stbi__get8(s) != 'a') return stbi__err("not GIF", "Corrupt GIF"); + + stbi__g_failure_reason = ""; + g->w = stbi__get16le(s); + g->h = stbi__get16le(s); + g->flags = stbi__get8(s); + g->bgindex = stbi__get8(s); + g->ratio = stbi__get8(s); + g->transparent = -1; + + if (g->w > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + if (g->h > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + + if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments + + if (is_info) return 1; + + if (g->flags & 0x80) + stbi__gif_parse_colortable(s,g->pal, 2 << (g->flags & 7), -1); + + return 1; +} + +static int stbi__gif_info_raw(stbi__context *s, int *x, int *y, int *comp) +{ + stbi__gif* g = (stbi__gif*) stbi__malloc(sizeof(stbi__gif)); + if (!g) return stbi__err("outofmem", "Out of memory"); + if (!stbi__gif_header(s, g, comp, 1)) { + STBI_FREE(g); + stbi__rewind( s ); + return 0; + } + if (x) *x = g->w; + if (y) *y = g->h; + STBI_FREE(g); + return 1; +} + +static void stbi__out_gif_code(stbi__gif *g, stbi__uint16 code) +{ + stbi_uc *p, *c; + int idx; + + // recurse to decode the prefixes, since the linked-list is backwards, + // and working backwards through an interleaved image would be nasty + if (g->codes[code].prefix >= 0) + stbi__out_gif_code(g, g->codes[code].prefix); + + if (g->cur_y >= g->max_y) return; + + idx = g->cur_x + g->cur_y; + p = &g->out[idx]; + g->history[idx / 4] = 1; + + c = &g->color_table[g->codes[code].suffix * 4]; + if (c[3] > 128) { // don't render transparent pixels; + p[0] = c[2]; + p[1] = c[1]; + p[2] = c[0]; + p[3] = c[3]; + } + g->cur_x += 4; + + if (g->cur_x >= g->max_x) { + g->cur_x = g->start_x; + g->cur_y += g->step; + + while (g->cur_y >= g->max_y && g->parse > 0) { + g->step = (1 << g->parse) * g->line_size; + g->cur_y = g->start_y + (g->step >> 1); + --g->parse; + } + } +} + +static stbi_uc *stbi__process_gif_raster(stbi__context *s, stbi__gif *g) +{ + stbi_uc lzw_cs; + stbi__int32 len, init_code; + stbi__uint32 first; + stbi__int32 codesize, codemask, avail, oldcode, bits, valid_bits, clear; + stbi__gif_lzw *p; + + lzw_cs = stbi__get8(s); + if (lzw_cs > 12) return NULL; + clear = 1 << lzw_cs; + first = 1; + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + bits = 0; + valid_bits = 0; + for (init_code = 0; init_code < clear; init_code++) { + g->codes[init_code].prefix = -1; + g->codes[init_code].first = (stbi_uc) init_code; + g->codes[init_code].suffix = (stbi_uc) init_code; + } + + // support no starting clear code + avail = clear+2; + oldcode = -1; + + len = 0; + for(;;) { + if (valid_bits < codesize) { + if (len == 0) { + len = stbi__get8(s); // start new block + if (len == 0) + return g->out; + } + --len; + bits |= (stbi__int32) stbi__get8(s) << valid_bits; + valid_bits += 8; + } else { + stbi__int32 code = bits & codemask; + bits >>= codesize; + valid_bits -= codesize; + // @OPTIMIZE: is there some way we can accelerate the non-clear path? + if (code == clear) { // clear code + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + avail = clear + 2; + oldcode = -1; + first = 0; + } else if (code == clear + 1) { // end of stream code + stbi__skip(s, len); + while ((len = stbi__get8(s)) > 0) + stbi__skip(s,len); + return g->out; + } else if (code <= avail) { + if (first) { + return stbi__errpuc("no clear code", "Corrupt GIF"); + } + + if (oldcode >= 0) { + p = &g->codes[avail++]; + if (avail > 8192) { + return stbi__errpuc("too many codes", "Corrupt GIF"); + } + + p->prefix = (stbi__int16) oldcode; + p->first = g->codes[oldcode].first; + p->suffix = (code == avail) ? p->first : g->codes[code].first; + } else if (code == avail) + return stbi__errpuc("illegal code in raster", "Corrupt GIF"); + + stbi__out_gif_code(g, (stbi__uint16) code); + + if ((avail & codemask) == 0 && avail <= 0x0FFF) { + codesize++; + codemask = (1 << codesize) - 1; + } + + oldcode = code; + } else { + return stbi__errpuc("illegal code in raster", "Corrupt GIF"); + } + } + } +} + +// this function is designed to support animated gifs, although stb_image doesn't support it +// two back is the image from two frames ago, used for a very specific disposal format +static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, int req_comp, stbi_uc *two_back) +{ + int dispose; + int first_frame; + int pi; + int pcount; + STBI_NOTUSED(req_comp); + + // on first frame, any non-written pixels get the background colour (non-transparent) + first_frame = 0; + if (g->out == 0) { + if (!stbi__gif_header(s, g, comp,0)) return 0; // stbi__g_failure_reason set by stbi__gif_header + if (!stbi__mad3sizes_valid(4, g->w, g->h, 0)) + return stbi__errpuc("too large", "GIF image is too large"); + pcount = g->w * g->h; + g->out = (stbi_uc *) stbi__malloc(4 * pcount); + g->background = (stbi_uc *) stbi__malloc(4 * pcount); + g->history = (stbi_uc *) stbi__malloc(pcount); + if (!g->out || !g->background || !g->history) + return stbi__errpuc("outofmem", "Out of memory"); + + // image is treated as "transparent" at the start - ie, nothing overwrites the current background; + // background colour is only used for pixels that are not rendered first frame, after that "background" + // color refers to the color that was there the previous frame. + memset(g->out, 0x00, 4 * pcount); + memset(g->background, 0x00, 4 * pcount); // state of the background (starts transparent) + memset(g->history, 0x00, pcount); // pixels that were affected previous frame + first_frame = 1; + } else { + // second frame - how do we dispose of the previous one? + dispose = (g->eflags & 0x1C) >> 2; + pcount = g->w * g->h; + + if ((dispose == 3) && (two_back == 0)) { + dispose = 2; // if I don't have an image to revert back to, default to the old background + } + + if (dispose == 3) { // use previous graphic + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi]) { + memcpy( &g->out[pi * 4], &two_back[pi * 4], 4 ); + } + } + } else if (dispose == 2) { + // restore what was changed last frame to background before that frame; + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi]) { + memcpy( &g->out[pi * 4], &g->background[pi * 4], 4 ); + } + } + } else { + // This is a non-disposal case eithe way, so just + // leave the pixels as is, and they will become the new background + // 1: do not dispose + // 0: not specified. + } + + // background is what out is after the undoing of the previou frame; + memcpy( g->background, g->out, 4 * g->w * g->h ); + } + + // clear my history; + memset( g->history, 0x00, g->w * g->h ); // pixels that were affected previous frame + + for (;;) { + int tag = stbi__get8(s); + switch (tag) { + case 0x2C: /* Image Descriptor */ + { + stbi__int32 x, y, w, h; + stbi_uc *o; + + x = stbi__get16le(s); + y = stbi__get16le(s); + w = stbi__get16le(s); + h = stbi__get16le(s); + if (((x + w) > (g->w)) || ((y + h) > (g->h))) + return stbi__errpuc("bad Image Descriptor", "Corrupt GIF"); + + g->line_size = g->w * 4; + g->start_x = x * 4; + g->start_y = y * g->line_size; + g->max_x = g->start_x + w * 4; + g->max_y = g->start_y + h * g->line_size; + g->cur_x = g->start_x; + g->cur_y = g->start_y; + + // if the width of the specified rectangle is 0, that means + // we may not see *any* pixels or the image is malformed; + // to make sure this is caught, move the current y down to + // max_y (which is what out_gif_code checks). + if (w == 0) + g->cur_y = g->max_y; + + g->lflags = stbi__get8(s); + + if (g->lflags & 0x40) { + g->step = 8 * g->line_size; // first interlaced spacing + g->parse = 3; + } else { + g->step = g->line_size; + g->parse = 0; + } + + if (g->lflags & 0x80) { + stbi__gif_parse_colortable(s,g->lpal, 2 << (g->lflags & 7), g->eflags & 0x01 ? g->transparent : -1); + g->color_table = (stbi_uc *) g->lpal; + } else if (g->flags & 0x80) { + g->color_table = (stbi_uc *) g->pal; + } else + return stbi__errpuc("missing color table", "Corrupt GIF"); + + o = stbi__process_gif_raster(s, g); + if (!o) return NULL; + + // if this was the first frame, + pcount = g->w * g->h; + if (first_frame && (g->bgindex > 0)) { + // if first frame, any pixel not drawn to gets the background color + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi] == 0) { + g->pal[g->bgindex][3] = 255; // just in case it was made transparent, undo that; It will be reset next frame if need be; + memcpy( &g->out[pi * 4], &g->pal[g->bgindex], 4 ); + } + } + } + + return o; + } + + case 0x21: // Comment Extension. + { + int len; + int ext = stbi__get8(s); + if (ext == 0xF9) { // Graphic Control Extension. + len = stbi__get8(s); + if (len == 4) { + g->eflags = stbi__get8(s); + g->delay = 10 * stbi__get16le(s); // delay - 1/100th of a second, saving as 1/1000ths. + + // unset old transparent + if (g->transparent >= 0) { + g->pal[g->transparent][3] = 255; + } + if (g->eflags & 0x01) { + g->transparent = stbi__get8(s); + if (g->transparent >= 0) { + g->pal[g->transparent][3] = 0; + } + } else { + // don't need transparent + stbi__skip(s, 1); + g->transparent = -1; + } + } else { + stbi__skip(s, len); + break; + } + } + while ((len = stbi__get8(s)) != 0) { + stbi__skip(s, len); + } + break; + } + + case 0x3B: // gif stream termination code + return (stbi_uc *) s; // using '1' causes warning on some compilers + + default: + return stbi__errpuc("unknown code", "Corrupt GIF"); + } + } +} + +static void *stbi__load_gif_main_outofmem(stbi__gif *g, stbi_uc *out, int **delays) +{ + STBI_FREE(g->out); + STBI_FREE(g->history); + STBI_FREE(g->background); + + if (out) STBI_FREE(out); + if (delays && *delays) STBI_FREE(*delays); + return stbi__errpuc("outofmem", "Out of memory"); +} + +static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp) +{ + if (stbi__gif_test(s)) { + int layers = 0; + stbi_uc *u = 0; + stbi_uc *out = 0; + stbi_uc *two_back = 0; + stbi__gif g; + int stride; + int out_size = 0; + int delays_size = 0; + + STBI_NOTUSED(out_size); + STBI_NOTUSED(delays_size); + + memset(&g, 0, sizeof(g)); + if (delays) { + *delays = 0; + } + + do { + u = stbi__gif_load_next(s, &g, comp, req_comp, two_back); + if (u == (stbi_uc *) s) u = 0; // end of animated gif marker + + if (u) { + *x = g.w; + *y = g.h; + ++layers; + stride = g.w * g.h * 4; + + if (out) { + void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride ); + if (!tmp) + return stbi__load_gif_main_outofmem(&g, out, delays); + else { + out = (stbi_uc*) tmp; + out_size = layers * stride; + } + + if (delays) { + int *new_delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers ); + if (!new_delays) + return stbi__load_gif_main_outofmem(&g, out, delays); + *delays = new_delays; + delays_size = layers * sizeof(int); + } + } else { + out = (stbi_uc*)stbi__malloc( layers * stride ); + if (!out) + return stbi__load_gif_main_outofmem(&g, out, delays); + out_size = layers * stride; + if (delays) { + *delays = (int*) stbi__malloc( layers * sizeof(int) ); + if (!*delays) + return stbi__load_gif_main_outofmem(&g, out, delays); + delays_size = layers * sizeof(int); + } + } + memcpy( out + ((layers - 1) * stride), u, stride ); + if (layers >= 2) { + two_back = out - 2 * stride; + } + + if (delays) { + (*delays)[layers - 1U] = g.delay; + } + } + } while (u != 0); + + // free temp buffer; + STBI_FREE(g.out); + STBI_FREE(g.history); + STBI_FREE(g.background); + + // do the final conversion after loading everything; + if (req_comp && req_comp != 4) + out = stbi__convert_format(out, 4, req_comp, layers * g.w, g.h); + + *z = layers; + return out; + } else { + return stbi__errpuc("not GIF", "Image was not as a gif type."); + } +} + +static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi_uc *u = 0; + stbi__gif g; + memset(&g, 0, sizeof(g)); + STBI_NOTUSED(ri); + + u = stbi__gif_load_next(s, &g, comp, req_comp, 0); + if (u == (stbi_uc *) s) u = 0; // end of animated gif marker + if (u) { + *x = g.w; + *y = g.h; + + // moved conversion to after successful load so that the same + // can be done for multiple frames. + if (req_comp && req_comp != 4) + u = stbi__convert_format(u, 4, req_comp, g.w, g.h); + } else if (g.out) { + // if there was an error and we allocated an image buffer, free it! + STBI_FREE(g.out); + } + + // free buffers needed for multiple frame loading; + STBI_FREE(g.history); + STBI_FREE(g.background); + + return u; +} + +static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp) +{ + return stbi__gif_info_raw(s,x,y,comp); +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR loader +// originally by Nicolas Schulz +#ifndef STBI_NO_HDR +static int stbi__hdr_test_core(stbi__context *s, const char *signature) +{ + int i; + for (i=0; signature[i]; ++i) + if (stbi__get8(s) != signature[i]) + return 0; + stbi__rewind(s); + return 1; +} + +static int stbi__hdr_test(stbi__context* s) +{ + int r = stbi__hdr_test_core(s, "#?RADIANCE\n"); + stbi__rewind(s); + if(!r) { + r = stbi__hdr_test_core(s, "#?RGBE\n"); + stbi__rewind(s); + } + return r; +} + +#define STBI__HDR_BUFLEN 1024 +static char *stbi__hdr_gettoken(stbi__context *z, char *buffer) +{ + int len=0; + char c = '\0'; + + c = (char) stbi__get8(z); + + while (!stbi__at_eof(z) && c != '\n') { + buffer[len++] = c; + if (len == STBI__HDR_BUFLEN-1) { + // flush to end of line + while (!stbi__at_eof(z) && stbi__get8(z) != '\n') + ; + break; + } + c = (char) stbi__get8(z); + } + + buffer[len] = 0; + return buffer; +} + +static void stbi__hdr_convert(float *output, stbi_uc *input, int req_comp) +{ + if ( input[3] != 0 ) { + float f1; + // Exponent + f1 = (float) ldexp(1.0f, input[3] - (int)(128 + 8)); + if (req_comp <= 2) + output[0] = (input[0] + input[1] + input[2]) * f1 / 3; + else { + output[0] = input[0] * f1; + output[1] = input[1] * f1; + output[2] = input[2] * f1; + } + if (req_comp == 2) output[1] = 1; + if (req_comp == 4) output[3] = 1; + } else { + switch (req_comp) { + case 4: output[3] = 1; /* fallthrough */ + case 3: output[0] = output[1] = output[2] = 0; + break; + case 2: output[1] = 1; /* fallthrough */ + case 1: output[0] = 0; + break; + } + } +} + +static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + char buffer[STBI__HDR_BUFLEN]; + char *token; + int valid = 0; + int width, height; + stbi_uc *scanline; + float *hdr_data; + int len; + unsigned char count, value; + int i, j, k, c1,c2, z; + const char *headerToken; + STBI_NOTUSED(ri); + + // Check identifier + headerToken = stbi__hdr_gettoken(s,buffer); + if (strcmp(headerToken, "#?RADIANCE") != 0 && strcmp(headerToken, "#?RGBE") != 0) + return stbi__errpf("not HDR", "Corrupt HDR image"); + + // Parse header + for(;;) { + token = stbi__hdr_gettoken(s,buffer); + if (token[0] == 0) break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; + } + + if (!valid) return stbi__errpf("unsupported format", "Unsupported HDR format"); + + // Parse width and height + // can't use sscanf() if we're not using stdio! + token = stbi__hdr_gettoken(s,buffer); + if (strncmp(token, "-Y ", 3)) return stbi__errpf("unsupported data layout", "Unsupported HDR format"); + token += 3; + height = (int) strtol(token, &token, 10); + while (*token == ' ') ++token; + if (strncmp(token, "+X ", 3)) return stbi__errpf("unsupported data layout", "Unsupported HDR format"); + token += 3; + width = (int) strtol(token, NULL, 10); + + if (height > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); + if (width > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); + + *x = width; + *y = height; + + if (comp) *comp = 3; + if (req_comp == 0) req_comp = 3; + + if (!stbi__mad4sizes_valid(width, height, req_comp, sizeof(float), 0)) + return stbi__errpf("too large", "HDR image is too large"); + + // Read data + hdr_data = (float *) stbi__malloc_mad4(width, height, req_comp, sizeof(float), 0); + if (!hdr_data) + return stbi__errpf("outofmem", "Out of memory"); + + // Load image data + // image data is stored as some number of sca + if ( width < 8 || width >= 32768) { + // Read flat data + for (j=0; j < height; ++j) { + for (i=0; i < width; ++i) { + stbi_uc rgbe[4]; + main_decode_loop: + stbi__getn(s, rgbe, 4); + stbi__hdr_convert(hdr_data + j * width * req_comp + i * req_comp, rgbe, req_comp); + } + } + } else { + // Read RLE-encoded data + scanline = NULL; + + for (j = 0; j < height; ++j) { + c1 = stbi__get8(s); + c2 = stbi__get8(s); + len = stbi__get8(s); + if (c1 != 2 || c2 != 2 || (len & 0x80)) { + // not run-length encoded, so we have to actually use THIS data as a decoded + // pixel (note this can't be a valid pixel--one of RGB must be >= 128) + stbi_uc rgbe[4]; + rgbe[0] = (stbi_uc) c1; + rgbe[1] = (stbi_uc) c2; + rgbe[2] = (stbi_uc) len; + rgbe[3] = (stbi_uc) stbi__get8(s); + stbi__hdr_convert(hdr_data, rgbe, req_comp); + i = 1; + j = 0; + STBI_FREE(scanline); + goto main_decode_loop; // yes, this makes no sense + } + len <<= 8; + len |= stbi__get8(s); + if (len != width) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("invalid decoded scanline length", "corrupt HDR"); } + if (scanline == NULL) { + scanline = (stbi_uc *) stbi__malloc_mad2(width, 4, 0); + if (!scanline) { + STBI_FREE(hdr_data); + return stbi__errpf("outofmem", "Out of memory"); + } + } + + for (k = 0; k < 4; ++k) { + int nleft; + i = 0; + while ((nleft = width - i) > 0) { + count = stbi__get8(s); + if (count > 128) { + // Run + value = stbi__get8(s); + count -= 128; + if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = value; + } else { + // Dump + if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = stbi__get8(s); + } + } + } + for (i=0; i < width; ++i) + stbi__hdr_convert(hdr_data+(j*width + i)*req_comp, scanline + i*4, req_comp); + } + if (scanline) + STBI_FREE(scanline); + } + + return hdr_data; +} + +static int stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp) +{ + char buffer[STBI__HDR_BUFLEN]; + char *token; + int valid = 0; + int dummy; + + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + + if (stbi__hdr_test(s) == 0) { + stbi__rewind( s ); + return 0; + } + + for(;;) { + token = stbi__hdr_gettoken(s,buffer); + if (token[0] == 0) break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; + } + + if (!valid) { + stbi__rewind( s ); + return 0; + } + token = stbi__hdr_gettoken(s,buffer); + if (strncmp(token, "-Y ", 3)) { + stbi__rewind( s ); + return 0; + } + token += 3; + *y = (int) strtol(token, &token, 10); + while (*token == ' ') ++token; + if (strncmp(token, "+X ", 3)) { + stbi__rewind( s ); + return 0; + } + token += 3; + *x = (int) strtol(token, NULL, 10); + *comp = 3; + return 1; +} +#endif // STBI_NO_HDR + +#ifndef STBI_NO_BMP +static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp) +{ + void *p; + stbi__bmp_data info; + + info.all_a = 255; + p = stbi__bmp_parse_header(s, &info); + if (p == NULL) { + stbi__rewind( s ); + return 0; + } + if (x) *x = s->img_x; + if (y) *y = s->img_y; + if (comp) { + if (info.bpp == 24 && info.ma == 0xff000000) + *comp = 3; + else + *comp = info.ma ? 4 : 3; + } + return 1; +} +#endif + +#ifndef STBI_NO_PSD +static int stbi__psd_info(stbi__context *s, int *x, int *y, int *comp) +{ + int channelCount, dummy, depth; + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + if (stbi__get32be(s) != 0x38425053) { + stbi__rewind( s ); + return 0; + } + if (stbi__get16be(s) != 1) { + stbi__rewind( s ); + return 0; + } + stbi__skip(s, 6); + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) { + stbi__rewind( s ); + return 0; + } + *y = stbi__get32be(s); + *x = stbi__get32be(s); + depth = stbi__get16be(s); + if (depth != 8 && depth != 16) { + stbi__rewind( s ); + return 0; + } + if (stbi__get16be(s) != 3) { + stbi__rewind( s ); + return 0; + } + *comp = 4; + return 1; +} + +static int stbi__psd_is16(stbi__context *s) +{ + int channelCount, depth; + if (stbi__get32be(s) != 0x38425053) { + stbi__rewind( s ); + return 0; + } + if (stbi__get16be(s) != 1) { + stbi__rewind( s ); + return 0; + } + stbi__skip(s, 6); + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) { + stbi__rewind( s ); + return 0; + } + STBI_NOTUSED(stbi__get32be(s)); + STBI_NOTUSED(stbi__get32be(s)); + depth = stbi__get16be(s); + if (depth != 16) { + stbi__rewind( s ); + return 0; + } + return 1; +} +#endif + +#ifndef STBI_NO_PIC +static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp) +{ + int act_comp=0,num_packets=0,chained,dummy; + stbi__pic_packet packets[10]; + + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + + if (!stbi__pic_is4(s,"\x53\x80\xF6\x34")) { + stbi__rewind(s); + return 0; + } + + stbi__skip(s, 88); + + *x = stbi__get16be(s); + *y = stbi__get16be(s); + if (stbi__at_eof(s)) { + stbi__rewind( s); + return 0; + } + if ( (*x) != 0 && (1 << 28) / (*x) < (*y)) { + stbi__rewind( s ); + return 0; + } + + stbi__skip(s, 8); + + do { + stbi__pic_packet *packet; + + if (num_packets==sizeof(packets)/sizeof(packets[0])) + return 0; + + packet = &packets[num_packets++]; + chained = stbi__get8(s); + packet->size = stbi__get8(s); + packet->type = stbi__get8(s); + packet->channel = stbi__get8(s); + act_comp |= packet->channel; + + if (stbi__at_eof(s)) { + stbi__rewind( s ); + return 0; + } + if (packet->size != 8) { + stbi__rewind( s ); + return 0; + } + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); + + return 1; +} +#endif + +// ************************************************************************************************* +// Portable Gray Map and Portable Pixel Map loader +// by Ken Miller +// +// PGM: http://netpbm.sourceforge.net/doc/pgm.html +// PPM: http://netpbm.sourceforge.net/doc/ppm.html +// +// Known limitations: +// Does not support comments in the header section +// Does not support ASCII image data (formats P2 and P3) + +#ifndef STBI_NO_PNM + +static int stbi__pnm_test(stbi__context *s) +{ + char p, t; + p = (char) stbi__get8(s); + t = (char) stbi__get8(s); + if (p != 'P' || (t != '5' && t != '6')) { + stbi__rewind( s ); + return 0; + } + return 1; +} + +static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi_uc *out; + STBI_NOTUSED(ri); + + ri->bits_per_channel = stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n); + if (ri->bits_per_channel == 0) + return 0; + + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + *x = s->img_x; + *y = s->img_y; + if (comp) *comp = s->img_n; + + if (!stbi__mad4sizes_valid(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0)) + return stbi__errpuc("too large", "PNM too large"); + + out = (stbi_uc *) stbi__malloc_mad4(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0); + if (!out) return stbi__errpuc("outofmem", "Out of memory"); + if (!stbi__getn(s, out, s->img_n * s->img_x * s->img_y * (ri->bits_per_channel / 8))) { + STBI_FREE(out); + return stbi__errpuc("bad PNM", "PNM file truncated"); + } + + if (req_comp && req_comp != s->img_n) { + if (ri->bits_per_channel == 16) { + out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, s->img_n, req_comp, s->img_x, s->img_y); + } else { + out = stbi__convert_format(out, s->img_n, req_comp, s->img_x, s->img_y); + } + if (out == NULL) return out; // stbi__convert_format frees input on failure + } + return out; +} + +static int stbi__pnm_isspace(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +static void stbi__pnm_skip_whitespace(stbi__context *s, char *c) +{ + for (;;) { + while (!stbi__at_eof(s) && stbi__pnm_isspace(*c)) + *c = (char) stbi__get8(s); + + if (stbi__at_eof(s) || *c != '#') + break; + + while (!stbi__at_eof(s) && *c != '\n' && *c != '\r' ) + *c = (char) stbi__get8(s); + } +} + +static int stbi__pnm_isdigit(char c) +{ + return c >= '0' && c <= '9'; +} + +static int stbi__pnm_getinteger(stbi__context *s, char *c) +{ + int value = 0; + + while (!stbi__at_eof(s) && stbi__pnm_isdigit(*c)) { + value = value*10 + (*c - '0'); + *c = (char) stbi__get8(s); + if((value > 214748364) || (value == 214748364 && *c > '7')) + return stbi__err("integer parse overflow", "Parsing an integer in the PPM header overflowed a 32-bit int"); + } + + return value; +} + +static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp) +{ + int maxv, dummy; + char c, p, t; + + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + + stbi__rewind(s); + + // Get identifier + p = (char) stbi__get8(s); + t = (char) stbi__get8(s); + if (p != 'P' || (t != '5' && t != '6')) { + stbi__rewind(s); + return 0; + } + + *comp = (t == '6') ? 3 : 1; // '5' is 1-component .pgm; '6' is 3-component .ppm + + c = (char) stbi__get8(s); + stbi__pnm_skip_whitespace(s, &c); + + *x = stbi__pnm_getinteger(s, &c); // read width + if(*x == 0) + return stbi__err("invalid width", "PPM image header had zero or overflowing width"); + stbi__pnm_skip_whitespace(s, &c); + + *y = stbi__pnm_getinteger(s, &c); // read height + if (*y == 0) + return stbi__err("invalid width", "PPM image header had zero or overflowing width"); + stbi__pnm_skip_whitespace(s, &c); + + maxv = stbi__pnm_getinteger(s, &c); // read max value + if (maxv > 65535) + return stbi__err("max value > 65535", "PPM image supports only 8-bit and 16-bit images"); + else if (maxv > 255) + return 16; + else + return 8; +} + +static int stbi__pnm_is16(stbi__context *s) +{ + if (stbi__pnm_info(s, NULL, NULL, NULL) == 16) + return 1; + return 0; +} +#endif + +static int stbi__info_main(stbi__context *s, int *x, int *y, int *comp) +{ + #ifndef STBI_NO_JPEG + if (stbi__jpeg_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PNG + if (stbi__png_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_GIF + if (stbi__gif_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_BMP + if (stbi__bmp_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PSD + if (stbi__psd_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PIC + if (stbi__pic_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PNM + if (stbi__pnm_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_HDR + if (stbi__hdr_info(s, x, y, comp)) return 1; + #endif + + // test tga last because it's a crappy test! + #ifndef STBI_NO_TGA + if (stbi__tga_info(s, x, y, comp)) + return 1; + #endif + return stbi__err("unknown image type", "Image not of any known type, or corrupt"); +} + +static int stbi__is_16_main(stbi__context *s) +{ + #ifndef STBI_NO_PNG + if (stbi__png_is16(s)) return 1; + #endif + + #ifndef STBI_NO_PSD + if (stbi__psd_is16(s)) return 1; + #endif + + #ifndef STBI_NO_PNM + if (stbi__pnm_is16(s)) return 1; + #endif + return 0; +} + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_info(char const *filename, int *x, int *y, int *comp) +{ + FILE *f = stbi__fopen(filename, "rb"); + int result; + if (!f) return stbi__err("can't fopen", "Unable to open file"); + result = stbi_info_from_file(f, x, y, comp); + fclose(f); + return result; +} + +STBIDEF int stbi_info_from_file(FILE *f, int *x, int *y, int *comp) +{ + int r; + stbi__context s; + long pos = ftell(f); + stbi__start_file(&s, f); + r = stbi__info_main(&s,x,y,comp); + fseek(f,pos,SEEK_SET); + return r; +} + +STBIDEF int stbi_is_16_bit(char const *filename) +{ + FILE *f = stbi__fopen(filename, "rb"); + int result; + if (!f) return stbi__err("can't fopen", "Unable to open file"); + result = stbi_is_16_bit_from_file(f); + fclose(f); + return result; +} + +STBIDEF int stbi_is_16_bit_from_file(FILE *f) +{ + int r; + stbi__context s; + long pos = ftell(f); + stbi__start_file(&s, f); + r = stbi__is_16_main(&s); + fseek(f,pos,SEEK_SET); + return r; +} +#endif // !STBI_NO_STDIO + +STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__info_main(&s,x,y,comp); +} + +STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *c, void *user, int *x, int *y, int *comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); + return stbi__info_main(&s,x,y,comp); +} + +STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__is_16_main(&s); +} + +STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *c, void *user) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); + return stbi__is_16_main(&s); +} + +#endif // STB_IMAGE_IMPLEMENTATION + +/* + revision history: + 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs + 2.19 (2018-02-11) fix warning + 2.18 (2018-01-30) fix warnings + 2.17 (2018-01-29) change sbti__shiftsigned to avoid clang -O2 bug + 1-bit BMP + *_is_16_bit api + avoid warnings + 2.16 (2017-07-23) all functions have 16-bit variants; + STBI_NO_STDIO works again; + compilation fixes; + fix rounding in unpremultiply; + optimize vertical flip; + disable raw_len validation; + documentation fixes + 2.15 (2017-03-18) fix png-1,2,4 bug; now all Imagenet JPGs decode; + warning fixes; disable run-time SSE detection on gcc; + uniform handling of optional "return" values; + thread-safe initialization of zlib tables + 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs + 2.13 (2016-11-29) add 16-bit API, only supported for PNG right now + 2.12 (2016-04-02) fix typo in 2.11 PSD fix that caused crashes + 2.11 (2016-04-02) allocate large structures on the stack + remove white matting for transparent PSD + fix reported channel count for PNG & BMP + re-enable SSE2 in non-gcc 64-bit + support RGB-formatted JPEG + read 16-bit PNGs (only as 8-bit) + 2.10 (2016-01-22) avoid warning introduced in 2.09 by STBI_REALLOC_SIZED + 2.09 (2016-01-16) allow comments in PNM files + 16-bit-per-pixel TGA (not bit-per-component) + info() for TGA could break due to .hdr handling + info() for BMP to shares code instead of sloppy parse + can use STBI_REALLOC_SIZED if allocator doesn't support realloc + code cleanup + 2.08 (2015-09-13) fix to 2.07 cleanup, reading RGB PSD as RGBA + 2.07 (2015-09-13) fix compiler warnings + partial animated GIF support + limited 16-bpc PSD support + #ifdef unused functions + bug with < 92 byte PIC,PNM,HDR,TGA + 2.06 (2015-04-19) fix bug where PSD returns wrong '*comp' value + 2.05 (2015-04-19) fix bug in progressive JPEG handling, fix warning + 2.04 (2015-04-15) try to re-enable SIMD on MinGW 64-bit + 2.03 (2015-04-12) extra corruption checking (mmozeiko) + stbi_set_flip_vertically_on_load (nguillemot) + fix NEON support; fix mingw support + 2.02 (2015-01-19) fix incorrect assert, fix warning + 2.01 (2015-01-17) fix various warnings; suppress SIMD on gcc 32-bit without -msse2 + 2.00b (2014-12-25) fix STBI_MALLOC in progressive JPEG + 2.00 (2014-12-25) optimize JPG, including x86 SSE2 & NEON SIMD (ryg) + progressive JPEG (stb) + PGM/PPM support (Ken Miller) + STBI_MALLOC,STBI_REALLOC,STBI_FREE + GIF bugfix -- seemingly never worked + STBI_NO_*, STBI_ONLY_* + 1.48 (2014-12-14) fix incorrectly-named assert() + 1.47 (2014-12-14) 1/2/4-bit PNG support, both direct and paletted (Omar Cornut & stb) + optimize PNG (ryg) + fix bug in interlaced PNG with user-specified channel count (stb) + 1.46 (2014-08-26) + fix broken tRNS chunk (colorkey-style transparency) in non-paletted PNG + 1.45 (2014-08-16) + fix MSVC-ARM internal compiler error by wrapping malloc + 1.44 (2014-08-07) + various warning fixes from Ronny Chevalier + 1.43 (2014-07-15) + fix MSVC-only compiler problem in code changed in 1.42 + 1.42 (2014-07-09) + don't define _CRT_SECURE_NO_WARNINGS (affects user code) + fixes to stbi__cleanup_jpeg path + added STBI_ASSERT to avoid requiring assert.h + 1.41 (2014-06-25) + fix search&replace from 1.36 that messed up comments/error messages + 1.40 (2014-06-22) + fix gcc struct-initialization warning + 1.39 (2014-06-15) + fix to TGA optimization when req_comp != number of components in TGA; + fix to GIF loading because BMP wasn't rewinding (whoops, no GIFs in my test suite) + add support for BMP version 5 (more ignored fields) + 1.38 (2014-06-06) + suppress MSVC warnings on integer casts truncating values + fix accidental rename of 'skip' field of I/O + 1.37 (2014-06-04) + remove duplicate typedef + 1.36 (2014-06-03) + convert to header file single-file library + if de-iphone isn't set, load iphone images color-swapped instead of returning NULL + 1.35 (2014-05-27) + various warnings + fix broken STBI_SIMD path + fix bug where stbi_load_from_file no longer left file pointer in correct place + fix broken non-easy path for 32-bit BMP (possibly never used) + TGA optimization by Arseny Kapoulkine + 1.34 (unknown) + use STBI_NOTUSED in stbi__resample_row_generic(), fix one more leak in tga failure case + 1.33 (2011-07-14) + make stbi_is_hdr work in STBI_NO_HDR (as specified), minor compiler-friendly improvements + 1.32 (2011-07-13) + support for "info" function for all supported filetypes (SpartanJ) + 1.31 (2011-06-20) + a few more leak fixes, bug in PNG handling (SpartanJ) + 1.30 (2011-06-11) + added ability to load files via callbacks to accomidate custom input streams (Ben Wenger) + removed deprecated format-specific test/load functions + removed support for installable file formats (stbi_loader) -- would have been broken for IO callbacks anyway + error cases in bmp and tga give messages and don't leak (Raymond Barbiero, grisha) + fix inefficiency in decoding 32-bit BMP (David Woo) + 1.29 (2010-08-16) + various warning fixes from Aurelien Pocheville + 1.28 (2010-08-01) + fix bug in GIF palette transparency (SpartanJ) + 1.27 (2010-08-01) + cast-to-stbi_uc to fix warnings + 1.26 (2010-07-24) + fix bug in file buffering for PNG reported by SpartanJ + 1.25 (2010-07-17) + refix trans_data warning (Won Chun) + 1.24 (2010-07-12) + perf improvements reading from files on platforms with lock-heavy fgetc() + minor perf improvements for jpeg + deprecated type-specific functions so we'll get feedback if they're needed + attempt to fix trans_data warning (Won Chun) + 1.23 fixed bug in iPhone support + 1.22 (2010-07-10) + removed image *writing* support + stbi_info support from Jetro Lauha + GIF support from Jean-Marc Lienher + iPhone PNG-extensions from James Brown + warning-fixes from Nicolas Schulz and Janez Zemva (i.stbi__err. Janez (U+017D)emva) + 1.21 fix use of 'stbi_uc' in header (reported by jon blow) + 1.20 added support for Softimage PIC, by Tom Seddon + 1.19 bug in interlaced PNG corruption check (found by ryg) + 1.18 (2008-08-02) + fix a threading bug (local mutable static) + 1.17 support interlaced PNG + 1.16 major bugfix - stbi__convert_format converted one too many pixels + 1.15 initialize some fields for thread safety + 1.14 fix threadsafe conversion bug + header-file-only version (#define STBI_HEADER_FILE_ONLY before including) + 1.13 threadsafe + 1.12 const qualifiers in the API + 1.11 Support installable IDCT, colorspace conversion routines + 1.10 Fixes for 64-bit (don't use "unsigned long") + optimized upsampling by Fabian "ryg" Giesen + 1.09 Fix format-conversion for PSD code (bad global variables!) + 1.08 Thatcher Ulrich's PSD code integrated by Nicolas Schulz + 1.07 attempt to fix C++ warning/errors again + 1.06 attempt to fix C++ warning/errors again + 1.05 fix TGA loading to return correct *comp and use good luminance calc + 1.04 default float alpha is 1, not 255; use 'void *' for stbi_image_free + 1.03 bugfixes to STBI_NO_STDIO, STBI_NO_HDR + 1.02 support for (subset of) HDR files, float interface for preferred access to them + 1.01 fix bug: possible bug in handling right-side up bmps... not sure + fix bug: the stbi__bmp_load() and stbi__tga_load() functions didn't work at all + 1.00 interface to zlib that skips zlib header + 0.99 correct handling of alpha in palette + 0.98 TGA loader by lonesock; dynamically add loaders (untested) + 0.97 jpeg errors on too large a file; also catch another malloc failure + 0.96 fix detection of invalid v value - particleman@mollyrocket forum + 0.95 during header scan, seek to markers in case of padding + 0.94 STBI_NO_STDIO to disable stdio usage; rename all #defines the same + 0.93 handle jpegtran output; verbose errors + 0.92 read 4,8,16,24,32-bit BMP files of several formats + 0.91 output 24-bit Windows 3.0 BMP files + 0.90 fix a few more warnings; bump version number to approach 1.0 + 0.61 bugfixes due to Marc LeBlanc, Christopher Lloyd + 0.60 fix compiling as c++ + 0.59 fix warnings: merge Dave Moore's -Wall fixes + 0.58 fix bug: zlib uncompressed mode len/nlen was wrong endian + 0.57 fix bug: jpg last huffman symbol before marker was >9 bits but less than 16 available + 0.56 fix bug: zlib uncompressed mode len vs. nlen + 0.55 fix bug: restart_interval not initialized to 0 + 0.54 allow NULL for 'int *comp' + 0.53 fix bug in png 3->4; speedup png decoding + 0.52 png handles req_comp=3,4 directly; minor cleanup; jpeg comments + 0.51 obey req_comp requests, 1-component jpegs return as 1-component, + on 'test' only check type, not whether we support this variant + 0.50 (2006-11-19) + first released version +*/ + + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/src/cdeps/stb_image_resize2.h b/src/cdeps/stb_image_resize2.h new file mode 100644 index 0000000..0798976 --- /dev/null +++ b/src/cdeps/stb_image_resize2.h @@ -0,0 +1,10679 @@ +/* stb_image_resize2 - v2.18 - public domain image resizing + + by Jeff Roberts (v2) and Jorge L Rodriguez + http://github.com/nothings/stb + + Can be threaded with the extended API. SSE2, AVX, Neon and WASM SIMD support. Only + scaling and translation is supported, no rotations or shears. + + COMPILING & LINKING + In one C/C++ file that #includes this file, do this: + #define STB_IMAGE_RESIZE_IMPLEMENTATION + before the #include. That will create the implementation in that file. + + EASY API CALLS: + Easy API downsamples w/Mitchell filter, upsamples w/cubic interpolation, clamps to edge. + + stbir_resize_uint8_srgb( input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + pixel_layout_enum ) + + stbir_resize_uint8_linear( input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + pixel_layout_enum ) + + stbir_resize_float_linear( input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + pixel_layout_enum ) + + If you pass NULL or zero for the output_pixels, we will allocate the output buffer + for you and return it from the function (free with free() or STBIR_FREE). + As a special case, XX_stride_in_bytes of 0 means packed continuously in memory. + + API LEVELS + There are three levels of API - easy-to-use, medium-complexity and extended-complexity. + + See the "header file" section of the source for API documentation. + + ADDITIONAL DOCUMENTATION + + MEMORY ALLOCATION + By default, we use malloc and free for memory allocation. To override the + memory allocation, before the implementation #include, add a: + + #define STBIR_MALLOC(size,user_data) ... + #define STBIR_FREE(ptr,user_data) ... + + Each resize makes exactly one call to malloc/free (unless you use the + extended API where you can do one allocation for many resizes). Under + address sanitizer, we do separate allocations to find overread/writes. + + PERFORMANCE + This library was written with an emphasis on performance. When testing + stb_image_resize with RGBA, the fastest mode is STBIR_4CHANNEL with + STBIR_TYPE_UINT8 pixels and CLAMPed edges (which is what many other resize + libs do by default). Also, make sure SIMD is turned on of course (default + for 64-bit targets). Avoid WRAP edge mode if you want the fastest speed. + + This library also comes with profiling built-in. If you define STBIR_PROFILE, + you can use the advanced API and get low-level profiling information by + calling stbir_resize_extended_profile_info() or stbir_resize_split_profile_info() + after a resize. + + SIMD + Most of the routines have optimized SSE2, AVX, NEON and WASM versions. + + On Microsoft compilers, we automatically turn on SIMD for 64-bit x64 and + ARM; for 32-bit x86 and ARM, you select SIMD mode by defining STBIR_SSE2 or + STBIR_NEON. For AVX and AVX2, we auto-select it by detecting the /arch:AVX + or /arch:AVX2 switches. You can also always manually turn SSE2, AVX or AVX2 + support on by defining STBIR_SSE2, STBIR_AVX or STBIR_AVX2. + + On Linux, SSE2 and Neon is on by default for 64-bit x64 or ARM64. For 32-bit, + we select x86 SIMD mode by whether you have -msse2, -mavx or -mavx2 enabled + on the command line. For 32-bit ARM, you must pass -mfpu=neon-vfpv4 for both + clang and GCC, but GCC also requires an additional -mfp16-format=ieee to + automatically enable NEON. + + On x86 platforms, you can also define STBIR_FP16C to turn on FP16C instructions + for converting back and forth to half-floats. This is autoselected when we + are using AVX2. Clang and GCC also require the -mf16c switch. ARM always uses + the built-in half float hardware NEON instructions. + + You can also tell us to use multiply-add instructions with STBIR_USE_FMA. + Because x86 doesn't always have fma, we turn it off by default to maintain + determinism across all platforms. If you don't care about non-FMA determinism + and are willing to restrict yourself to more recent x86 CPUs (around the AVX + timeframe), then fma will give you around a 15% speedup. + + You can force off SIMD in all cases by defining STBIR_NO_SIMD. You can turn + off AVX or AVX2 specifically with STBIR_NO_AVX or STBIR_NO_AVX2. AVX is 10% + to 40% faster, and AVX2 is generally another 12%. + + ALPHA CHANNEL + Most of the resizing functions provide the ability to control how the alpha + channel of an image is processed. + + When alpha represents transparency, it is important that when combining + colors with filtering, the pixels should not be treated equally; they + should use a weighted average based on their alpha values. For example, + if a pixel is 1% opaque bright green and another pixel is 99% opaque + black and you average them, the average will be 50% opaque, but the + unweighted average and will be a middling green color, while the weighted + average will be nearly black. This means the unweighted version introduced + green energy that didn't exist in the source image. + + (If you want to know why this makes sense, you can work out the math for + the following: consider what happens if you alpha composite a source image + over a fixed color and then average the output, vs. if you average the + source image pixels and then composite that over the same fixed color. + Only the weighted average produces the same result as the ground truth + composite-then-average result.) + + Therefore, it is in general best to "alpha weight" the pixels when applying + filters to them. This essentially means multiplying the colors by the alpha + values before combining them, and then dividing by the alpha value at the + end. + + The computer graphics industry introduced a technique called "premultiplied + alpha" or "associated alpha" in which image colors are stored in image files + already multiplied by their alpha. This saves some math when compositing, + and also avoids the need to divide by the alpha at the end (which is quite + inefficient). However, while premultiplied alpha is common in the movie CGI + industry, it is not commonplace in other industries like videogames, and most + consumer file formats are generally expected to contain not-premultiplied + colors. For example, Photoshop saves PNG files "unpremultiplied", and web + browsers like Chrome and Firefox expect PNG images to be unpremultiplied. + + Note that there are three possibilities that might describe your image + and resize expectation: + + 1. images are not premultiplied, alpha weighting is desired + 2. images are not premultiplied, alpha weighting is not desired + 3. images are premultiplied + + Both case #2 and case #3 require the exact same math: no alpha weighting + should be applied or removed. Only case 1 requires extra math operations; + the other two cases can be handled identically. + + stb_image_resize expects case #1 by default, applying alpha weighting to + images, expecting the input images to be unpremultiplied. This is what the + COLOR+ALPHA buffer types tell the resizer to do. + + When you use the pixel layouts STBIR_RGBA, STBIR_BGRA, STBIR_ARGB, + STBIR_ABGR, STBIR_RA, or STBIR_AR you are telling us that the pixels are + non-premultiplied. In these cases, the resizer will alpha weight the colors + (effectively creating the premultiplied image), do the filtering, and then + convert back to non-premult on exit. + + When you use the pixel layouts STBIR_RGBA_PM, STBIR_BGRA_PM, STBIR_ARGB_PM, + STBIR_ABGR_PM, STBIR_RA_PM or STBIR_AR_PM, you are telling that the pixels + ARE premultiplied. In this case, the resizer doesn't have to do the + premultipling - it can filter directly on the input. This about twice as + fast as the non-premultiplied case, so it's the right option if your data is + already setup correctly. + + When you use the pixel layout STBIR_4CHANNEL or STBIR_2CHANNEL, you are + telling us that there is no channel that represents transparency; it may be + RGB and some unrelated fourth channel that has been stored in the alpha + channel, but it is actually not alpha. No special processing will be + performed. + + The difference between the generic 4 or 2 channel layouts, and the + specialized _PM versions is with the _PM versions you are telling us that + the data *is* alpha, just don't premultiply it. That's important when + using SRGB pixel formats, we need to know where the alpha is, because + it is converted linearly (rather than with the SRGB converters). + + Because alpha weighting produces the same effect as premultiplying, you + even have the option with non-premultiplied inputs to let the resizer + produce a premultiplied output. Because the intially computed alpha-weighted + output image is effectively premultiplied, this is actually more performant + than the normal path which un-premultiplies the output image as a final step. + + Finally, when converting both in and out of non-premulitplied space (for + example, when using STBIR_RGBA), we go to somewhat heroic measures to + ensure that areas with zero alpha value pixels get something reasonable + in the RGB values. If you don't care about the RGB values of zero alpha + pixels, you can call the stbir_set_non_pm_alpha_speed_over_quality() + function - this runs a premultiplied resize about 25% faster. That said, + when you really care about speed, using premultiplied pixels for both in + and out (STBIR_RGBA_PM, etc) much faster than both of these premultiplied + options. + + PIXEL LAYOUT CONVERSION + The resizer can convert from some pixel layouts to others. When using the + stbir_set_pixel_layouts(), you can, for example, specify STBIR_RGBA + on input, and STBIR_ARGB on output, and it will re-organize the channels + during the resize. Currently, you can only convert between two pixel + layouts with the same number of channels. + + DETERMINISM + We commit to being deterministic (from x64 to ARM to scalar to SIMD, etc). + This requires compiling with fast-math off (using at least /fp:precise). + Also, you must turn off fp-contracting (which turns mult+adds into fmas)! + We attempt to do this with pragmas, but with Clang, you usually want to add + -ffp-contract=off to the command line as well. + + For 32-bit x86, you must use SSE and SSE2 codegen for determinism. That is, + if the scalar x87 unit gets used at all, we immediately lose determinism. + On Microsoft Visual Studio 2008 and earlier, from what we can tell there is + no way to be deterministic in 32-bit x86 (some x87 always leaks in, even + with fp:strict). On 32-bit x86 GCC, determinism requires both -msse2 and + -fpmath=sse. + + Note that we will not be deterministic with float data containing NaNs - + the NaNs will propagate differently on different SIMD and platforms. + + If you turn on STBIR_USE_FMA, then we will be deterministic with other + fma targets, but we will differ from non-fma targets (this is unavoidable, + because a fma isn't simply an add with a mult - it also introduces a + rounding difference compared to non-fma instruction sequences. + + FLOAT PIXEL FORMAT RANGE + Any range of values can be used for the non-alpha float data that you pass + in (0 to 1, -1 to 1, whatever). However, if you are inputting float values + but *outputting* bytes or shorts, you must use a range of 0 to 1 so that we + scale back properly. The alpha channel must also be 0 to 1 for any format + that does premultiplication prior to resizing. + + Note also that with float output, using filters with negative lobes, the + output filtered values might go slightly out of range. You can define + STBIR_FLOAT_LOW_CLAMP and/or STBIR_FLOAT_HIGH_CLAMP to specify the range + to clamp to on output, if that's important. + + MAX/MIN SCALE FACTORS + The input pixel resolutions are in integers, and we do the internal pointer + resolution in size_t sized integers. However, the scale ratio from input + resolution to output resolution is calculated in float form. This means + the effective possible scale ratio is limited to 24 bits (or 16 million + to 1). As you get close to the size of the float resolution (again, 16 + million pixels wide or high), you might start seeing float inaccuracy + issues in general in the pipeline. If you have to do extreme resizes, + you can usually do this is multiple stages (using float intermediate + buffers). + + FLIPPED IMAGES + Stride is just the delta from one scanline to the next. This means you can + use a negative stride to handle inverted images (point to the final + scanline and use a negative stride). You can invert the input or output, + using negative strides. + + DEFAULT FILTERS + For functions which don't provide explicit control over what filters to + use, you can change the compile-time defaults with: + + #define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_something + #define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_something + + See stbir_filter in the header-file section for the list of filters. + + NEW FILTERS + A number of 1D filter kernels are supplied. For a list of supported + filters, see the stbir_filter enum. You can install your own filters by + using the stbir_set_filter_callbacks function. + + PROGRESS + For interactive use with slow resize operations, you can use the + scanline callbacks in the extended API. It would have to be a *very* large + image resample to need progress though - we're very fast. + + CEIL and FLOOR + In scalar mode, the only functions we use from math.h are ceilf and floorf, + but if you have your own versions, you can define the STBIR_CEILF(v) and + STBIR_FLOORF(v) macros and we'll use them instead. In SIMD, we just use + our own versions. + + ASSERT + Define STBIR_ASSERT(boolval) to override assert() and not use assert.h + + PORTING FROM VERSION 1 + The API has changed. You can continue to use the old version of stb_image_resize.h, + which is available in the "deprecated/" directory. + + If you're using the old simple-to-use API, porting is straightforward. + (For more advanced APIs, read the documentation.) + + stbir_resize_uint8(): + - call `stbir_resize_uint8_linear`, cast channel count to `stbir_pixel_layout` + + stbir_resize_float(): + - call `stbir_resize_float_linear`, cast channel count to `stbir_pixel_layout` + + stbir_resize_uint8_srgb(): + - function name is unchanged + - cast channel count to `stbir_pixel_layout` + - above is sufficient unless your image has alpha and it's not RGBA/BGRA + - in that case, follow the below instructions for stbir_resize_uint8_srgb_edgemode + + stbir_resize_uint8_srgb_edgemode() + - switch to the "medium complexity" API + - stbir_resize(), very similar API but a few more parameters: + - pixel_layout: cast channel count to `stbir_pixel_layout` + - data_type: STBIR_TYPE_UINT8_SRGB + - edge: unchanged (STBIR_EDGE_WRAP, etc.) + - filter: STBIR_FILTER_DEFAULT + - which channel is alpha is specified in stbir_pixel_layout, see enum for details + + FUTURE TODOS + * For polyphase integral filters, we just memcpy the coeffs to dupe + them, but we should indirect and use the same coeff memory. + * Add pixel layout conversions for sensible different channel counts + (maybe, 1->3/4, 3->4, 4->1, 3->1). + * For SIMD encode and decode scanline routines, do any pre-aligning + for bad input/output buffer alignments and pitch? + * For very wide scanlines, we should we do vertical strips to stay within + L2 cache. Maybe do chunks of 1K pixels at a time. There would be + some pixel reconversion, but probably dwarfed by things falling out + of cache. Probably also something possible with alternating between + scattering and gathering at high resize scales? + * Should we have a multiple MIPs at the same time function (could keep + more memory in cache during multiple resizes)? + * Rewrite the coefficient generator to do many at once. + * AVX-512 vertical kernels - worried about downclocking here. + * Convert the reincludes to macros when we know they aren't changing. + * Experiment with pivoting the horizontal and always using the + vertical filters (which are faster, but perhaps not enough to overcome + the pivot cost and the extra memory touches). Need to buffer the whole + image so have to balance memory use. + * Most of our code is internally function pointers, should we compile + all the SIMD stuff always and dynamically dispatch? + + CONTRIBUTORS + Jeff Roberts: 2.0 implementation, optimizations, SIMD + Martins Mozeiko: NEON simd, WASM simd, clang and GCC whisperer + Fabian Giesen: half float and srgb converters + Sean Barrett: API design, optimizations + Jorge L Rodriguez: Original 1.0 implementation + Aras Pranckevicius: bugfixes + Nathan Reed: warning fixes for 1.0 + + REVISIONS + 2.18 (2026-03-25) fixed coefficient calculation when skipping a coefficient off + the left side of the window, added non-aligned access safe + memcpy mode for scalar path, fixed various typos, and fixed + define error in the float clamp output mode. + 2.17 (2025-10-25) silly format bug in easy-to-use APIs. + 2.16 (2025-10-21) fixed the easy-to-use APIs to allow inverted bitmaps (negative + strides), fix vertical filter kernel callback, fix threaded + gather buffer priming (and assert). + (thanks adipose, TainZerL, and Harrison Green) + 2.15 (2025-07-17) fixed an assert in debug mode when using floats with input + callbacks, work around GCC warning when adding to null ptr + (thanks Johannes Spohr and Pyry Kovanen). + 2.14 (2025-05-09) fixed a bug using downsampling gather horizontal first, and + scatter with vertical first. + 2.13 (2025-02-27) fixed a bug when using input callbacks, turned off simd for + tiny-c, fixed some variables that should have been static, + fixes a bug when calculating temp memory with resizes that + exceed 2GB of temp memory (very large resizes). + 2.12 (2024-10-18) fix incorrect use of user_data with STBIR_FREE + 2.11 (2024-09-08) fix harmless asan warnings in 2-channel and 3-channel mode + with AVX-2, fix some weird scaling edge conditions with + point sample mode. + 2.10 (2024-07-27) fix the defines GCC and mingw for loop unroll control, + fix MSVC 32-bit arm half float routines. + 2.09 (2024-06-19) fix the defines for 32-bit ARM GCC builds (was selecting + hardware half floats). + 2.08 (2024-06-10) fix for RGB->BGR three channel flips and add SIMD (thanks + to Ryan Salsbury), fix for sub-rect resizes, use the + pragmas to control unrolling when they are available. + 2.07 (2024-05-24) fix for slow final split during threaded conversions of very + wide scanlines when downsampling (caused by extra input + converting), fix for wide scanline resamples with many + splits (int overflow), fix GCC warning. + 2.06 (2024-02-10) fix for identical width/height 3x or more down-scaling + undersampling a single row on rare resize ratios (about 1%). + 2.05 (2024-02-07) fix for 2 pixel to 1 pixel resizes with wrap (thanks Aras), + fix for output callback (thanks Julien Koenen). + 2.04 (2023-11-17) fix for rare AVX bug, shadowed symbol (thanks Nikola Smiljanic). + 2.03 (2023-11-01) ASAN and TSAN warnings fixed, minor tweaks. + 2.00 (2023-10-10) mostly new source: new api, optimizations, simd, vertical-first, etc + 2x-5x faster without simd, 4x-12x faster with simd, + in some cases, 20x to 40x faster esp resizing large to very small. + 0.96 (2019-03-04) fixed warnings + 0.95 (2017-07-23) fixed warnings + 0.94 (2017-03-18) fixed warnings + 0.93 (2017-03-03) fixed bug with certain combinations of heights + 0.92 (2017-01-02) fix integer overflow on large (>2GB) images + 0.91 (2016-04-02) fix warnings; fix handling of subpixel regions + 0.90 (2014-09-17) first released version + + LICENSE + See end of file for license information. +*/ + +#if !defined(STB_IMAGE_RESIZE_DO_HORIZONTALS) && !defined(STB_IMAGE_RESIZE_DO_VERTICALS) && !defined(STB_IMAGE_RESIZE_DO_CODERS) // for internal re-includes + +#ifndef STBIR_INCLUDE_STB_IMAGE_RESIZE2_H +#define STBIR_INCLUDE_STB_IMAGE_RESIZE2_H + +#include <stddef.h> +#ifdef _MSC_VER +typedef unsigned char stbir_uint8; +typedef unsigned short stbir_uint16; +typedef unsigned int stbir_uint32; +typedef unsigned __int64 stbir_uint64; +#else +#include <stdint.h> +typedef uint8_t stbir_uint8; +typedef uint16_t stbir_uint16; +typedef uint32_t stbir_uint32; +typedef uint64_t stbir_uint64; +#endif + +#ifndef STBIRDEF +#ifdef STB_IMAGE_RESIZE_STATIC +#define STBIRDEF static +#else +#ifdef __cplusplus +#define STBIRDEF extern "C" +#else +#define STBIRDEF extern +#endif +#endif +#endif + +////////////////////////////////////////////////////////////////////////////// +//// start "header file" /////////////////////////////////////////////////// +// +// Easy-to-use API: +// +// * stride is the offset between successive rows of image data +// in memory, in bytes. specify 0 for packed continuously in memory +// * colorspace is linear or sRGB as specified by function name +// * Uses the default filters +// * Uses edge mode clamped +// * returned result is 1 for success or 0 in case of an error. + + +// stbir_pixel_layout specifies: +// number of channels +// order of channels +// whether color is premultiplied by alpha +// for back compatibility, you can cast the old channel count to an stbir_pixel_layout +typedef enum +{ + STBIR_1CHANNEL = 1, + STBIR_2CHANNEL = 2, + STBIR_RGB = 3, // 3-chan, with order specified (for channel flipping) + STBIR_BGR = 0, // 3-chan, with order specified (for channel flipping) + STBIR_4CHANNEL = 5, + + STBIR_RGBA = 4, // alpha formats, where alpha is NOT premultiplied into color channels + STBIR_BGRA = 6, + STBIR_ARGB = 7, + STBIR_ABGR = 8, + STBIR_RA = 9, + STBIR_AR = 10, + + STBIR_RGBA_PM = 11, // alpha formats, where alpha is premultiplied into color channels + STBIR_BGRA_PM = 12, + STBIR_ARGB_PM = 13, + STBIR_ABGR_PM = 14, + STBIR_RA_PM = 15, + STBIR_AR_PM = 16, + + STBIR_RGBA_NO_AW = 11, // alpha formats, where NO alpha weighting is applied at all! + STBIR_BGRA_NO_AW = 12, // these are just synonyms for the _PM flags (which also do + STBIR_ARGB_NO_AW = 13, // no alpha weighting). These names just make it more clear + STBIR_ABGR_NO_AW = 14, // for some folks). + STBIR_RA_NO_AW = 15, + STBIR_AR_NO_AW = 16, + +} stbir_pixel_layout; + +//=============================================================== +// Simple-complexity API +// +// If output_pixels is NULL (0), then we will allocate the buffer and return it to you. +//-------------------------------- + +STBIRDEF unsigned char * stbir_resize_uint8_srgb( const unsigned char *input_pixels , int input_w , int input_h, int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_pixel_layout pixel_type ); + +STBIRDEF unsigned char * stbir_resize_uint8_linear( const unsigned char *input_pixels , int input_w , int input_h, int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_pixel_layout pixel_type ); + +STBIRDEF float * stbir_resize_float_linear( const float *input_pixels , int input_w , int input_h, int input_stride_in_bytes, + float *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_pixel_layout pixel_type ); +//=============================================================== + +//=============================================================== +// Medium-complexity API +// +// This extends the easy-to-use API as follows: +// +// * Can specify the datatype - U8, U8_SRGB, U16, FLOAT, HALF_FLOAT +// * Edge wrap can selected explicitly +// * Filter can be selected explicitly +//-------------------------------- + +typedef enum +{ + STBIR_EDGE_CLAMP = 0, + STBIR_EDGE_REFLECT = 1, + STBIR_EDGE_WRAP = 2, // this edge mode is slower and uses more memory + STBIR_EDGE_ZERO = 3, +} stbir_edge; + +typedef enum +{ + STBIR_FILTER_DEFAULT = 0, // use same filter type that easy-to-use API chooses + STBIR_FILTER_BOX = 1, // A trapezoid w/1-pixel wide ramps, same result as box for integer scale ratios + STBIR_FILTER_TRIANGLE = 2, // On upsampling, produces same results as bilinear texture filtering + STBIR_FILTER_CUBICBSPLINE = 3, // The cubic b-spline (aka Mitchell-Netrevalli with B=1,C=0), gaussian-esque + STBIR_FILTER_CATMULLROM = 4, // An interpolating cubic spline + STBIR_FILTER_MITCHELL = 5, // Mitchell-Netrevalli filter with B=1/3, C=1/3 + STBIR_FILTER_POINT_SAMPLE = 6, // Simple point sampling + STBIR_FILTER_OTHER = 7, // User callback specified +} stbir_filter; + +typedef enum +{ + STBIR_TYPE_UINT8 = 0, + STBIR_TYPE_UINT8_SRGB = 1, + STBIR_TYPE_UINT8_SRGB_ALPHA = 2, // alpha channel, when present, should also be SRGB (this is very unusual) + STBIR_TYPE_UINT16 = 3, + STBIR_TYPE_FLOAT = 4, + STBIR_TYPE_HALF_FLOAT = 5 +} stbir_datatype; + +// medium api +STBIRDEF void * stbir_resize( const void *input_pixels , int input_w , int input_h, int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_pixel_layout pixel_layout, stbir_datatype data_type, + stbir_edge edge, stbir_filter filter ); +//=============================================================== + + + +//=============================================================== +// Extended-complexity API +// +// This API exposes all resize functionality. +// +// * Separate filter types for each axis +// * Separate edge modes for each axis +// * Separate input and output data types +// * Can specify regions with subpixel correctness +// * Can specify alpha flags +// * Can specify a memory callback +// * Can specify a callback data type for pixel input and output +// * Can be threaded for a single resize +// * Can be used to resize many frames without recalculating the sampler info +// +// Use this API as follows: +// 1) Call the stbir_resize_init function on a local STBIR_RESIZE structure +// 2) Call any of the stbir_set functions +// 3) Optionally call stbir_build_samplers() if you are going to resample multiple times +// with the same input and output dimensions (like resizing video frames) +// 4) Resample by calling stbir_resize_extended(). +// 5) Call stbir_free_samplers() if you called stbir_build_samplers() +//-------------------------------- + + +// Types: + +// INPUT CALLBACK: this callback is used for input scanlines +typedef void const * stbir_input_callback( void * optional_output, void const * input_ptr, int num_pixels, int x, int y, void * context ); + +// OUTPUT CALLBACK: this callback is used for output scanlines +typedef void stbir_output_callback( void const * output_ptr, int num_pixels, int y, void * context ); + +// callbacks for user installed filters +typedef float stbir__kernel_callback( float x, float scale, void * user_data ); // centered at zero +typedef float stbir__support_callback( float scale, void * user_data ); + +// internal structure with precomputed scaling +typedef struct stbir__info stbir__info; + +typedef struct STBIR_RESIZE // use the stbir_resize_init and stbir_override functions to set these values for future compatibility +{ + void * user_data; + void const * input_pixels; + int input_w, input_h; + double input_s0, input_t0, input_s1, input_t1; + stbir_input_callback * input_cb; + void * output_pixels; + int output_w, output_h; + int output_subx, output_suby, output_subw, output_subh; + stbir_output_callback * output_cb; + int input_stride_in_bytes; + int output_stride_in_bytes; + int splits; + int fast_alpha; + int needs_rebuild; + int called_alloc; + stbir_pixel_layout input_pixel_layout_public; + stbir_pixel_layout output_pixel_layout_public; + stbir_datatype input_data_type; + stbir_datatype output_data_type; + stbir_filter horizontal_filter, vertical_filter; + stbir_edge horizontal_edge, vertical_edge; + stbir__kernel_callback * horizontal_filter_kernel; stbir__support_callback * horizontal_filter_support; + stbir__kernel_callback * vertical_filter_kernel; stbir__support_callback * vertical_filter_support; + stbir__info * samplers; +} STBIR_RESIZE; + +// extended complexity api + + +// First off, you must ALWAYS call stbir_resize_init on your resize structure before any of the other calls! +STBIRDEF void stbir_resize_init( STBIR_RESIZE * resize, + const void *input_pixels, int input_w, int input_h, int input_stride_in_bytes, // stride can be zero + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, // stride can be zero + stbir_pixel_layout pixel_layout, stbir_datatype data_type ); + +//=============================================================== +// You can update these parameters any time after resize_init and there is no cost +//-------------------------------- + +STBIRDEF void stbir_set_datatypes( STBIR_RESIZE * resize, stbir_datatype input_type, stbir_datatype output_type ); +STBIRDEF void stbir_set_pixel_callbacks( STBIR_RESIZE * resize, stbir_input_callback * input_cb, stbir_output_callback * output_cb ); // no callbacks by default +STBIRDEF void stbir_set_user_data( STBIR_RESIZE * resize, void * user_data ); // pass back STBIR_RESIZE* by default +STBIRDEF void stbir_set_buffer_ptrs( STBIR_RESIZE * resize, const void * input_pixels, int input_stride_in_bytes, void * output_pixels, int output_stride_in_bytes ); + +//=============================================================== + + +//=============================================================== +// If you call any of these functions, you will trigger a sampler rebuild! +//-------------------------------- + +STBIRDEF int stbir_set_pixel_layouts( STBIR_RESIZE * resize, stbir_pixel_layout input_pixel_layout, stbir_pixel_layout output_pixel_layout ); // sets new buffer layouts +STBIRDEF int stbir_set_edgemodes( STBIR_RESIZE * resize, stbir_edge horizontal_edge, stbir_edge vertical_edge ); // CLAMP by default + +STBIRDEF int stbir_set_filters( STBIR_RESIZE * resize, stbir_filter horizontal_filter, stbir_filter vertical_filter ); // STBIR_DEFAULT_FILTER_UPSAMPLE/DOWNSAMPLE by default +STBIRDEF int stbir_set_filter_callbacks( STBIR_RESIZE * resize, stbir__kernel_callback * horizontal_filter, stbir__support_callback * horizontal_support, stbir__kernel_callback * vertical_filter, stbir__support_callback * vertical_support ); + +STBIRDEF int stbir_set_pixel_subrect( STBIR_RESIZE * resize, int subx, int suby, int subw, int subh ); // sets both sub-regions (full regions by default) +STBIRDEF int stbir_set_input_subrect( STBIR_RESIZE * resize, double s0, double t0, double s1, double t1 ); // sets input sub-region (full region by default) +STBIRDEF int stbir_set_output_pixel_subrect( STBIR_RESIZE * resize, int subx, int suby, int subw, int subh ); // sets output sub-region (full region by default) + +// when inputting AND outputting non-premultiplied alpha pixels, we use a slower but higher quality technique +// that fills the zero alpha pixel's RGB values with something plausible. If you don't care about areas of +// zero alpha, you can call this function to get about a 25% speed improvement for STBIR_RGBA to STBIR_RGBA +// types of resizes. +STBIRDEF int stbir_set_non_pm_alpha_speed_over_quality( STBIR_RESIZE * resize, int non_pma_alpha_speed_over_quality ); +//=============================================================== + + +//=============================================================== +// You can call build_samplers to prebuild all the internal data we need to resample. +// Then, if you call resize_extended many times with the same resize, you only pay the +// cost once. +// If you do call build_samplers, you MUST call free_samplers eventually. +//-------------------------------- + +// This builds the samplers and does one allocation +STBIRDEF int stbir_build_samplers( STBIR_RESIZE * resize ); + +// You MUST call this, if you call stbir_build_samplers or stbir_build_samplers_with_splits +STBIRDEF void stbir_free_samplers( STBIR_RESIZE * resize ); +//=============================================================== + + +// And this is the main function to perform the resize synchronously on one thread. +STBIRDEF int stbir_resize_extended( STBIR_RESIZE * resize ); + + +//=============================================================== +// Use these functions for multithreading. +// 1) You call stbir_build_samplers_with_splits first on the main thread +// 2) Then stbir_resize_with_split on each thread +// 3) stbir_free_samplers when done on the main thread +//-------------------------------- + +// This will build samplers for threading. +// You can pass in the number of threads you'd like to use (try_splits). +// It returns the number of splits (threads) that you can call it with. +/// It might be less if the image resize can't be split up that many ways. + +STBIRDEF int stbir_build_samplers_with_splits( STBIR_RESIZE * resize, int try_splits ); + +// This function does a split of the resizing (you call this fuction for each +// split, on multiple threads). A split is a piece of the output resize pixel space. + +// Note that you MUST call stbir_build_samplers_with_splits before stbir_resize_extended_split! + +// Usually, you will always call stbir_resize_split with split_start as the thread_index +// and "1" for the split_count. +// But, if you have a weird situation where you MIGHT want 8 threads, but sometimes +// only 4 threads, you can use 0,2,4,6 for the split_start's and use "2" for the +// split_count each time to turn in into a 4 thread resize. (This is unusual). + +STBIRDEF int stbir_resize_extended_split( STBIR_RESIZE * resize, int split_start, int split_count ); +//=============================================================== + + +//=============================================================== +// Pixel Callbacks info: +//-------------------------------- + +// The input callback is super flexible - it calls you with the input address +// (based on the stride and base pointer), it gives you an optional_output +// pointer that you can fill, or you can just return your own pointer into +// your own data. +// +// You can also do conversion from non-supported data types if necessary - in +// this case, you ignore the input_ptr and just use the x and y parameters to +// calculate your own input_ptr based on the size of each non-supported pixel. +// (Something like the third example below.) +// +// You can also install just an input or just an output callback by setting the +// callback that you don't want to zero. +// +// First example, progress: (getting a callback that you can monitor the progress): +// void const * my_callback( void * optional_output, void const * input_ptr, int num_pixels, int x, int y, void * context ) +// { +// percentage_done = y / input_height; +// return input_ptr; // use buffer from call +// } +// +// Next example, copying: (copy from some other buffer or stream): +// void const * my_callback( void * optional_output, void const * input_ptr, int num_pixels, int x, int y, void * context ) +// { +// CopyOrStreamData( optional_output, other_data_src, num_pixels * pixel_width_in_bytes ); +// return optional_output; // return the optional buffer that we filled +// } +// +// Third example, input another buffer without copying: (zero-copy from other buffer): +// void const * my_callback( void * optional_output, void const * input_ptr, int num_pixels, int x, int y, void * context ) +// { +// void * pixels = ( (char*) other_image_base ) + ( y * other_image_stride ) + ( x * other_pixel_width_in_bytes ); +// return pixels; // return pointer to your data without copying +// } +// +// +// The output callback is considerably simpler - it just calls you so that you can dump +// out each scanline. You could even directly copy out to disk if you have a simple format +// like TGA or BMP. You can also convert to other output types here if you want. +// +// Simple example: +// void const * my_output( void * output_ptr, int num_pixels, int y, void * context ) +// { +// percentage_done = y / output_height; +// fwrite( output_ptr, pixel_width_in_bytes, num_pixels, output_file ); +// } +//=============================================================== + + + + +//=============================================================== +// optional built-in profiling API +//-------------------------------- + +#ifdef STBIR_PROFILE + +typedef struct STBIR_PROFILE_INFO +{ + stbir_uint64 total_clocks; + + // how many clocks spent (of total_clocks) in the various resize routines, along with a string description + // there are "resize_count" number of zones + stbir_uint64 clocks[ 8 ]; + char const ** descriptions; + + // count of clocks and descriptions + stbir_uint32 count; +} STBIR_PROFILE_INFO; + +// use after calling stbir_resize_extended (or stbir_build_samplers or stbir_build_samplers_with_splits) +STBIRDEF void stbir_resize_build_profile_info( STBIR_PROFILE_INFO * out_info, STBIR_RESIZE const * resize ); + +// use after calling stbir_resize_extended +STBIRDEF void stbir_resize_extended_profile_info( STBIR_PROFILE_INFO * out_info, STBIR_RESIZE const * resize ); + +// use after calling stbir_resize_extended_split +STBIRDEF void stbir_resize_split_profile_info( STBIR_PROFILE_INFO * out_info, STBIR_RESIZE const * resize, int split_start, int split_num ); + +//=============================================================== + +#endif + + +//// end header file ///////////////////////////////////////////////////// +#endif // STBIR_INCLUDE_STB_IMAGE_RESIZE2_H + +#if defined(STB_IMAGE_RESIZE_IMPLEMENTATION) || defined(STB_IMAGE_RESIZE2_IMPLEMENTATION) + +#ifndef STBIR_ASSERT +#include <assert.h> +#define STBIR_ASSERT(x) assert(x) +#endif + +#ifndef STBIR_MALLOC +#include <stdlib.h> +#define STBIR_MALLOC(size,user_data) ((void)(user_data), malloc(size)) +#define STBIR_FREE(ptr,user_data) ((void)(user_data), free(ptr)) +// (we used the comma operator to evaluate user_data, to avoid "unused parameter" warnings) +#endif + +#ifdef _MSC_VER + +#define stbir__inline __forceinline + +#else + +#define stbir__inline __inline__ + +// Clang address sanitizer +#if defined(__has_feature) + #if __has_feature(address_sanitizer) || __has_feature(memory_sanitizer) + #ifndef STBIR__SEPARATE_ALLOCATIONS + #define STBIR__SEPARATE_ALLOCATIONS + #endif + #endif +#endif + +#endif + +// GCC and MSVC +#if defined(__SANITIZE_ADDRESS__) + #ifndef STBIR__SEPARATE_ALLOCATIONS + #define STBIR__SEPARATE_ALLOCATIONS + #endif +#endif + +// Always turn off automatic FMA use - use STBIR_USE_FMA if you want. +// Otherwise, this is a determinism disaster. +#ifndef STBIR_DONT_CHANGE_FP_CONTRACT // override in case you don't want this behavior +#if defined(_MSC_VER) && !defined(__clang__) +#if _MSC_VER > 1200 +#pragma fp_contract(off) +#endif +#elif defined(__GNUC__) && !defined(__clang__) +#pragma GCC optimize("fp-contract=off") +#else +#pragma STDC FP_CONTRACT OFF +#endif +#endif + +#ifdef _MSC_VER +#define STBIR__UNUSED(v) (void)(v) +#else +#define STBIR__UNUSED(v) (void)sizeof(v) +#endif + +#define STBIR__ARRAY_SIZE(a) (sizeof((a))/sizeof((a)[0])) + + +#ifndef STBIR_DEFAULT_FILTER_UPSAMPLE +#define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_CATMULLROM +#endif + +#ifndef STBIR_DEFAULT_FILTER_DOWNSAMPLE +#define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_MITCHELL +#endif + + +#ifndef STBIR__HEADER_FILENAME +#define STBIR__HEADER_FILENAME "stb_image_resize2.h" +#endif + +// the internal pixel layout enums are in a different order, so we can easily do range comparisons of types +// the public pixel layout is ordered in a way that if you cast num_channels (1-4) to the enum, you get something sensible +typedef enum +{ + STBIRI_1CHANNEL = 0, + STBIRI_2CHANNEL = 1, + STBIRI_RGB = 2, + STBIRI_BGR = 3, + STBIRI_4CHANNEL = 4, + + STBIRI_RGBA = 5, + STBIRI_BGRA = 6, + STBIRI_ARGB = 7, + STBIRI_ABGR = 8, + STBIRI_RA = 9, + STBIRI_AR = 10, + + STBIRI_RGBA_PM = 11, + STBIRI_BGRA_PM = 12, + STBIRI_ARGB_PM = 13, + STBIRI_ABGR_PM = 14, + STBIRI_RA_PM = 15, + STBIRI_AR_PM = 16, +} stbir_internal_pixel_layout; + +// define the public pixel layouts to not compile inside the implementation (to avoid accidental use) +#define STBIR_BGR bad_dont_use_in_implementation +#define STBIR_1CHANNEL STBIR_BGR +#define STBIR_2CHANNEL STBIR_BGR +#define STBIR_RGB STBIR_BGR +#define STBIR_RGBA STBIR_BGR +#define STBIR_4CHANNEL STBIR_BGR +#define STBIR_BGRA STBIR_BGR +#define STBIR_ARGB STBIR_BGR +#define STBIR_ABGR STBIR_BGR +#define STBIR_RA STBIR_BGR +#define STBIR_AR STBIR_BGR +#define STBIR_RGBA_PM STBIR_BGR +#define STBIR_BGRA_PM STBIR_BGR +#define STBIR_ARGB_PM STBIR_BGR +#define STBIR_ABGR_PM STBIR_BGR +#define STBIR_RA_PM STBIR_BGR +#define STBIR_AR_PM STBIR_BGR + +// must match stbir_datatype +static unsigned char stbir__type_size[] = { + 1,1,1,2,4,2 // STBIR_TYPE_UINT8,STBIR_TYPE_UINT8_SRGB,STBIR_TYPE_UINT8_SRGB_ALPHA,STBIR_TYPE_UINT16,STBIR_TYPE_FLOAT,STBIR_TYPE_HALF_FLOAT +}; + +// When gathering, the contributors are which source pixels contribute. +// When scattering, the contributors are which destination pixels are contributed to. +typedef struct +{ + int n0; // First contributing pixel + int n1; // Last contributing pixel +} stbir__contributors; + +typedef struct +{ + int lowest; // First sample index for whole filter + int highest; // Last sample index for whole filter + int widest; // widest single set of samples for an output +} stbir__filter_extent_info; + +typedef struct +{ + int n0; // First pixel of decode buffer to write to + int n1; // Last pixel of decode that will be written to + int pixel_offset_for_input; // Pixel offset into input_scanline +} stbir__span; + +typedef struct stbir__scale_info +{ + int input_full_size; + int output_sub_size; + float scale; + float inv_scale; + float pixel_shift; // starting shift in output pixel space (in pixels) + int scale_is_rational; + stbir_uint32 scale_numerator, scale_denominator; +} stbir__scale_info; + +typedef struct +{ + stbir__contributors * contributors; + float* coefficients; + stbir__contributors * gather_prescatter_contributors; + float * gather_prescatter_coefficients; + stbir__scale_info scale_info; + float support; + stbir_filter filter_enum; + stbir__kernel_callback * filter_kernel; + stbir__support_callback * filter_support; + stbir_edge edge; + int coefficient_width; + int filter_pixel_width; + int filter_pixel_margin; + int num_contributors; + int contributors_size; + int coefficients_size; + stbir__filter_extent_info extent_info; + int is_gather; // 0 = scatter, 1 = gather with scale >= 1, 2 = gather with scale < 1 + int gather_prescatter_num_contributors; + int gather_prescatter_coefficient_width; + int gather_prescatter_contributors_size; + int gather_prescatter_coefficients_size; +} stbir__sampler; + +typedef struct +{ + stbir__contributors conservative; + int edge_sizes[2]; // this can be less than filter_pixel_margin, if the filter and scaling falls off + stbir__span spans[2]; // can be two spans, if doing input subrect with clamp mode WRAP +} stbir__extents; + +typedef struct +{ +#ifdef STBIR_PROFILE + union + { + struct { stbir_uint64 total, looping, vertical, horizontal, decode, encode, alpha, unalpha; } named; + stbir_uint64 array[8]; + } profile; + stbir_uint64 * current_zone_excluded_ptr; +#endif + float* decode_buffer; + + int ring_buffer_first_scanline; + int ring_buffer_last_scanline; + int ring_buffer_begin_index; // first_scanline is at this index in the ring buffer + int start_output_y, end_output_y; + int start_input_y, end_input_y; // used in scatter only + + #ifdef STBIR__SEPARATE_ALLOCATIONS + float** ring_buffers; // one pointer for each ring buffer + #else + float* ring_buffer; // one big buffer that we index into + #endif + + float* vertical_buffer; + + char no_cache_straddle[64]; +} stbir__per_split_info; + +typedef float * stbir__decode_pixels_func( float * decode, int width_times_channels, void const * input ); +typedef void stbir__alpha_weight_func( float * decode_buffer, int width_times_channels ); +typedef void stbir__horizontal_gather_channels_func( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, + stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ); +typedef void stbir__alpha_unweight_func(float * encode_buffer, int width_times_channels ); +typedef void stbir__encode_pixels_func( void * output, int width_times_channels, float const * encode ); + +struct stbir__info +{ +#ifdef STBIR_PROFILE + union + { + struct { stbir_uint64 total, build, alloc, horizontal, vertical, cleanup, pivot; } named; + stbir_uint64 array[7]; + } profile; + stbir_uint64 * current_zone_excluded_ptr; +#endif + stbir__sampler horizontal; + stbir__sampler vertical; + + void const * input_data; + void * output_data; + + int input_stride_bytes; + int output_stride_bytes; + int ring_buffer_length_bytes; // The length of an individual entry in the ring buffer. The total number of ring buffers is stbir__get_filter_pixel_width(filter) + int ring_buffer_num_entries; // Total number of entries in the ring buffer. + + stbir_datatype input_type; + stbir_datatype output_type; + + stbir_input_callback * in_pixels_cb; + void * user_data; + stbir_output_callback * out_pixels_cb; + + stbir__extents scanline_extents; + + void * alloced_mem; + stbir__per_split_info * split_info; // by default 1, but there will be N of these allocated based on the thread init you did + + stbir__decode_pixels_func * decode_pixels; + stbir__alpha_weight_func * alpha_weight; + stbir__horizontal_gather_channels_func * horizontal_gather_channels; + stbir__alpha_unweight_func * alpha_unweight; + stbir__encode_pixels_func * encode_pixels; + + int alloc_ring_buffer_num_entries; // Number of entries in the ring buffer that will be allocated + int splits; // count of splits + + stbir_internal_pixel_layout input_pixel_layout_internal; + stbir_internal_pixel_layout output_pixel_layout_internal; + + int input_color_and_type; + int offset_x, offset_y; // offset within output_data + int vertical_first; + int channels; + int effective_channels; // same as channels, except on RGBA/ARGB (7), or XA/AX (3) + size_t alloced_total; +}; + + +#define stbir__max_uint8_as_float 255.0f +#define stbir__max_uint16_as_float 65535.0f +#define stbir__max_uint8_as_float_inverted 3.9215689e-03f // (1.0f/255.0f) +#define stbir__max_uint16_as_float_inverted 1.5259022e-05f // (1.0f/65535.0f) +#define stbir__small_float ((float)1 / (1 << 20) / (1 << 20) / (1 << 20) / (1 << 20) / (1 << 20) / (1 << 20)) + +// min/max friendly +#define STBIR_CLAMP(x, xmin, xmax) for(;;) { \ + if ( (x) < (xmin) ) (x) = (xmin); \ + if ( (x) > (xmax) ) (x) = (xmax); \ + break; \ +} + +static stbir__inline int stbir__min(int a, int b) +{ + return a < b ? a : b; +} + +static stbir__inline int stbir__max(int a, int b) +{ + return a > b ? a : b; +} + +static float stbir__srgb_uchar_to_linear_float[256] = { + 0.000000f, 0.000304f, 0.000607f, 0.000911f, 0.001214f, 0.001518f, 0.001821f, 0.002125f, 0.002428f, 0.002732f, 0.003035f, + 0.003347f, 0.003677f, 0.004025f, 0.004391f, 0.004777f, 0.005182f, 0.005605f, 0.006049f, 0.006512f, 0.006995f, 0.007499f, + 0.008023f, 0.008568f, 0.009134f, 0.009721f, 0.010330f, 0.010960f, 0.011612f, 0.012286f, 0.012983f, 0.013702f, 0.014444f, + 0.015209f, 0.015996f, 0.016807f, 0.017642f, 0.018500f, 0.019382f, 0.020289f, 0.021219f, 0.022174f, 0.023153f, 0.024158f, + 0.025187f, 0.026241f, 0.027321f, 0.028426f, 0.029557f, 0.030713f, 0.031896f, 0.033105f, 0.034340f, 0.035601f, 0.036889f, + 0.038204f, 0.039546f, 0.040915f, 0.042311f, 0.043735f, 0.045186f, 0.046665f, 0.048172f, 0.049707f, 0.051269f, 0.052861f, + 0.054480f, 0.056128f, 0.057805f, 0.059511f, 0.061246f, 0.063010f, 0.064803f, 0.066626f, 0.068478f, 0.070360f, 0.072272f, + 0.074214f, 0.076185f, 0.078187f, 0.080220f, 0.082283f, 0.084376f, 0.086500f, 0.088656f, 0.090842f, 0.093059f, 0.095307f, + 0.097587f, 0.099899f, 0.102242f, 0.104616f, 0.107023f, 0.109462f, 0.111932f, 0.114435f, 0.116971f, 0.119538f, 0.122139f, + 0.124772f, 0.127438f, 0.130136f, 0.132868f, 0.135633f, 0.138432f, 0.141263f, 0.144128f, 0.147027f, 0.149960f, 0.152926f, + 0.155926f, 0.158961f, 0.162029f, 0.165132f, 0.168269f, 0.171441f, 0.174647f, 0.177888f, 0.181164f, 0.184475f, 0.187821f, + 0.191202f, 0.194618f, 0.198069f, 0.201556f, 0.205079f, 0.208637f, 0.212231f, 0.215861f, 0.219526f, 0.223228f, 0.226966f, + 0.230740f, 0.234551f, 0.238398f, 0.242281f, 0.246201f, 0.250158f, 0.254152f, 0.258183f, 0.262251f, 0.266356f, 0.270498f, + 0.274677f, 0.278894f, 0.283149f, 0.287441f, 0.291771f, 0.296138f, 0.300544f, 0.304987f, 0.309469f, 0.313989f, 0.318547f, + 0.323143f, 0.327778f, 0.332452f, 0.337164f, 0.341914f, 0.346704f, 0.351533f, 0.356400f, 0.361307f, 0.366253f, 0.371238f, + 0.376262f, 0.381326f, 0.386430f, 0.391573f, 0.396755f, 0.401978f, 0.407240f, 0.412543f, 0.417885f, 0.423268f, 0.428691f, + 0.434154f, 0.439657f, 0.445201f, 0.450786f, 0.456411f, 0.462077f, 0.467784f, 0.473532f, 0.479320f, 0.485150f, 0.491021f, + 0.496933f, 0.502887f, 0.508881f, 0.514918f, 0.520996f, 0.527115f, 0.533276f, 0.539480f, 0.545725f, 0.552011f, 0.558340f, + 0.564712f, 0.571125f, 0.577581f, 0.584078f, 0.590619f, 0.597202f, 0.603827f, 0.610496f, 0.617207f, 0.623960f, 0.630757f, + 0.637597f, 0.644480f, 0.651406f, 0.658375f, 0.665387f, 0.672443f, 0.679543f, 0.686685f, 0.693872f, 0.701102f, 0.708376f, + 0.715694f, 0.723055f, 0.730461f, 0.737911f, 0.745404f, 0.752942f, 0.760525f, 0.768151f, 0.775822f, 0.783538f, 0.791298f, + 0.799103f, 0.806952f, 0.814847f, 0.822786f, 0.830770f, 0.838799f, 0.846873f, 0.854993f, 0.863157f, 0.871367f, 0.879622f, + 0.887923f, 0.896269f, 0.904661f, 0.913099f, 0.921582f, 0.930111f, 0.938686f, 0.947307f, 0.955974f, 0.964686f, 0.973445f, + 0.982251f, 0.991102f, 1.0f +}; + +typedef union +{ + unsigned int u; + float f; +} stbir__FP32; + +// From https://gist.github.com/rygorous/2203834 + +static const stbir_uint32 fp32_to_srgb8_tab4[104] = { + 0x0073000d, 0x007a000d, 0x0080000d, 0x0087000d, 0x008d000d, 0x0094000d, 0x009a000d, 0x00a1000d, + 0x00a7001a, 0x00b4001a, 0x00c1001a, 0x00ce001a, 0x00da001a, 0x00e7001a, 0x00f4001a, 0x0101001a, + 0x010e0033, 0x01280033, 0x01410033, 0x015b0033, 0x01750033, 0x018f0033, 0x01a80033, 0x01c20033, + 0x01dc0067, 0x020f0067, 0x02430067, 0x02760067, 0x02aa0067, 0x02dd0067, 0x03110067, 0x03440067, + 0x037800ce, 0x03df00ce, 0x044600ce, 0x04ad00ce, 0x051400ce, 0x057b00c5, 0x05dd00bc, 0x063b00b5, + 0x06970158, 0x07420142, 0x07e30130, 0x087b0120, 0x090b0112, 0x09940106, 0x0a1700fc, 0x0a9500f2, + 0x0b0f01cb, 0x0bf401ae, 0x0ccb0195, 0x0d950180, 0x0e56016e, 0x0f0d015e, 0x0fbc0150, 0x10630143, + 0x11070264, 0x1238023e, 0x1357021d, 0x14660201, 0x156601e9, 0x165a01d3, 0x174401c0, 0x182401af, + 0x18fe0331, 0x1a9602fe, 0x1c1502d2, 0x1d7e02ad, 0x1ed4028d, 0x201a0270, 0x21520256, 0x227d0240, + 0x239f0443, 0x25c003fe, 0x27bf03c4, 0x29a10392, 0x2b6a0367, 0x2d1d0341, 0x2ebe031f, 0x304d0300, + 0x31d105b0, 0x34a80555, 0x37520507, 0x39d504c5, 0x3c37048b, 0x3e7c0458, 0x40a8042a, 0x42bd0401, + 0x44c20798, 0x488e071e, 0x4c1c06b6, 0x4f76065d, 0x52a50610, 0x55ac05cc, 0x5892058f, 0x5b590559, + 0x5e0c0a23, 0x631c0980, 0x67db08f6, 0x6c55087f, 0x70940818, 0x74a007bd, 0x787d076c, 0x7c330723, +}; + +static stbir__inline stbir_uint8 stbir__linear_to_srgb_uchar(float in) +{ + static const stbir__FP32 almostone = { 0x3f7fffff }; // 1-eps + static const stbir__FP32 minval = { (127-13) << 23 }; + stbir_uint32 tab,bias,scale,t; + stbir__FP32 f; + + // Clamp to [2^(-13), 1-eps]; these two values map to 0 and 1, respectively. + // The tests are carefully written so that NaNs map to 0, same as in the reference + // implementation. + if (!(in > minval.f)) // written this way to catch NaNs + return 0; + if (in > almostone.f) + return 255; + + // Do the table lookup and unpack bias, scale + f.f = in; + tab = fp32_to_srgb8_tab4[(f.u - minval.u) >> 20]; + bias = (tab >> 16) << 9; + scale = tab & 0xffff; + + // Grab next-highest mantissa bits and perform linear interpolation + t = (f.u >> 12) & 0xff; + return (unsigned char) ((bias + scale*t) >> 16); +} + +#ifndef STBIR_FORCE_GATHER_FILTER_SCANLINES_AMOUNT +#define STBIR_FORCE_GATHER_FILTER_SCANLINES_AMOUNT 32 // when downsampling and <= 32 scanlines of buffering, use gather. gather used down to 1/8th scaling for 25% win. +#endif + +#ifndef STBIR_FORCE_MINIMUM_SCANLINES_FOR_SPLITS +#define STBIR_FORCE_MINIMUM_SCANLINES_FOR_SPLITS 4 // when threading, what is the minimum number of scanlines for a split? +#endif + +#define STBIR_INPUT_CALLBACK_PADDING 3 + +#ifdef _M_IX86_FP +#if ( _M_IX86_FP >= 1 ) +#ifndef STBIR_SSE +#define STBIR_SSE +#endif +#endif +#endif + +#ifdef __TINYC__ + // tiny c has no intrinsics yet - this can become a version check if they add them + #define STBIR_NO_SIMD +#endif + +#if defined(_x86_64) || defined( __x86_64__ ) || defined( _M_X64 ) || defined(__x86_64) || defined(_M_AMD64) || defined(__SSE2__) || defined(STBIR_SSE) || defined(STBIR_SSE2) + #ifndef STBIR_SSE2 + #define STBIR_SSE2 + #endif + #if defined(__AVX__) || defined(STBIR_AVX2) + #ifndef STBIR_AVX + #ifndef STBIR_NO_AVX + #define STBIR_AVX + #endif + #endif + #endif + #if defined(__AVX2__) || defined(STBIR_AVX2) + #ifndef STBIR_NO_AVX2 + #ifndef STBIR_AVX2 + #define STBIR_AVX2 + #endif + #if defined( _MSC_VER ) && !defined(__clang__) + #ifndef STBIR_FP16C // FP16C instructions are on all AVX2 cpus, so we can autoselect it here on microsoft - clang needs -mf16c + #define STBIR_FP16C + #endif + #endif + #endif + #endif + #ifdef __F16C__ + #ifndef STBIR_FP16C // turn on FP16C instructions if the define is set (for clang and gcc) + #define STBIR_FP16C + #endif + #endif +#endif + +#if defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) || ((__ARM_NEON_FP & 4) != 0) || defined(__ARM_NEON__) +#ifndef STBIR_NEON +#define STBIR_NEON +#endif +#endif + +#if defined(_M_ARM) || defined(__arm__) +#ifdef STBIR_USE_FMA +#undef STBIR_USE_FMA // no FMA for 32-bit arm on MSVC +#endif +#endif + +#if defined(__wasm__) && defined(__wasm_simd128__) +#ifndef STBIR_WASM +#define STBIR_WASM +#endif +#endif + +// restrict pointers for the output pointers, other loop and unroll control +#if defined( _MSC_VER ) && !defined(__clang__) + #define STBIR_STREAMOUT_PTR( star ) star __restrict + #define STBIR_NO_UNROLL( ptr ) __assume(ptr) // this oddly keeps msvc from unrolling a loop + #if _MSC_VER >= 1900 + #define STBIR_NO_UNROLL_LOOP_START __pragma(loop( no_vector )) + #else + #define STBIR_NO_UNROLL_LOOP_START + #endif +#elif defined( __clang__ ) + #define STBIR_STREAMOUT_PTR( star ) star __restrict__ + #define STBIR_NO_UNROLL( ptr ) __asm__ (""::"r"(ptr)) + #if ( __clang_major__ >= 4 ) || ( ( __clang_major__ >= 3 ) && ( __clang_minor__ >= 5 ) ) + #define STBIR_NO_UNROLL_LOOP_START _Pragma("clang loop unroll(disable)") _Pragma("clang loop vectorize(disable)") + #else + #define STBIR_NO_UNROLL_LOOP_START + #endif +#elif defined( __GNUC__ ) + #define STBIR_STREAMOUT_PTR( star ) star __restrict__ + #define STBIR_NO_UNROLL( ptr ) __asm__ (""::"r"(ptr)) + #if __GNUC__ >= 14 + #define STBIR_NO_UNROLL_LOOP_START _Pragma("GCC unroll 0") _Pragma("GCC novector") + #else + #define STBIR_NO_UNROLL_LOOP_START + #endif + #define STBIR_NO_UNROLL_LOOP_START_INF_FOR +#else + #define STBIR_STREAMOUT_PTR( star ) star + #define STBIR_NO_UNROLL( ptr ) + #define STBIR_NO_UNROLL_LOOP_START +#endif + +#ifndef STBIR_NO_UNROLL_LOOP_START_INF_FOR +#define STBIR_NO_UNROLL_LOOP_START_INF_FOR STBIR_NO_UNROLL_LOOP_START +#endif + +#ifdef STBIR_NO_SIMD // force simd off for whatever reason + +// force simd off overrides everything else, so clear it all + +#ifdef STBIR_SSE2 +#undef STBIR_SSE2 +#endif + +#ifdef STBIR_AVX +#undef STBIR_AVX +#endif + +#ifdef STBIR_NEON +#undef STBIR_NEON +#endif + +#ifdef STBIR_AVX2 +#undef STBIR_AVX2 +#endif + +#ifdef STBIR_FP16C +#undef STBIR_FP16C +#endif + +#ifdef STBIR_WASM +#undef STBIR_WASM +#endif + +#ifdef STBIR_SIMD +#undef STBIR_SIMD +#endif + +#else // STBIR_SIMD + +#ifdef STBIR_SSE2 + #include <emmintrin.h> + + #define stbir__simdf __m128 + #define stbir__simdi __m128i + + #define stbir_simdi_castf( reg ) _mm_castps_si128(reg) + #define stbir_simdf_casti( reg ) _mm_castsi128_ps(reg) + + #define stbir__simdf_load( reg, ptr ) (reg) = _mm_loadu_ps( (float const*)(ptr) ) + #define stbir__simdi_load( reg, ptr ) (reg) = _mm_loadu_si128 ( (stbir__simdi const*)(ptr) ) + #define stbir__simdf_load1( out, ptr ) (out) = _mm_load_ss( (float const*)(ptr) ) // top values can be random (not denormal or nan for perf) + #define stbir__simdi_load1( out, ptr ) (out) = _mm_castps_si128( _mm_load_ss( (float const*)(ptr) )) + #define stbir__simdf_load1z( out, ptr ) (out) = _mm_load_ss( (float const*)(ptr) ) // top values must be zero + #define stbir__simdf_frep4( fvar ) _mm_set_ps1( fvar ) + #define stbir__simdf_load1frep4( out, fvar ) (out) = _mm_set_ps1( fvar ) + #define stbir__simdf_load2( out, ptr ) (out) = _mm_castsi128_ps( _mm_loadl_epi64( (__m128i*)(ptr)) ) // top values can be random (not denormal or nan for perf) + #define stbir__simdf_load2z( out, ptr ) (out) = _mm_castsi128_ps( _mm_loadl_epi64( (__m128i*)(ptr)) ) // top values must be zero + #define stbir__simdf_load2hmerge( out, reg, ptr ) (out) = _mm_castpd_ps(_mm_loadh_pd( _mm_castps_pd(reg), (double*)(ptr) )) + + #define stbir__simdf_zeroP() _mm_setzero_ps() + #define stbir__simdf_zero( reg ) (reg) = _mm_setzero_ps() + + #define stbir__simdf_store( ptr, reg ) _mm_storeu_ps( (float*)(ptr), reg ) + #define stbir__simdf_store1( ptr, reg ) _mm_store_ss( (float*)(ptr), reg ) + #define stbir__simdf_store2( ptr, reg ) _mm_storel_epi64( (__m128i*)(ptr), _mm_castps_si128(reg) ) + #define stbir__simdf_store2h( ptr, reg ) _mm_storeh_pd( (double*)(ptr), _mm_castps_pd(reg) ) + + #define stbir__simdi_store( ptr, reg ) _mm_storeu_si128( (__m128i*)(ptr), reg ) + #define stbir__simdi_store1( ptr, reg ) _mm_store_ss( (float*)(ptr), _mm_castsi128_ps(reg) ) + #define stbir__simdi_store2( ptr, reg ) _mm_storel_epi64( (__m128i*)(ptr), (reg) ) + + #define stbir__prefetch( ptr ) _mm_prefetch((char*)(ptr), _MM_HINT_T0 ) + + #define stbir__simdi_expand_u8_to_u32(out0,out1,out2,out3,ireg) \ + { \ + stbir__simdi zero = _mm_setzero_si128(); \ + out2 = _mm_unpacklo_epi8( ireg, zero ); \ + out3 = _mm_unpackhi_epi8( ireg, zero ); \ + out0 = _mm_unpacklo_epi16( out2, zero ); \ + out1 = _mm_unpackhi_epi16( out2, zero ); \ + out2 = _mm_unpacklo_epi16( out3, zero ); \ + out3 = _mm_unpackhi_epi16( out3, zero ); \ + } + +#define stbir__simdi_expand_u8_to_1u32(out,ireg) \ + { \ + stbir__simdi zero = _mm_setzero_si128(); \ + out = _mm_unpacklo_epi8( ireg, zero ); \ + out = _mm_unpacklo_epi16( out, zero ); \ + } + + #define stbir__simdi_expand_u16_to_u32(out0,out1,ireg) \ + { \ + stbir__simdi zero = _mm_setzero_si128(); \ + out0 = _mm_unpacklo_epi16( ireg, zero ); \ + out1 = _mm_unpackhi_epi16( ireg, zero ); \ + } + + #define stbir__simdf_convert_float_to_i32( i, f ) (i) = _mm_cvttps_epi32(f) + #define stbir__simdf_convert_float_to_int( f ) _mm_cvtt_ss2si(f) + #define stbir__simdf_convert_float_to_uint8( f ) ((unsigned char)_mm_cvtsi128_si32(_mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(f,STBIR__CONSTF(STBIR_max_uint8_as_float)),_mm_setzero_ps())))) + #define stbir__simdf_convert_float_to_short( f ) ((unsigned short)_mm_cvtsi128_si32(_mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(f,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps())))) + + #define stbir__simdi_to_int( i ) _mm_cvtsi128_si32(i) + #define stbir__simdi_convert_i32_to_float(out, ireg) (out) = _mm_cvtepi32_ps( ireg ) + #define stbir__simdf_add( out, reg0, reg1 ) (out) = _mm_add_ps( reg0, reg1 ) + #define stbir__simdf_mult( out, reg0, reg1 ) (out) = _mm_mul_ps( reg0, reg1 ) + #define stbir__simdf_mult_mem( out, reg, ptr ) (out) = _mm_mul_ps( reg, _mm_loadu_ps( (float const*)(ptr) ) ) + #define stbir__simdf_mult1_mem( out, reg, ptr ) (out) = _mm_mul_ss( reg, _mm_load_ss( (float const*)(ptr) ) ) + #define stbir__simdf_add_mem( out, reg, ptr ) (out) = _mm_add_ps( reg, _mm_loadu_ps( (float const*)(ptr) ) ) + #define stbir__simdf_add1_mem( out, reg, ptr ) (out) = _mm_add_ss( reg, _mm_load_ss( (float const*)(ptr) ) ) + + #ifdef STBIR_USE_FMA // not on by default to maintain bit identical simd to non-simd + #include <immintrin.h> + #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = _mm_fmadd_ps( mul1, mul2, add ) + #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = _mm_fmadd_ss( mul1, mul2, add ) + #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = _mm_fmadd_ps( mul, _mm_loadu_ps( (float const*)(ptr) ), add ) + #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = _mm_fmadd_ss( mul, _mm_load_ss( (float const*)(ptr) ), add ) + #else + #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = _mm_add_ps( add, _mm_mul_ps( mul1, mul2 ) ) + #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = _mm_add_ss( add, _mm_mul_ss( mul1, mul2 ) ) + #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = _mm_add_ps( add, _mm_mul_ps( mul, _mm_loadu_ps( (float const*)(ptr) ) ) ) + #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = _mm_add_ss( add, _mm_mul_ss( mul, _mm_load_ss( (float const*)(ptr) ) ) ) + #endif + + #define stbir__simdf_add1( out, reg0, reg1 ) (out) = _mm_add_ss( reg0, reg1 ) + #define stbir__simdf_mult1( out, reg0, reg1 ) (out) = _mm_mul_ss( reg0, reg1 ) + + #define stbir__simdf_and( out, reg0, reg1 ) (out) = _mm_and_ps( reg0, reg1 ) + #define stbir__simdf_or( out, reg0, reg1 ) (out) = _mm_or_ps( reg0, reg1 ) + + #define stbir__simdf_min( out, reg0, reg1 ) (out) = _mm_min_ps( reg0, reg1 ) + #define stbir__simdf_max( out, reg0, reg1 ) (out) = _mm_max_ps( reg0, reg1 ) + #define stbir__simdf_min1( out, reg0, reg1 ) (out) = _mm_min_ss( reg0, reg1 ) + #define stbir__simdf_max1( out, reg0, reg1 ) (out) = _mm_max_ss( reg0, reg1 ) + + #define stbir__simdf_0123ABCDto3ABx( out, reg0, reg1 ) (out)=_mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( _mm_shuffle_ps( reg1,reg0, (0<<0) + (1<<2) + (2<<4) + (3<<6) )), (3<<0) + (0<<2) + (1<<4) + (2<<6) ) ) + #define stbir__simdf_0123ABCDto23Ax( out, reg0, reg1 ) (out)=_mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( _mm_shuffle_ps( reg1,reg0, (0<<0) + (1<<2) + (2<<4) + (3<<6) )), (2<<0) + (3<<2) + (0<<4) + (1<<6) ) ) + + static const stbir__simdf STBIR_zeroones = { 0.0f,1.0f,0.0f,1.0f }; + static const stbir__simdf STBIR_onezeros = { 1.0f,0.0f,1.0f,0.0f }; + #define stbir__simdf_aaa1( out, alp, ones ) (out)=_mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( _mm_movehl_ps( ones, alp ) ), (1<<0) + (1<<2) + (1<<4) + (2<<6) ) ) + #define stbir__simdf_1aaa( out, alp, ones ) (out)=_mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( _mm_movelh_ps( ones, alp ) ), (0<<0) + (2<<2) + (2<<4) + (2<<6) ) ) + #define stbir__simdf_a1a1( out, alp, ones) (out) = _mm_or_ps( _mm_castsi128_ps( _mm_srli_epi64( _mm_castps_si128(alp), 32 ) ), STBIR_zeroones ) + #define stbir__simdf_1a1a( out, alp, ones) (out) = _mm_or_ps( _mm_castsi128_ps( _mm_slli_epi64( _mm_castps_si128(alp), 32 ) ), STBIR_onezeros ) + + #define stbir__simdf_swiz( reg, one, two, three, four ) _mm_castsi128_ps( _mm_shuffle_epi32( _mm_castps_si128( reg ), (one<<0) + (two<<2) + (three<<4) + (four<<6) ) ) + + #define stbir__simdi_and( out, reg0, reg1 ) (out) = _mm_and_si128( reg0, reg1 ) + #define stbir__simdi_or( out, reg0, reg1 ) (out) = _mm_or_si128( reg0, reg1 ) + #define stbir__simdi_16madd( out, reg0, reg1 ) (out) = _mm_madd_epi16( reg0, reg1 ) + + #define stbir__simdf_pack_to_8bytes(out,aa,bb) \ + { \ + stbir__simdf af,bf; \ + stbir__simdi a,b; \ + af = _mm_min_ps( aa, STBIR_max_uint8_as_float ); \ + bf = _mm_min_ps( bb, STBIR_max_uint8_as_float ); \ + af = _mm_max_ps( af, _mm_setzero_ps() ); \ + bf = _mm_max_ps( bf, _mm_setzero_ps() ); \ + a = _mm_cvttps_epi32( af ); \ + b = _mm_cvttps_epi32( bf ); \ + a = _mm_packs_epi32( a, b ); \ + out = _mm_packus_epi16( a, a ); \ + } + + #define stbir__simdf_load4_transposed( o0, o1, o2, o3, ptr ) \ + stbir__simdf_load( o0, (ptr) ); \ + stbir__simdf_load( o1, (ptr)+4 ); \ + stbir__simdf_load( o2, (ptr)+8 ); \ + stbir__simdf_load( o3, (ptr)+12 ); \ + { \ + __m128 tmp0, tmp1, tmp2, tmp3; \ + tmp0 = _mm_unpacklo_ps(o0, o1); \ + tmp2 = _mm_unpacklo_ps(o2, o3); \ + tmp1 = _mm_unpackhi_ps(o0, o1); \ + tmp3 = _mm_unpackhi_ps(o2, o3); \ + o0 = _mm_movelh_ps(tmp0, tmp2); \ + o1 = _mm_movehl_ps(tmp2, tmp0); \ + o2 = _mm_movelh_ps(tmp1, tmp3); \ + o3 = _mm_movehl_ps(tmp3, tmp1); \ + } + + #define stbir__interleave_pack_and_store_16_u8( ptr, r0, r1, r2, r3 ) \ + r0 = _mm_packs_epi32( r0, r1 ); \ + r2 = _mm_packs_epi32( r2, r3 ); \ + r1 = _mm_unpacklo_epi16( r0, r2 ); \ + r3 = _mm_unpackhi_epi16( r0, r2 ); \ + r0 = _mm_unpacklo_epi16( r1, r3 ); \ + r2 = _mm_unpackhi_epi16( r1, r3 ); \ + r0 = _mm_packus_epi16( r0, r2 ); \ + stbir__simdi_store( ptr, r0 ); \ + + #define stbir__simdi_32shr( out, reg, imm ) out = _mm_srli_epi32( reg, imm ) + + #if defined(_MSC_VER) && !defined(__clang__) + // msvc inits with 8 bytes + #define STBIR__CONST_32_TO_8( v ) (char)(unsigned char)((v)&255),(char)(unsigned char)(((v)>>8)&255),(char)(unsigned char)(((v)>>16)&255),(char)(unsigned char)(((v)>>24)&255) + #define STBIR__CONST_4_32i( v ) STBIR__CONST_32_TO_8( v ), STBIR__CONST_32_TO_8( v ), STBIR__CONST_32_TO_8( v ), STBIR__CONST_32_TO_8( v ) + #define STBIR__CONST_4d_32i( v0, v1, v2, v3 ) STBIR__CONST_32_TO_8( v0 ), STBIR__CONST_32_TO_8( v1 ), STBIR__CONST_32_TO_8( v2 ), STBIR__CONST_32_TO_8( v3 ) + #else + // everything else inits with long long's + #define STBIR__CONST_4_32i( v ) (long long)((((stbir_uint64)(stbir_uint32)(v))<<32)|((stbir_uint64)(stbir_uint32)(v))),(long long)((((stbir_uint64)(stbir_uint32)(v))<<32)|((stbir_uint64)(stbir_uint32)(v))) + #define STBIR__CONST_4d_32i( v0, v1, v2, v3 ) (long long)((((stbir_uint64)(stbir_uint32)(v1))<<32)|((stbir_uint64)(stbir_uint32)(v0))),(long long)((((stbir_uint64)(stbir_uint32)(v3))<<32)|((stbir_uint64)(stbir_uint32)(v2))) + #endif + + #define STBIR__SIMDF_CONST(var, x) stbir__simdf var = { x, x, x, x } + #define STBIR__SIMDI_CONST(var, x) stbir__simdi var = { STBIR__CONST_4_32i(x) } + #define STBIR__CONSTF(var) (var) + #define STBIR__CONSTI(var) (var) + + #if defined(STBIR_AVX) || defined(__SSE4_1__) + #include <smmintrin.h> + #define stbir__simdf_pack_to_8words(out,reg0,reg1) out = _mm_packus_epi32(_mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(reg0,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps())), _mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(reg1,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps()))) + #else + static STBIR__SIMDI_CONST(stbir__s32_32768, 32768); + static STBIR__SIMDI_CONST(stbir__s16_32768, ((32768<<16)|32768)); + + #define stbir__simdf_pack_to_8words(out,reg0,reg1) \ + { \ + stbir__simdi tmp0,tmp1; \ + tmp0 = _mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(reg0,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps())); \ + tmp1 = _mm_cvttps_epi32(_mm_max_ps(_mm_min_ps(reg1,STBIR__CONSTF(STBIR_max_uint16_as_float)),_mm_setzero_ps())); \ + tmp0 = _mm_sub_epi32( tmp0, stbir__s32_32768 ); \ + tmp1 = _mm_sub_epi32( tmp1, stbir__s32_32768 ); \ + out = _mm_packs_epi32( tmp0, tmp1 ); \ + out = _mm_sub_epi16( out, stbir__s16_32768 ); \ + } + + #endif + + #define STBIR_SIMD + + // if we detect AVX, set the simd8 defines + #ifdef STBIR_AVX + #include <immintrin.h> + #define STBIR_SIMD8 + #define stbir__simdf8 __m256 + #define stbir__simdi8 __m256i + #define stbir__simdf8_load( out, ptr ) (out) = _mm256_loadu_ps( (float const *)(ptr) ) + #define stbir__simdi8_load( out, ptr ) (out) = _mm256_loadu_si256( (__m256i const *)(ptr) ) + #define stbir__simdf8_mult( out, a, b ) (out) = _mm256_mul_ps( (a), (b) ) + #define stbir__simdf8_store( ptr, out ) _mm256_storeu_ps( (float*)(ptr), out ) + #define stbir__simdi8_store( ptr, reg ) _mm256_storeu_si256( (__m256i*)(ptr), reg ) + #define stbir__simdf8_frep8( fval ) _mm256_set1_ps( fval ) + + #define stbir__simdf8_min( out, reg0, reg1 ) (out) = _mm256_min_ps( reg0, reg1 ) + #define stbir__simdf8_max( out, reg0, reg1 ) (out) = _mm256_max_ps( reg0, reg1 ) + + #define stbir__simdf8_add4halves( out, bot4, top8 ) (out) = _mm_add_ps( bot4, _mm256_extractf128_ps( top8, 1 ) ) + #define stbir__simdf8_mult_mem( out, reg, ptr ) (out) = _mm256_mul_ps( reg, _mm256_loadu_ps( (float const*)(ptr) ) ) + #define stbir__simdf8_add_mem( out, reg, ptr ) (out) = _mm256_add_ps( reg, _mm256_loadu_ps( (float const*)(ptr) ) ) + #define stbir__simdf8_add( out, a, b ) (out) = _mm256_add_ps( a, b ) + #define stbir__simdf8_load1b( out, ptr ) (out) = _mm256_broadcast_ss( ptr ) + #define stbir__simdf_load1rep4( out, ptr ) (out) = _mm_broadcast_ss( ptr ) // avx load instruction + + #define stbir__simdi8_convert_i32_to_float(out, ireg) (out) = _mm256_cvtepi32_ps( ireg ) + #define stbir__simdf8_convert_float_to_i32( i, f ) (i) = _mm256_cvttps_epi32(f) + + #define stbir__simdf8_bot4s( out, a, b ) (out) = _mm256_permute2f128_ps(a,b, (0<<0)+(2<<4) ) + #define stbir__simdf8_top4s( out, a, b ) (out) = _mm256_permute2f128_ps(a,b, (1<<0)+(3<<4) ) + + #define stbir__simdf8_gettop4( reg ) _mm256_extractf128_ps(reg,1) + + #ifdef STBIR_AVX2 + + #define stbir__simdi8_expand_u8_to_u32(out0,out1,ireg) \ + { \ + stbir__simdi8 a, zero =_mm256_setzero_si256();\ + a = _mm256_permute4x64_epi64( _mm256_unpacklo_epi8( _mm256_permute4x64_epi64(_mm256_castsi128_si256(ireg),(0<<0)+(2<<2)+(1<<4)+(3<<6)), zero ),(0<<0)+(2<<2)+(1<<4)+(3<<6)); \ + out0 = _mm256_unpacklo_epi16( a, zero ); \ + out1 = _mm256_unpackhi_epi16( a, zero ); \ + } + + #define stbir__simdf8_pack_to_16bytes(out,aa,bb) \ + { \ + stbir__simdi8 t; \ + stbir__simdf8 af,bf; \ + stbir__simdi8 a,b; \ + af = _mm256_min_ps( aa, STBIR_max_uint8_as_floatX ); \ + bf = _mm256_min_ps( bb, STBIR_max_uint8_as_floatX ); \ + af = _mm256_max_ps( af, _mm256_setzero_ps() ); \ + bf = _mm256_max_ps( bf, _mm256_setzero_ps() ); \ + a = _mm256_cvttps_epi32( af ); \ + b = _mm256_cvttps_epi32( bf ); \ + t = _mm256_permute4x64_epi64( _mm256_packs_epi32( a, b ), (0<<0)+(2<<2)+(1<<4)+(3<<6) ); \ + out = _mm256_castsi256_si128( _mm256_permute4x64_epi64( _mm256_packus_epi16( t, t ), (0<<0)+(2<<2)+(1<<4)+(3<<6) ) ); \ + } + + #define stbir__simdi8_expand_u16_to_u32(out,ireg) out = _mm256_unpacklo_epi16( _mm256_permute4x64_epi64(_mm256_castsi128_si256(ireg),(0<<0)+(2<<2)+(1<<4)+(3<<6)), _mm256_setzero_si256() ); + + #define stbir__simdf8_pack_to_16words(out,aa,bb) \ + { \ + stbir__simdf8 af,bf; \ + stbir__simdi8 a,b; \ + af = _mm256_min_ps( aa, STBIR_max_uint16_as_floatX ); \ + bf = _mm256_min_ps( bb, STBIR_max_uint16_as_floatX ); \ + af = _mm256_max_ps( af, _mm256_setzero_ps() ); \ + bf = _mm256_max_ps( bf, _mm256_setzero_ps() ); \ + a = _mm256_cvttps_epi32( af ); \ + b = _mm256_cvttps_epi32( bf ); \ + (out) = _mm256_permute4x64_epi64( _mm256_packus_epi32(a, b), (0<<0)+(2<<2)+(1<<4)+(3<<6) ); \ + } + + #else + + #define stbir__simdi8_expand_u8_to_u32(out0,out1,ireg) \ + { \ + stbir__simdi a,zero = _mm_setzero_si128(); \ + a = _mm_unpacklo_epi8( ireg, zero ); \ + out0 = _mm256_setr_m128i( _mm_unpacklo_epi16( a, zero ), _mm_unpackhi_epi16( a, zero ) ); \ + a = _mm_unpackhi_epi8( ireg, zero ); \ + out1 = _mm256_setr_m128i( _mm_unpacklo_epi16( a, zero ), _mm_unpackhi_epi16( a, zero ) ); \ + } + + #define stbir__simdf8_pack_to_16bytes(out,aa,bb) \ + { \ + stbir__simdi t; \ + stbir__simdf8 af,bf; \ + stbir__simdi8 a,b; \ + af = _mm256_min_ps( aa, STBIR_max_uint8_as_floatX ); \ + bf = _mm256_min_ps( bb, STBIR_max_uint8_as_floatX ); \ + af = _mm256_max_ps( af, _mm256_setzero_ps() ); \ + bf = _mm256_max_ps( bf, _mm256_setzero_ps() ); \ + a = _mm256_cvttps_epi32( af ); \ + b = _mm256_cvttps_epi32( bf ); \ + out = _mm_packs_epi32( _mm256_castsi256_si128(a), _mm256_extractf128_si256( a, 1 ) ); \ + out = _mm_packus_epi16( out, out ); \ + t = _mm_packs_epi32( _mm256_castsi256_si128(b), _mm256_extractf128_si256( b, 1 ) ); \ + t = _mm_packus_epi16( t, t ); \ + out = _mm_castps_si128( _mm_shuffle_ps( _mm_castsi128_ps(out), _mm_castsi128_ps(t), (0<<0)+(1<<2)+(0<<4)+(1<<6) ) ); \ + } + + #define stbir__simdi8_expand_u16_to_u32(out,ireg) \ + { \ + stbir__simdi a,b,zero = _mm_setzero_si128(); \ + a = _mm_unpacklo_epi16( ireg, zero ); \ + b = _mm_unpackhi_epi16( ireg, zero ); \ + out = _mm256_insertf128_si256( _mm256_castsi128_si256( a ), b, 1 ); \ + } + + #define stbir__simdf8_pack_to_16words(out,aa,bb) \ + { \ + stbir__simdi t0,t1; \ + stbir__simdf8 af,bf; \ + stbir__simdi8 a,b; \ + af = _mm256_min_ps( aa, STBIR_max_uint16_as_floatX ); \ + bf = _mm256_min_ps( bb, STBIR_max_uint16_as_floatX ); \ + af = _mm256_max_ps( af, _mm256_setzero_ps() ); \ + bf = _mm256_max_ps( bf, _mm256_setzero_ps() ); \ + a = _mm256_cvttps_epi32( af ); \ + b = _mm256_cvttps_epi32( bf ); \ + t0 = _mm_packus_epi32( _mm256_castsi256_si128(a), _mm256_extractf128_si256( a, 1 ) ); \ + t1 = _mm_packus_epi32( _mm256_castsi256_si128(b), _mm256_extractf128_si256( b, 1 ) ); \ + out = _mm256_setr_m128i( t0, t1 ); \ + } + + #endif + + static __m256i stbir_00001111 = { STBIR__CONST_4d_32i( 0, 0, 0, 0 ), STBIR__CONST_4d_32i( 1, 1, 1, 1 ) }; + #define stbir__simdf8_0123to00001111( out, in ) (out) = _mm256_permutevar_ps ( in, stbir_00001111 ) + + static __m256i stbir_22223333 = { STBIR__CONST_4d_32i( 2, 2, 2, 2 ), STBIR__CONST_4d_32i( 3, 3, 3, 3 ) }; + #define stbir__simdf8_0123to22223333( out, in ) (out) = _mm256_permutevar_ps ( in, stbir_22223333 ) + + #define stbir__simdf8_0123to2222( out, in ) (out) = stbir__simdf_swiz(_mm256_castps256_ps128(in), 2,2,2,2 ) + + #define stbir__simdf8_load4b( out, ptr ) (out) = _mm256_broadcast_ps( (__m128 const *)(ptr) ) + + static __m256i stbir_00112233 = { STBIR__CONST_4d_32i( 0, 0, 1, 1 ), STBIR__CONST_4d_32i( 2, 2, 3, 3 ) }; + #define stbir__simdf8_0123to00112233( out, in ) (out) = _mm256_permutevar_ps ( in, stbir_00112233 ) + #define stbir__simdf8_add4( out, a8, b ) (out) = _mm256_add_ps( a8, _mm256_castps128_ps256( b ) ) + + static __m256i stbir_load6 = { STBIR__CONST_4_32i( 0x80000000 ), STBIR__CONST_4d_32i( 0x80000000, 0x80000000, 0, 0 ) }; + #define stbir__simdf8_load6z( out, ptr ) (out) = _mm256_maskload_ps( ptr, stbir_load6 ) + + #define stbir__simdf8_0123to00000000( out, in ) (out) = _mm256_shuffle_ps ( in, in, (0<<0)+(0<<2)+(0<<4)+(0<<6) ) + #define stbir__simdf8_0123to11111111( out, in ) (out) = _mm256_shuffle_ps ( in, in, (1<<0)+(1<<2)+(1<<4)+(1<<6) ) + #define stbir__simdf8_0123to22222222( out, in ) (out) = _mm256_shuffle_ps ( in, in, (2<<0)+(2<<2)+(2<<4)+(2<<6) ) + #define stbir__simdf8_0123to33333333( out, in ) (out) = _mm256_shuffle_ps ( in, in, (3<<0)+(3<<2)+(3<<4)+(3<<6) ) + #define stbir__simdf8_0123to21032103( out, in ) (out) = _mm256_shuffle_ps ( in, in, (2<<0)+(1<<2)+(0<<4)+(3<<6) ) + #define stbir__simdf8_0123to32103210( out, in ) (out) = _mm256_shuffle_ps ( in, in, (3<<0)+(2<<2)+(1<<4)+(0<<6) ) + #define stbir__simdf8_0123to12301230( out, in ) (out) = _mm256_shuffle_ps ( in, in, (1<<0)+(2<<2)+(3<<4)+(0<<6) ) + #define stbir__simdf8_0123to10321032( out, in ) (out) = _mm256_shuffle_ps ( in, in, (1<<0)+(0<<2)+(3<<4)+(2<<6) ) + #define stbir__simdf8_0123to30123012( out, in ) (out) = _mm256_shuffle_ps ( in, in, (3<<0)+(0<<2)+(1<<4)+(2<<6) ) + + #define stbir__simdf8_0123to11331133( out, in ) (out) = _mm256_shuffle_ps ( in, in, (1<<0)+(1<<2)+(3<<4)+(3<<6) ) + #define stbir__simdf8_0123to00220022( out, in ) (out) = _mm256_shuffle_ps ( in, in, (0<<0)+(0<<2)+(2<<4)+(2<<6) ) + + #define stbir__simdf8_aaa1( out, alp, ones ) (out) = _mm256_blend_ps( alp, ones, (1<<0)+(1<<1)+(1<<2)+(0<<3)+(1<<4)+(1<<5)+(1<<6)+(0<<7)); (out)=_mm256_shuffle_ps( out,out, (3<<0) + (3<<2) + (3<<4) + (0<<6) ) + #define stbir__simdf8_1aaa( out, alp, ones ) (out) = _mm256_blend_ps( alp, ones, (0<<0)+(1<<1)+(1<<2)+(1<<3)+(0<<4)+(1<<5)+(1<<6)+(1<<7)); (out)=_mm256_shuffle_ps( out,out, (1<<0) + (0<<2) + (0<<4) + (0<<6) ) + #define stbir__simdf8_a1a1( out, alp, ones) (out) = _mm256_blend_ps( alp, ones, (1<<0)+(0<<1)+(1<<2)+(0<<3)+(1<<4)+(0<<5)+(1<<6)+(0<<7)); (out)=_mm256_shuffle_ps( out,out, (1<<0) + (0<<2) + (3<<4) + (2<<6) ) + #define stbir__simdf8_1a1a( out, alp, ones) (out) = _mm256_blend_ps( alp, ones, (0<<0)+(1<<1)+(0<<2)+(1<<3)+(0<<4)+(1<<5)+(0<<6)+(1<<7)); (out)=_mm256_shuffle_ps( out,out, (1<<0) + (0<<2) + (3<<4) + (2<<6) ) + + #define stbir__simdf8_zero( reg ) (reg) = _mm256_setzero_ps() + + #ifdef STBIR_USE_FMA // not on by default to maintain bit identical simd to non-simd + #define stbir__simdf8_madd( out, add, mul1, mul2 ) (out) = _mm256_fmadd_ps( mul1, mul2, add ) + #define stbir__simdf8_madd_mem( out, add, mul, ptr ) (out) = _mm256_fmadd_ps( mul, _mm256_loadu_ps( (float const*)(ptr) ), add ) + #define stbir__simdf8_madd_mem4( out, add, mul, ptr )(out) = _mm256_fmadd_ps( _mm256_setr_m128( mul, _mm_setzero_ps() ), _mm256_setr_m128( _mm_loadu_ps( (float const*)(ptr) ), _mm_setzero_ps() ), add ) + #else + #define stbir__simdf8_madd( out, add, mul1, mul2 ) (out) = _mm256_add_ps( add, _mm256_mul_ps( mul1, mul2 ) ) + #define stbir__simdf8_madd_mem( out, add, mul, ptr ) (out) = _mm256_add_ps( add, _mm256_mul_ps( mul, _mm256_loadu_ps( (float const*)(ptr) ) ) ) + #define stbir__simdf8_madd_mem4( out, add, mul, ptr ) (out) = _mm256_add_ps( add, _mm256_setr_m128( _mm_mul_ps( mul, _mm_loadu_ps( (float const*)(ptr) ) ), _mm_setzero_ps() ) ) + #endif + #define stbir__if_simdf8_cast_to_simdf4( val ) _mm256_castps256_ps128( val ) + + #endif + + #ifdef STBIR_FLOORF + #undef STBIR_FLOORF + #endif + #define STBIR_FLOORF stbir_simd_floorf + static stbir__inline float stbir_simd_floorf(float x) // martins floorf + { + #if defined(STBIR_AVX) || defined(__SSE4_1__) || defined(STBIR_SSE41) + __m128 t = _mm_set_ss(x); + return _mm_cvtss_f32( _mm_floor_ss(t, t) ); + #else + __m128 f = _mm_set_ss(x); + __m128 t = _mm_cvtepi32_ps(_mm_cvttps_epi32(f)); + __m128 r = _mm_add_ss(t, _mm_and_ps(_mm_cmplt_ss(f, t), _mm_set_ss(-1.0f))); + return _mm_cvtss_f32(r); + #endif + } + + #ifdef STBIR_CEILF + #undef STBIR_CEILF + #endif + #define STBIR_CEILF stbir_simd_ceilf + static stbir__inline float stbir_simd_ceilf(float x) // martins ceilf + { + #if defined(STBIR_AVX) || defined(__SSE4_1__) || defined(STBIR_SSE41) + __m128 t = _mm_set_ss(x); + return _mm_cvtss_f32( _mm_ceil_ss(t, t) ); + #else + __m128 f = _mm_set_ss(x); + __m128 t = _mm_cvtepi32_ps(_mm_cvttps_epi32(f)); + __m128 r = _mm_add_ss(t, _mm_and_ps(_mm_cmplt_ss(t, f), _mm_set_ss(1.0f))); + return _mm_cvtss_f32(r); + #endif + } + +#elif defined(STBIR_NEON) + + #include <arm_neon.h> + + #define stbir__simdf float32x4_t + #define stbir__simdi uint32x4_t + + #define stbir_simdi_castf( reg ) vreinterpretq_u32_f32(reg) + #define stbir_simdf_casti( reg ) vreinterpretq_f32_u32(reg) + + #define stbir__simdf_load( reg, ptr ) (reg) = vld1q_f32( (float const*)(ptr) ) + #define stbir__simdi_load( reg, ptr ) (reg) = vld1q_u32( (uint32_t const*)(ptr) ) + #define stbir__simdf_load1( out, ptr ) (out) = vld1q_dup_f32( (float const*)(ptr) ) // top values can be random (not denormal or nan for perf) + #define stbir__simdi_load1( out, ptr ) (out) = vld1q_dup_u32( (uint32_t const*)(ptr) ) + #define stbir__simdf_load1z( out, ptr ) (out) = vld1q_lane_f32( (float const*)(ptr), vdupq_n_f32(0), 0 ) // top values must be zero + #define stbir__simdf_frep4( fvar ) vdupq_n_f32( fvar ) + #define stbir__simdf_load1frep4( out, fvar ) (out) = vdupq_n_f32( fvar ) + #define stbir__simdf_load2( out, ptr ) (out) = vcombine_f32( vld1_f32( (float const*)(ptr) ), vcreate_f32(0) ) // top values can be random (not denormal or nan for perf) + #define stbir__simdf_load2z( out, ptr ) (out) = vcombine_f32( vld1_f32( (float const*)(ptr) ), vcreate_f32(0) ) // top values must be zero + #define stbir__simdf_load2hmerge( out, reg, ptr ) (out) = vcombine_f32( vget_low_f32(reg), vld1_f32( (float const*)(ptr) ) ) + + #define stbir__simdf_zeroP() vdupq_n_f32(0) + #define stbir__simdf_zero( reg ) (reg) = vdupq_n_f32(0) + + #define stbir__simdf_store( ptr, reg ) vst1q_f32( (float*)(ptr), reg ) + #define stbir__simdf_store1( ptr, reg ) vst1q_lane_f32( (float*)(ptr), reg, 0) + #define stbir__simdf_store2( ptr, reg ) vst1_f32( (float*)(ptr), vget_low_f32(reg) ) + #define stbir__simdf_store2h( ptr, reg ) vst1_f32( (float*)(ptr), vget_high_f32(reg) ) + + #define stbir__simdi_store( ptr, reg ) vst1q_u32( (uint32_t*)(ptr), reg ) + #define stbir__simdi_store1( ptr, reg ) vst1q_lane_u32( (uint32_t*)(ptr), reg, 0 ) + #define stbir__simdi_store2( ptr, reg ) vst1_u32( (uint32_t*)(ptr), vget_low_u32(reg) ) + + #define stbir__prefetch( ptr ) + + #define stbir__simdi_expand_u8_to_u32(out0,out1,out2,out3,ireg) \ + { \ + uint16x8_t l = vmovl_u8( vget_low_u8 ( vreinterpretq_u8_u32(ireg) ) ); \ + uint16x8_t h = vmovl_u8( vget_high_u8( vreinterpretq_u8_u32(ireg) ) ); \ + out0 = vmovl_u16( vget_low_u16 ( l ) ); \ + out1 = vmovl_u16( vget_high_u16( l ) ); \ + out2 = vmovl_u16( vget_low_u16 ( h ) ); \ + out3 = vmovl_u16( vget_high_u16( h ) ); \ + } + + #define stbir__simdi_expand_u8_to_1u32(out,ireg) \ + { \ + uint16x8_t tmp = vmovl_u8( vget_low_u8( vreinterpretq_u8_u32(ireg) ) ); \ + out = vmovl_u16( vget_low_u16( tmp ) ); \ + } + + #define stbir__simdi_expand_u16_to_u32(out0,out1,ireg) \ + { \ + uint16x8_t tmp = vreinterpretq_u16_u32(ireg); \ + out0 = vmovl_u16( vget_low_u16 ( tmp ) ); \ + out1 = vmovl_u16( vget_high_u16( tmp ) ); \ + } + + #define stbir__simdf_convert_float_to_i32( i, f ) (i) = vreinterpretq_u32_s32( vcvtq_s32_f32(f) ) + #define stbir__simdf_convert_float_to_int( f ) vgetq_lane_s32(vcvtq_s32_f32(f), 0) + #define stbir__simdi_to_int( i ) (int)vgetq_lane_u32(i, 0) + #define stbir__simdf_convert_float_to_uint8( f ) ((unsigned char)vgetq_lane_s32(vcvtq_s32_f32(vmaxq_f32(vminq_f32(f,STBIR__CONSTF(STBIR_max_uint8_as_float)),vdupq_n_f32(0))), 0)) + #define stbir__simdf_convert_float_to_short( f ) ((unsigned short)vgetq_lane_s32(vcvtq_s32_f32(vmaxq_f32(vminq_f32(f,STBIR__CONSTF(STBIR_max_uint16_as_float)),vdupq_n_f32(0))), 0)) + #define stbir__simdi_convert_i32_to_float(out, ireg) (out) = vcvtq_f32_s32( vreinterpretq_s32_u32(ireg) ) + #define stbir__simdf_add( out, reg0, reg1 ) (out) = vaddq_f32( reg0, reg1 ) + #define stbir__simdf_mult( out, reg0, reg1 ) (out) = vmulq_f32( reg0, reg1 ) + #define stbir__simdf_mult_mem( out, reg, ptr ) (out) = vmulq_f32( reg, vld1q_f32( (float const*)(ptr) ) ) + #define stbir__simdf_mult1_mem( out, reg, ptr ) (out) = vmulq_f32( reg, vld1q_dup_f32( (float const*)(ptr) ) ) + #define stbir__simdf_add_mem( out, reg, ptr ) (out) = vaddq_f32( reg, vld1q_f32( (float const*)(ptr) ) ) + #define stbir__simdf_add1_mem( out, reg, ptr ) (out) = vaddq_f32( reg, vld1q_dup_f32( (float const*)(ptr) ) ) + + #ifdef STBIR_USE_FMA // not on by default to maintain bit identical simd to non-simd (and also x64 no madd to arm madd) + #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = vfmaq_f32( add, mul1, mul2 ) + #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = vfmaq_f32( add, mul1, mul2 ) + #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = vfmaq_f32( add, mul, vld1q_f32( (float const*)(ptr) ) ) + #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = vfmaq_f32( add, mul, vld1q_dup_f32( (float const*)(ptr) ) ) + #else + #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = vaddq_f32( add, vmulq_f32( mul1, mul2 ) ) + #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = vaddq_f32( add, vmulq_f32( mul1, mul2 ) ) + #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = vaddq_f32( add, vmulq_f32( mul, vld1q_f32( (float const*)(ptr) ) ) ) + #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = vaddq_f32( add, vmulq_f32( mul, vld1q_dup_f32( (float const*)(ptr) ) ) ) + #endif + + #define stbir__simdf_add1( out, reg0, reg1 ) (out) = vaddq_f32( reg0, reg1 ) + #define stbir__simdf_mult1( out, reg0, reg1 ) (out) = vmulq_f32( reg0, reg1 ) + + #define stbir__simdf_and( out, reg0, reg1 ) (out) = vreinterpretq_f32_u32( vandq_u32( vreinterpretq_u32_f32(reg0), vreinterpretq_u32_f32(reg1) ) ) + #define stbir__simdf_or( out, reg0, reg1 ) (out) = vreinterpretq_f32_u32( vorrq_u32( vreinterpretq_u32_f32(reg0), vreinterpretq_u32_f32(reg1) ) ) + + #define stbir__simdf_min( out, reg0, reg1 ) (out) = vminq_f32( reg0, reg1 ) + #define stbir__simdf_max( out, reg0, reg1 ) (out) = vmaxq_f32( reg0, reg1 ) + #define stbir__simdf_min1( out, reg0, reg1 ) (out) = vminq_f32( reg0, reg1 ) + #define stbir__simdf_max1( out, reg0, reg1 ) (out) = vmaxq_f32( reg0, reg1 ) + + #define stbir__simdf_0123ABCDto3ABx( out, reg0, reg1 ) (out) = vextq_f32( reg0, reg1, 3 ) + #define stbir__simdf_0123ABCDto23Ax( out, reg0, reg1 ) (out) = vextq_f32( reg0, reg1, 2 ) + + #define stbir__simdf_a1a1( out, alp, ones ) (out) = vzipq_f32(vuzpq_f32(alp, alp).val[1], ones).val[0] + #define stbir__simdf_1a1a( out, alp, ones ) (out) = vzipq_f32(ones, vuzpq_f32(alp, alp).val[0]).val[0] + + #if defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) + + #define stbir__simdf_aaa1( out, alp, ones ) (out) = vcopyq_laneq_f32(vdupq_n_f32(vgetq_lane_f32(alp, 3)), 3, ones, 3) + #define stbir__simdf_1aaa( out, alp, ones ) (out) = vcopyq_laneq_f32(vdupq_n_f32(vgetq_lane_f32(alp, 0)), 0, ones, 0) + + #if defined( _MSC_VER ) && !defined(__clang__) + #define stbir_make16(a,b,c,d) vcombine_u8( \ + vcreate_u8( (4*a+0) | ((4*a+1)<<8) | ((4*a+2)<<16) | ((4*a+3)<<24) | \ + ((stbir_uint64)(4*b+0)<<32) | ((stbir_uint64)(4*b+1)<<40) | ((stbir_uint64)(4*b+2)<<48) | ((stbir_uint64)(4*b+3)<<56)), \ + vcreate_u8( (4*c+0) | ((4*c+1)<<8) | ((4*c+2)<<16) | ((4*c+3)<<24) | \ + ((stbir_uint64)(4*d+0)<<32) | ((stbir_uint64)(4*d+1)<<40) | ((stbir_uint64)(4*d+2)<<48) | ((stbir_uint64)(4*d+3)<<56) ) ) + + static stbir__inline uint8x16x2_t stbir_make16x2(float32x4_t rega,float32x4_t regb) + { + uint8x16x2_t r = { vreinterpretq_u8_f32(rega), vreinterpretq_u8_f32(regb) }; + return r; + } + #else + #define stbir_make16(a,b,c,d) (uint8x16_t){4*a+0,4*a+1,4*a+2,4*a+3,4*b+0,4*b+1,4*b+2,4*b+3,4*c+0,4*c+1,4*c+2,4*c+3,4*d+0,4*d+1,4*d+2,4*d+3} + #define stbir_make16x2(a,b) (uint8x16x2_t){{vreinterpretq_u8_f32(a),vreinterpretq_u8_f32(b)}} + #endif + + #define stbir__simdf_swiz( reg, one, two, three, four ) vreinterpretq_f32_u8( vqtbl1q_u8( vreinterpretq_u8_f32(reg), stbir_make16(one, two, three, four) ) ) + #define stbir__simdf_swiz2( rega, regb, one, two, three, four ) vreinterpretq_f32_u8( vqtbl2q_u8( stbir_make16x2(rega,regb), stbir_make16(one, two, three, four) ) ) + + #define stbir__simdi_16madd( out, reg0, reg1 ) \ + { \ + int16x8_t r0 = vreinterpretq_s16_u32(reg0); \ + int16x8_t r1 = vreinterpretq_s16_u32(reg1); \ + int32x4_t tmp0 = vmull_s16( vget_low_s16(r0), vget_low_s16(r1) ); \ + int32x4_t tmp1 = vmull_s16( vget_high_s16(r0), vget_high_s16(r1) ); \ + (out) = vreinterpretq_u32_s32( vpaddq_s32(tmp0, tmp1) ); \ + } + + #else + + #define stbir__simdf_aaa1( out, alp, ones ) (out) = vsetq_lane_f32(1.0f, vdupq_n_f32(vgetq_lane_f32(alp, 3)), 3) + #define stbir__simdf_1aaa( out, alp, ones ) (out) = vsetq_lane_f32(1.0f, vdupq_n_f32(vgetq_lane_f32(alp, 0)), 0) + + #if defined( _MSC_VER ) && !defined(__clang__) + static stbir__inline uint8x8x2_t stbir_make8x2(float32x4_t reg) + { + uint8x8x2_t r = { { vget_low_u8(vreinterpretq_u8_f32(reg)), vget_high_u8(vreinterpretq_u8_f32(reg)) } }; + return r; + } + #define stbir_make8(a,b) vcreate_u8( \ + (4*a+0) | ((4*a+1)<<8) | ((4*a+2)<<16) | ((4*a+3)<<24) | \ + ((stbir_uint64)(4*b+0)<<32) | ((stbir_uint64)(4*b+1)<<40) | ((stbir_uint64)(4*b+2)<<48) | ((stbir_uint64)(4*b+3)<<56) ) + #else + #define stbir_make8x2(reg) (uint8x8x2_t){ { vget_low_u8(vreinterpretq_u8_f32(reg)), vget_high_u8(vreinterpretq_u8_f32(reg)) } } + #define stbir_make8(a,b) (uint8x8_t){4*a+0,4*a+1,4*a+2,4*a+3,4*b+0,4*b+1,4*b+2,4*b+3} + #endif + + #define stbir__simdf_swiz( reg, one, two, three, four ) vreinterpretq_f32_u8( vcombine_u8( \ + vtbl2_u8( stbir_make8x2( reg ), stbir_make8( one, two ) ), \ + vtbl2_u8( stbir_make8x2( reg ), stbir_make8( three, four ) ) ) ) + + #define stbir__simdi_16madd( out, reg0, reg1 ) \ + { \ + int16x8_t r0 = vreinterpretq_s16_u32(reg0); \ + int16x8_t r1 = vreinterpretq_s16_u32(reg1); \ + int32x4_t tmp0 = vmull_s16( vget_low_s16(r0), vget_low_s16(r1) ); \ + int32x4_t tmp1 = vmull_s16( vget_high_s16(r0), vget_high_s16(r1) ); \ + int32x2_t out0 = vpadd_s32( vget_low_s32(tmp0), vget_high_s32(tmp0) ); \ + int32x2_t out1 = vpadd_s32( vget_low_s32(tmp1), vget_high_s32(tmp1) ); \ + (out) = vreinterpretq_u32_s32( vcombine_s32(out0, out1) ); \ + } + + #endif + + #define stbir__simdi_and( out, reg0, reg1 ) (out) = vandq_u32( reg0, reg1 ) + #define stbir__simdi_or( out, reg0, reg1 ) (out) = vorrq_u32( reg0, reg1 ) + + #define stbir__simdf_pack_to_8bytes(out,aa,bb) \ + { \ + float32x4_t af = vmaxq_f32( vminq_f32(aa,STBIR__CONSTF(STBIR_max_uint8_as_float) ), vdupq_n_f32(0) ); \ + float32x4_t bf = vmaxq_f32( vminq_f32(bb,STBIR__CONSTF(STBIR_max_uint8_as_float) ), vdupq_n_f32(0) ); \ + int16x4_t ai = vqmovn_s32( vcvtq_s32_f32( af ) ); \ + int16x4_t bi = vqmovn_s32( vcvtq_s32_f32( bf ) ); \ + uint8x8_t out8 = vqmovun_s16( vcombine_s16(ai, bi) ); \ + out = vreinterpretq_u32_u8( vcombine_u8(out8, out8) ); \ + } + + #define stbir__simdf_pack_to_8words(out,aa,bb) \ + { \ + float32x4_t af = vmaxq_f32( vminq_f32(aa,STBIR__CONSTF(STBIR_max_uint16_as_float) ), vdupq_n_f32(0) ); \ + float32x4_t bf = vmaxq_f32( vminq_f32(bb,STBIR__CONSTF(STBIR_max_uint16_as_float) ), vdupq_n_f32(0) ); \ + int32x4_t ai = vcvtq_s32_f32( af ); \ + int32x4_t bi = vcvtq_s32_f32( bf ); \ + out = vreinterpretq_u32_u16( vcombine_u16(vqmovun_s32(ai), vqmovun_s32(bi)) ); \ + } + + #define stbir__interleave_pack_and_store_16_u8( ptr, r0, r1, r2, r3 ) \ + { \ + int16x4x2_t tmp0 = vzip_s16( vqmovn_s32(vreinterpretq_s32_u32(r0)), vqmovn_s32(vreinterpretq_s32_u32(r2)) ); \ + int16x4x2_t tmp1 = vzip_s16( vqmovn_s32(vreinterpretq_s32_u32(r1)), vqmovn_s32(vreinterpretq_s32_u32(r3)) ); \ + uint8x8x2_t out = \ + { { \ + vqmovun_s16( vcombine_s16(tmp0.val[0], tmp0.val[1]) ), \ + vqmovun_s16( vcombine_s16(tmp1.val[0], tmp1.val[1]) ), \ + } }; \ + vst2_u8(ptr, out); \ + } + + #define stbir__simdf_load4_transposed( o0, o1, o2, o3, ptr ) \ + { \ + float32x4x4_t tmp = vld4q_f32(ptr); \ + o0 = tmp.val[0]; \ + o1 = tmp.val[1]; \ + o2 = tmp.val[2]; \ + o3 = tmp.val[3]; \ + } + + #define stbir__simdi_32shr( out, reg, imm ) out = vshrq_n_u32( reg, imm ) + + #if defined( _MSC_VER ) && !defined(__clang__) + #define STBIR__SIMDF_CONST(var, x) __declspec(align(8)) float var[] = { x, x, x, x } + #define STBIR__SIMDI_CONST(var, x) __declspec(align(8)) uint32_t var[] = { x, x, x, x } + #define STBIR__CONSTF(var) (*(const float32x4_t*)var) + #define STBIR__CONSTI(var) (*(const uint32x4_t*)var) + #else + #define STBIR__SIMDF_CONST(var, x) stbir__simdf var = { x, x, x, x } + #define STBIR__SIMDI_CONST(var, x) stbir__simdi var = { x, x, x, x } + #define STBIR__CONSTF(var) (var) + #define STBIR__CONSTI(var) (var) + #endif + + #ifdef STBIR_FLOORF + #undef STBIR_FLOORF + #endif + #define STBIR_FLOORF stbir_simd_floorf + static stbir__inline float stbir_simd_floorf(float x) + { + #if defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) + return vget_lane_f32( vrndm_f32( vdup_n_f32(x) ), 0); + #else + float32x2_t f = vdup_n_f32(x); + float32x2_t t = vcvt_f32_s32(vcvt_s32_f32(f)); + uint32x2_t a = vclt_f32(f, t); + uint32x2_t b = vreinterpret_u32_f32(vdup_n_f32(-1.0f)); + float32x2_t r = vadd_f32(t, vreinterpret_f32_u32(vand_u32(a, b))); + return vget_lane_f32(r, 0); + #endif + } + + #ifdef STBIR_CEILF + #undef STBIR_CEILF + #endif + #define STBIR_CEILF stbir_simd_ceilf + static stbir__inline float stbir_simd_ceilf(float x) + { + #if defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) + return vget_lane_f32( vrndp_f32( vdup_n_f32(x) ), 0); + #else + float32x2_t f = vdup_n_f32(x); + float32x2_t t = vcvt_f32_s32(vcvt_s32_f32(f)); + uint32x2_t a = vclt_f32(t, f); + uint32x2_t b = vreinterpret_u32_f32(vdup_n_f32(1.0f)); + float32x2_t r = vadd_f32(t, vreinterpret_f32_u32(vand_u32(a, b))); + return vget_lane_f32(r, 0); + #endif + } + + #define STBIR_SIMD + +#elif defined(STBIR_WASM) + + #include <wasm_simd128.h> + + #define stbir__simdf v128_t + #define stbir__simdi v128_t + + #define stbir_simdi_castf( reg ) (reg) + #define stbir_simdf_casti( reg ) (reg) + + #define stbir__simdf_load( reg, ptr ) (reg) = wasm_v128_load( (void const*)(ptr) ) + #define stbir__simdi_load( reg, ptr ) (reg) = wasm_v128_load( (void const*)(ptr) ) + #define stbir__simdf_load1( out, ptr ) (out) = wasm_v128_load32_splat( (void const*)(ptr) ) // top values can be random (not denormal or nan for perf) + #define stbir__simdi_load1( out, ptr ) (out) = wasm_v128_load32_splat( (void const*)(ptr) ) + #define stbir__simdf_load1z( out, ptr ) (out) = wasm_v128_load32_zero( (void const*)(ptr) ) // top values must be zero + #define stbir__simdf_frep4( fvar ) wasm_f32x4_splat( fvar ) + #define stbir__simdf_load1frep4( out, fvar ) (out) = wasm_f32x4_splat( fvar ) + #define stbir__simdf_load2( out, ptr ) (out) = wasm_v128_load64_splat( (void const*)(ptr) ) // top values can be random (not denormal or nan for perf) + #define stbir__simdf_load2z( out, ptr ) (out) = wasm_v128_load64_zero( (void const*)(ptr) ) // top values must be zero + #define stbir__simdf_load2hmerge( out, reg, ptr ) (out) = wasm_v128_load64_lane( (void const*)(ptr), reg, 1 ) + + #define stbir__simdf_zeroP() wasm_f32x4_const_splat(0) + #define stbir__simdf_zero( reg ) (reg) = wasm_f32x4_const_splat(0) + + #define stbir__simdf_store( ptr, reg ) wasm_v128_store( (void*)(ptr), reg ) + #define stbir__simdf_store1( ptr, reg ) wasm_v128_store32_lane( (void*)(ptr), reg, 0 ) + #define stbir__simdf_store2( ptr, reg ) wasm_v128_store64_lane( (void*)(ptr), reg, 0 ) + #define stbir__simdf_store2h( ptr, reg ) wasm_v128_store64_lane( (void*)(ptr), reg, 1 ) + + #define stbir__simdi_store( ptr, reg ) wasm_v128_store( (void*)(ptr), reg ) + #define stbir__simdi_store1( ptr, reg ) wasm_v128_store32_lane( (void*)(ptr), reg, 0 ) + #define stbir__simdi_store2( ptr, reg ) wasm_v128_store64_lane( (void*)(ptr), reg, 0 ) + + #define stbir__prefetch( ptr ) + + #define stbir__simdi_expand_u8_to_u32(out0,out1,out2,out3,ireg) \ + { \ + v128_t l = wasm_u16x8_extend_low_u8x16 ( ireg ); \ + v128_t h = wasm_u16x8_extend_high_u8x16( ireg ); \ + out0 = wasm_u32x4_extend_low_u16x8 ( l ); \ + out1 = wasm_u32x4_extend_high_u16x8( l ); \ + out2 = wasm_u32x4_extend_low_u16x8 ( h ); \ + out3 = wasm_u32x4_extend_high_u16x8( h ); \ + } + + #define stbir__simdi_expand_u8_to_1u32(out,ireg) \ + { \ + v128_t tmp = wasm_u16x8_extend_low_u8x16(ireg); \ + out = wasm_u32x4_extend_low_u16x8(tmp); \ + } + + #define stbir__simdi_expand_u16_to_u32(out0,out1,ireg) \ + { \ + out0 = wasm_u32x4_extend_low_u16x8 ( ireg ); \ + out1 = wasm_u32x4_extend_high_u16x8( ireg ); \ + } + + #define stbir__simdf_convert_float_to_i32( i, f ) (i) = wasm_i32x4_trunc_sat_f32x4(f) + #define stbir__simdf_convert_float_to_int( f ) wasm_i32x4_extract_lane(wasm_i32x4_trunc_sat_f32x4(f), 0) + #define stbir__simdi_to_int( i ) wasm_i32x4_extract_lane(i, 0) + #define stbir__simdf_convert_float_to_uint8( f ) ((unsigned char)wasm_i32x4_extract_lane(wasm_i32x4_trunc_sat_f32x4(wasm_f32x4_max(wasm_f32x4_min(f,STBIR_max_uint8_as_float),wasm_f32x4_const_splat(0))), 0)) + #define stbir__simdf_convert_float_to_short( f ) ((unsigned short)wasm_i32x4_extract_lane(wasm_i32x4_trunc_sat_f32x4(wasm_f32x4_max(wasm_f32x4_min(f,STBIR_max_uint16_as_float),wasm_f32x4_const_splat(0))), 0)) + #define stbir__simdi_convert_i32_to_float(out, ireg) (out) = wasm_f32x4_convert_i32x4(ireg) + #define stbir__simdf_add( out, reg0, reg1 ) (out) = wasm_f32x4_add( reg0, reg1 ) + #define stbir__simdf_mult( out, reg0, reg1 ) (out) = wasm_f32x4_mul( reg0, reg1 ) + #define stbir__simdf_mult_mem( out, reg, ptr ) (out) = wasm_f32x4_mul( reg, wasm_v128_load( (void const*)(ptr) ) ) + #define stbir__simdf_mult1_mem( out, reg, ptr ) (out) = wasm_f32x4_mul( reg, wasm_v128_load32_splat( (void const*)(ptr) ) ) + #define stbir__simdf_add_mem( out, reg, ptr ) (out) = wasm_f32x4_add( reg, wasm_v128_load( (void const*)(ptr) ) ) + #define stbir__simdf_add1_mem( out, reg, ptr ) (out) = wasm_f32x4_add( reg, wasm_v128_load32_splat( (void const*)(ptr) ) ) + + #define stbir__simdf_madd( out, add, mul1, mul2 ) (out) = wasm_f32x4_add( add, wasm_f32x4_mul( mul1, mul2 ) ) + #define stbir__simdf_madd1( out, add, mul1, mul2 ) (out) = wasm_f32x4_add( add, wasm_f32x4_mul( mul1, mul2 ) ) + #define stbir__simdf_madd_mem( out, add, mul, ptr ) (out) = wasm_f32x4_add( add, wasm_f32x4_mul( mul, wasm_v128_load( (void const*)(ptr) ) ) ) + #define stbir__simdf_madd1_mem( out, add, mul, ptr ) (out) = wasm_f32x4_add( add, wasm_f32x4_mul( mul, wasm_v128_load32_splat( (void const*)(ptr) ) ) ) + + #define stbir__simdf_add1( out, reg0, reg1 ) (out) = wasm_f32x4_add( reg0, reg1 ) + #define stbir__simdf_mult1( out, reg0, reg1 ) (out) = wasm_f32x4_mul( reg0, reg1 ) + + #define stbir__simdf_and( out, reg0, reg1 ) (out) = wasm_v128_and( reg0, reg1 ) + #define stbir__simdf_or( out, reg0, reg1 ) (out) = wasm_v128_or( reg0, reg1 ) + + #define stbir__simdf_min( out, reg0, reg1 ) (out) = wasm_f32x4_min( reg0, reg1 ) + #define stbir__simdf_max( out, reg0, reg1 ) (out) = wasm_f32x4_max( reg0, reg1 ) + #define stbir__simdf_min1( out, reg0, reg1 ) (out) = wasm_f32x4_min( reg0, reg1 ) + #define stbir__simdf_max1( out, reg0, reg1 ) (out) = wasm_f32x4_max( reg0, reg1 ) + + #define stbir__simdf_0123ABCDto3ABx( out, reg0, reg1 ) (out) = wasm_i32x4_shuffle( reg0, reg1, 3, 4, 5, -1 ) + #define stbir__simdf_0123ABCDto23Ax( out, reg0, reg1 ) (out) = wasm_i32x4_shuffle( reg0, reg1, 2, 3, 4, -1 ) + + #define stbir__simdf_aaa1(out,alp,ones) (out) = wasm_i32x4_shuffle(alp, ones, 3, 3, 3, 4) + #define stbir__simdf_1aaa(out,alp,ones) (out) = wasm_i32x4_shuffle(alp, ones, 4, 0, 0, 0) + #define stbir__simdf_a1a1(out,alp,ones) (out) = wasm_i32x4_shuffle(alp, ones, 1, 4, 3, 4) + #define stbir__simdf_1a1a(out,alp,ones) (out) = wasm_i32x4_shuffle(alp, ones, 4, 0, 4, 2) + + #define stbir__simdf_swiz( reg, one, two, three, four ) wasm_i32x4_shuffle(reg, reg, one, two, three, four) + + #define stbir__simdi_and( out, reg0, reg1 ) (out) = wasm_v128_and( reg0, reg1 ) + #define stbir__simdi_or( out, reg0, reg1 ) (out) = wasm_v128_or( reg0, reg1 ) + #define stbir__simdi_16madd( out, reg0, reg1 ) (out) = wasm_i32x4_dot_i16x8( reg0, reg1 ) + + #define stbir__simdf_pack_to_8bytes(out,aa,bb) \ + { \ + v128_t af = wasm_f32x4_max( wasm_f32x4_min(aa, STBIR_max_uint8_as_float), wasm_f32x4_const_splat(0) ); \ + v128_t bf = wasm_f32x4_max( wasm_f32x4_min(bb, STBIR_max_uint8_as_float), wasm_f32x4_const_splat(0) ); \ + v128_t ai = wasm_i32x4_trunc_sat_f32x4( af ); \ + v128_t bi = wasm_i32x4_trunc_sat_f32x4( bf ); \ + v128_t out16 = wasm_i16x8_narrow_i32x4( ai, bi ); \ + out = wasm_u8x16_narrow_i16x8( out16, out16 ); \ + } + + #define stbir__simdf_pack_to_8words(out,aa,bb) \ + { \ + v128_t af = wasm_f32x4_max( wasm_f32x4_min(aa, STBIR_max_uint16_as_float), wasm_f32x4_const_splat(0)); \ + v128_t bf = wasm_f32x4_max( wasm_f32x4_min(bb, STBIR_max_uint16_as_float), wasm_f32x4_const_splat(0)); \ + v128_t ai = wasm_i32x4_trunc_sat_f32x4( af ); \ + v128_t bi = wasm_i32x4_trunc_sat_f32x4( bf ); \ + out = wasm_u16x8_narrow_i32x4( ai, bi ); \ + } + + #define stbir__interleave_pack_and_store_16_u8( ptr, r0, r1, r2, r3 ) \ + { \ + v128_t tmp0 = wasm_i16x8_narrow_i32x4(r0, r1); \ + v128_t tmp1 = wasm_i16x8_narrow_i32x4(r2, r3); \ + v128_t tmp = wasm_u8x16_narrow_i16x8(tmp0, tmp1); \ + tmp = wasm_i8x16_shuffle(tmp, tmp, 0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15); \ + wasm_v128_store( (void*)(ptr), tmp); \ + } + + #define stbir__simdf_load4_transposed( o0, o1, o2, o3, ptr ) \ + { \ + v128_t t0 = wasm_v128_load( ptr ); \ + v128_t t1 = wasm_v128_load( ptr+4 ); \ + v128_t t2 = wasm_v128_load( ptr+8 ); \ + v128_t t3 = wasm_v128_load( ptr+12 ); \ + v128_t s0 = wasm_i32x4_shuffle(t0, t1, 0, 4, 2, 6); \ + v128_t s1 = wasm_i32x4_shuffle(t0, t1, 1, 5, 3, 7); \ + v128_t s2 = wasm_i32x4_shuffle(t2, t3, 0, 4, 2, 6); \ + v128_t s3 = wasm_i32x4_shuffle(t2, t3, 1, 5, 3, 7); \ + o0 = wasm_i32x4_shuffle(s0, s2, 0, 1, 4, 5); \ + o1 = wasm_i32x4_shuffle(s1, s3, 0, 1, 4, 5); \ + o2 = wasm_i32x4_shuffle(s0, s2, 2, 3, 6, 7); \ + o3 = wasm_i32x4_shuffle(s1, s3, 2, 3, 6, 7); \ + } + + #define stbir__simdi_32shr( out, reg, imm ) out = wasm_u32x4_shr( reg, imm ) + + typedef float stbir__f32x4 __attribute__((__vector_size__(16), __aligned__(16))); + #define STBIR__SIMDF_CONST(var, x) stbir__simdf var = (v128_t)(stbir__f32x4){ x, x, x, x } + #define STBIR__SIMDI_CONST(var, x) stbir__simdi var = { x, x, x, x } + #define STBIR__CONSTF(var) (var) + #define STBIR__CONSTI(var) (var) + + #ifdef STBIR_FLOORF + #undef STBIR_FLOORF + #endif + #define STBIR_FLOORF stbir_simd_floorf + static stbir__inline float stbir_simd_floorf(float x) + { + return wasm_f32x4_extract_lane( wasm_f32x4_floor( wasm_f32x4_splat(x) ), 0); + } + + #ifdef STBIR_CEILF + #undef STBIR_CEILF + #endif + #define STBIR_CEILF stbir_simd_ceilf + static stbir__inline float stbir_simd_ceilf(float x) + { + return wasm_f32x4_extract_lane( wasm_f32x4_ceil( wasm_f32x4_splat(x) ), 0); + } + + #define STBIR_SIMD + +#endif // SSE2/NEON/WASM + +#endif // NO SIMD + +#ifdef STBIR_SIMD8 + #define stbir__simdfX stbir__simdf8 + #define stbir__simdiX stbir__simdi8 + #define stbir__simdfX_load stbir__simdf8_load + #define stbir__simdiX_load stbir__simdi8_load + #define stbir__simdfX_mult stbir__simdf8_mult + #define stbir__simdfX_add_mem stbir__simdf8_add_mem + #define stbir__simdfX_madd_mem stbir__simdf8_madd_mem + #define stbir__simdfX_store stbir__simdf8_store + #define stbir__simdiX_store stbir__simdi8_store + #define stbir__simdf_frepX stbir__simdf8_frep8 + #define stbir__simdfX_madd stbir__simdf8_madd + #define stbir__simdfX_min stbir__simdf8_min + #define stbir__simdfX_max stbir__simdf8_max + #define stbir__simdfX_aaa1 stbir__simdf8_aaa1 + #define stbir__simdfX_1aaa stbir__simdf8_1aaa + #define stbir__simdfX_a1a1 stbir__simdf8_a1a1 + #define stbir__simdfX_1a1a stbir__simdf8_1a1a + #define stbir__simdfX_convert_float_to_i32 stbir__simdf8_convert_float_to_i32 + #define stbir__simdfX_pack_to_words stbir__simdf8_pack_to_16words + #define stbir__simdfX_zero stbir__simdf8_zero + #define STBIR_onesX STBIR_ones8 + #define STBIR_max_uint8_as_floatX STBIR_max_uint8_as_float8 + #define STBIR_max_uint16_as_floatX STBIR_max_uint16_as_float8 + #define STBIR_simd_point5X STBIR_simd_point58 + #define stbir__simdfX_float_count 8 + #define stbir__simdfX_0123to1230 stbir__simdf8_0123to12301230 + #define stbir__simdfX_0123to2103 stbir__simdf8_0123to21032103 + static const stbir__simdf8 STBIR_max_uint16_as_float_inverted8 = { stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted,stbir__max_uint16_as_float_inverted }; + static const stbir__simdf8 STBIR_max_uint8_as_float_inverted8 = { stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted,stbir__max_uint8_as_float_inverted }; + static const stbir__simdf8 STBIR_ones8 = { 1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 }; + static const stbir__simdf8 STBIR_simd_point58 = { 0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5 }; + static const stbir__simdf8 STBIR_max_uint8_as_float8 = { stbir__max_uint8_as_float,stbir__max_uint8_as_float,stbir__max_uint8_as_float,stbir__max_uint8_as_float, stbir__max_uint8_as_float,stbir__max_uint8_as_float,stbir__max_uint8_as_float,stbir__max_uint8_as_float }; + static const stbir__simdf8 STBIR_max_uint16_as_float8 = { stbir__max_uint16_as_float,stbir__max_uint16_as_float,stbir__max_uint16_as_float,stbir__max_uint16_as_float, stbir__max_uint16_as_float,stbir__max_uint16_as_float,stbir__max_uint16_as_float,stbir__max_uint16_as_float }; +#else + #define stbir__simdfX stbir__simdf + #define stbir__simdiX stbir__simdi + #define stbir__simdfX_load stbir__simdf_load + #define stbir__simdiX_load stbir__simdi_load + #define stbir__simdfX_mult stbir__simdf_mult + #define stbir__simdfX_add_mem stbir__simdf_add_mem + #define stbir__simdfX_madd_mem stbir__simdf_madd_mem + #define stbir__simdfX_store stbir__simdf_store + #define stbir__simdiX_store stbir__simdi_store + #define stbir__simdf_frepX stbir__simdf_frep4 + #define stbir__simdfX_madd stbir__simdf_madd + #define stbir__simdfX_min stbir__simdf_min + #define stbir__simdfX_max stbir__simdf_max + #define stbir__simdfX_aaa1 stbir__simdf_aaa1 + #define stbir__simdfX_1aaa stbir__simdf_1aaa + #define stbir__simdfX_a1a1 stbir__simdf_a1a1 + #define stbir__simdfX_1a1a stbir__simdf_1a1a + #define stbir__simdfX_convert_float_to_i32 stbir__simdf_convert_float_to_i32 + #define stbir__simdfX_pack_to_words stbir__simdf_pack_to_8words + #define stbir__simdfX_zero stbir__simdf_zero + #define STBIR_onesX STBIR__CONSTF(STBIR_ones) + #define STBIR_simd_point5X STBIR__CONSTF(STBIR_simd_point5) + #define STBIR_max_uint8_as_floatX STBIR__CONSTF(STBIR_max_uint8_as_float) + #define STBIR_max_uint16_as_floatX STBIR__CONSTF(STBIR_max_uint16_as_float) + #define stbir__simdfX_float_count 4 + #define stbir__if_simdf8_cast_to_simdf4( val ) ( val ) + #define stbir__simdfX_0123to1230 stbir__simdf_0123to1230 + #define stbir__simdfX_0123to2103 stbir__simdf_0123to2103 +#endif + + +#if defined(STBIR_NEON) && !defined(_M_ARM) && !defined(__arm__) + + #if defined( _MSC_VER ) && !defined(__clang__) + typedef __int16 stbir__FP16; + #else + typedef float16_t stbir__FP16; + #endif + +#else // no NEON, or 32-bit ARM for MSVC + + typedef union stbir__FP16 + { + unsigned short u; + } stbir__FP16; + +#endif + +#if (!defined(STBIR_NEON) && !defined(STBIR_FP16C)) || (defined(STBIR_NEON) && defined(_M_ARM)) || (defined(STBIR_NEON) && defined(__arm__)) + + // Fabian's half float routines, see: https://gist.github.com/rygorous/2156668 + + static stbir__inline float stbir__half_to_float( stbir__FP16 h ) + { + static const stbir__FP32 magic = { (254 - 15) << 23 }; + static const stbir__FP32 was_infnan = { (127 + 16) << 23 }; + stbir__FP32 o; + + o.u = (h.u & 0x7fff) << 13; // exponent/mantissa bits + o.f *= magic.f; // exponent adjust + if (o.f >= was_infnan.f) // make sure Inf/NaN survive + o.u |= 255 << 23; + o.u |= (h.u & 0x8000) << 16; // sign bit + return o.f; + } + + static stbir__inline stbir__FP16 stbir__float_to_half(float val) + { + stbir__FP32 f32infty = { 255 << 23 }; + stbir__FP32 f16max = { (127 + 16) << 23 }; + stbir__FP32 denorm_magic = { ((127 - 15) + (23 - 10) + 1) << 23 }; + unsigned int sign_mask = 0x80000000u; + stbir__FP16 o = { 0 }; + stbir__FP32 f; + unsigned int sign; + + f.f = val; + sign = f.u & sign_mask; + f.u ^= sign; + + if (f.u >= f16max.u) // result is Inf or NaN (all exponent bits set) + o.u = (f.u > f32infty.u) ? 0x7e00 : 0x7c00; // NaN->qNaN and Inf->Inf + else // (De)normalized number or zero + { + if (f.u < (113 << 23)) // resulting FP16 is subnormal or zero + { + // use a magic value to align our 10 mantissa bits at the bottom of + // the float. as long as FP addition is round-to-nearest-even this + // just works. + f.f += denorm_magic.f; + // and one integer subtract of the bias later, we have our final float! + o.u = (unsigned short) ( f.u - denorm_magic.u ); + } + else + { + unsigned int mant_odd = (f.u >> 13) & 1; // resulting mantissa is odd + // update exponent, rounding bias part 1 + f.u = f.u + ((15u - 127) << 23) + 0xfff; + // rounding bias part 2 + f.u += mant_odd; + // take the bits! + o.u = (unsigned short) ( f.u >> 13 ); + } + } + + o.u |= sign >> 16; + return o; + } + +#endif + + +#if defined(STBIR_FP16C) + + #include <immintrin.h> + + static stbir__inline void stbir__half_to_float_SIMD(float * output, stbir__FP16 const * input) + { + _mm256_storeu_ps( (float*)output, _mm256_cvtph_ps( _mm_loadu_si128( (__m128i const* )input ) ) ); + } + + static stbir__inline void stbir__float_to_half_SIMD(stbir__FP16 * output, float const * input) + { + _mm_storeu_si128( (__m128i*)output, _mm256_cvtps_ph( _mm256_loadu_ps( input ), 0 ) ); + } + + static stbir__inline float stbir__half_to_float( stbir__FP16 h ) + { + return _mm_cvtss_f32( _mm_cvtph_ps( _mm_cvtsi32_si128( (int)h.u ) ) ); + } + + static stbir__inline stbir__FP16 stbir__float_to_half( float f ) + { + stbir__FP16 h; + h.u = (unsigned short) _mm_cvtsi128_si32( _mm_cvtps_ph( _mm_set_ss( f ), 0 ) ); + return h; + } + +#elif defined(STBIR_SSE2) + + // Fabian's half float routines, see: https://gist.github.com/rygorous/2156668 + stbir__inline static void stbir__half_to_float_SIMD(float * output, void const * input) + { + static const STBIR__SIMDI_CONST(mask_nosign, 0x7fff); + static const STBIR__SIMDI_CONST(smallest_normal, 0x0400); + static const STBIR__SIMDI_CONST(infinity, 0x7c00); + static const STBIR__SIMDI_CONST(expadjust_normal, (127 - 15) << 23); + static const STBIR__SIMDI_CONST(magic_denorm, 113 << 23); + + __m128i i = _mm_loadu_si128 ( (__m128i const*)(input) ); + __m128i h = _mm_unpacklo_epi16 ( i, _mm_setzero_si128() ); + __m128i mnosign = STBIR__CONSTI(mask_nosign); + __m128i eadjust = STBIR__CONSTI(expadjust_normal); + __m128i smallest = STBIR__CONSTI(smallest_normal); + __m128i infty = STBIR__CONSTI(infinity); + __m128i expmant = _mm_and_si128(mnosign, h); + __m128i justsign = _mm_xor_si128(h, expmant); + __m128i b_notinfnan = _mm_cmpgt_epi32(infty, expmant); + __m128i b_isdenorm = _mm_cmpgt_epi32(smallest, expmant); + __m128i shifted = _mm_slli_epi32(expmant, 13); + __m128i adj_infnan = _mm_andnot_si128(b_notinfnan, eadjust); + __m128i adjusted = _mm_add_epi32(eadjust, shifted); + __m128i den1 = _mm_add_epi32(shifted, STBIR__CONSTI(magic_denorm)); + __m128i adjusted2 = _mm_add_epi32(adjusted, adj_infnan); + __m128 den2 = _mm_sub_ps(_mm_castsi128_ps(den1), *(const __m128 *)&magic_denorm); + __m128 adjusted3 = _mm_and_ps(den2, _mm_castsi128_ps(b_isdenorm)); + __m128 adjusted4 = _mm_andnot_ps(_mm_castsi128_ps(b_isdenorm), _mm_castsi128_ps(adjusted2)); + __m128 adjusted5 = _mm_or_ps(adjusted3, adjusted4); + __m128i sign = _mm_slli_epi32(justsign, 16); + __m128 final = _mm_or_ps(adjusted5, _mm_castsi128_ps(sign)); + stbir__simdf_store( output + 0, final ); + + h = _mm_unpackhi_epi16 ( i, _mm_setzero_si128() ); + expmant = _mm_and_si128(mnosign, h); + justsign = _mm_xor_si128(h, expmant); + b_notinfnan = _mm_cmpgt_epi32(infty, expmant); + b_isdenorm = _mm_cmpgt_epi32(smallest, expmant); + shifted = _mm_slli_epi32(expmant, 13); + adj_infnan = _mm_andnot_si128(b_notinfnan, eadjust); + adjusted = _mm_add_epi32(eadjust, shifted); + den1 = _mm_add_epi32(shifted, STBIR__CONSTI(magic_denorm)); + adjusted2 = _mm_add_epi32(adjusted, adj_infnan); + den2 = _mm_sub_ps(_mm_castsi128_ps(den1), *(const __m128 *)&magic_denorm); + adjusted3 = _mm_and_ps(den2, _mm_castsi128_ps(b_isdenorm)); + adjusted4 = _mm_andnot_ps(_mm_castsi128_ps(b_isdenorm), _mm_castsi128_ps(adjusted2)); + adjusted5 = _mm_or_ps(adjusted3, adjusted4); + sign = _mm_slli_epi32(justsign, 16); + final = _mm_or_ps(adjusted5, _mm_castsi128_ps(sign)); + stbir__simdf_store( output + 4, final ); + + // ~38 SSE2 ops for 8 values + } + + // Fabian's round-to-nearest-even float to half + // ~48 SSE2 ops for 8 output + stbir__inline static void stbir__float_to_half_SIMD(void * output, float const * input) + { + static const STBIR__SIMDI_CONST(mask_sign, 0x80000000u); + static const STBIR__SIMDI_CONST(c_f16max, (127 + 16) << 23); // all FP32 values >=this round to +inf + static const STBIR__SIMDI_CONST(c_nanbit, 0x200); + static const STBIR__SIMDI_CONST(c_infty_as_fp16, 0x7c00); + static const STBIR__SIMDI_CONST(c_min_normal, (127 - 14) << 23); // smallest FP32 that yields a normalized FP16 + static const STBIR__SIMDI_CONST(c_subnorm_magic, ((127 - 15) + (23 - 10) + 1) << 23); + static const STBIR__SIMDI_CONST(c_normal_bias, 0xfff - ((127 - 15) << 23)); // adjust exponent and add mantissa rounding + + __m128 f = _mm_loadu_ps(input); + __m128 msign = _mm_castsi128_ps(STBIR__CONSTI(mask_sign)); + __m128 justsign = _mm_and_ps(msign, f); + __m128 absf = _mm_xor_ps(f, justsign); + __m128i absf_int = _mm_castps_si128(absf); // the cast is "free" (extra bypass latency, but no thruput hit) + __m128i f16max = STBIR__CONSTI(c_f16max); + __m128 b_isnan = _mm_cmpunord_ps(absf, absf); // is this a NaN? + __m128i b_isregular = _mm_cmpgt_epi32(f16max, absf_int); // (sub)normalized or special? + __m128i nanbit = _mm_and_si128(_mm_castps_si128(b_isnan), STBIR__CONSTI(c_nanbit)); + __m128i inf_or_nan = _mm_or_si128(nanbit, STBIR__CONSTI(c_infty_as_fp16)); // output for specials + + __m128i min_normal = STBIR__CONSTI(c_min_normal); + __m128i b_issub = _mm_cmpgt_epi32(min_normal, absf_int); + + // "result is subnormal" path + __m128 subnorm1 = _mm_add_ps(absf, _mm_castsi128_ps(STBIR__CONSTI(c_subnorm_magic))); // magic value to round output mantissa + __m128i subnorm2 = _mm_sub_epi32(_mm_castps_si128(subnorm1), STBIR__CONSTI(c_subnorm_magic)); // subtract out bias + + // "result is normal" path + __m128i mantoddbit = _mm_slli_epi32(absf_int, 31 - 13); // shift bit 13 (mantissa LSB) to sign + __m128i mantodd = _mm_srai_epi32(mantoddbit, 31); // -1 if FP16 mantissa odd, else 0 + + __m128i round1 = _mm_add_epi32(absf_int, STBIR__CONSTI(c_normal_bias)); + __m128i round2 = _mm_sub_epi32(round1, mantodd); // if mantissa LSB odd, bias towards rounding up (RTNE) + __m128i normal = _mm_srli_epi32(round2, 13); // rounded result + + // combine the two non-specials + __m128i nonspecial = _mm_or_si128(_mm_and_si128(subnorm2, b_issub), _mm_andnot_si128(b_issub, normal)); + + // merge in specials as well + __m128i joined = _mm_or_si128(_mm_and_si128(nonspecial, b_isregular), _mm_andnot_si128(b_isregular, inf_or_nan)); + + __m128i sign_shift = _mm_srai_epi32(_mm_castps_si128(justsign), 16); + __m128i final2, final= _mm_or_si128(joined, sign_shift); + + f = _mm_loadu_ps(input+4); + justsign = _mm_and_ps(msign, f); + absf = _mm_xor_ps(f, justsign); + absf_int = _mm_castps_si128(absf); // the cast is "free" (extra bypass latency, but no thruput hit) + b_isnan = _mm_cmpunord_ps(absf, absf); // is this a NaN? + b_isregular = _mm_cmpgt_epi32(f16max, absf_int); // (sub)normalized or special? + nanbit = _mm_and_si128(_mm_castps_si128(b_isnan), c_nanbit); + inf_or_nan = _mm_or_si128(nanbit, STBIR__CONSTI(c_infty_as_fp16)); // output for specials + + b_issub = _mm_cmpgt_epi32(min_normal, absf_int); + + // "result is subnormal" path + subnorm1 = _mm_add_ps(absf, _mm_castsi128_ps(STBIR__CONSTI(c_subnorm_magic))); // magic value to round output mantissa + subnorm2 = _mm_sub_epi32(_mm_castps_si128(subnorm1), STBIR__CONSTI(c_subnorm_magic)); // subtract out bias + + // "result is normal" path + mantoddbit = _mm_slli_epi32(absf_int, 31 - 13); // shift bit 13 (mantissa LSB) to sign + mantodd = _mm_srai_epi32(mantoddbit, 31); // -1 if FP16 mantissa odd, else 0 + + round1 = _mm_add_epi32(absf_int, STBIR__CONSTI(c_normal_bias)); + round2 = _mm_sub_epi32(round1, mantodd); // if mantissa LSB odd, bias towards rounding up (RTNE) + normal = _mm_srli_epi32(round2, 13); // rounded result + + // combine the two non-specials + nonspecial = _mm_or_si128(_mm_and_si128(subnorm2, b_issub), _mm_andnot_si128(b_issub, normal)); + + // merge in specials as well + joined = _mm_or_si128(_mm_and_si128(nonspecial, b_isregular), _mm_andnot_si128(b_isregular, inf_or_nan)); + + sign_shift = _mm_srai_epi32(_mm_castps_si128(justsign), 16); + final2 = _mm_or_si128(joined, sign_shift); + final = _mm_packs_epi32(final, final2); + stbir__simdi_store( output,final ); + } + +#elif defined(STBIR_NEON) && defined(_MSC_VER) && defined(_M_ARM64) && !defined(__clang__) // 64-bit ARM on MSVC (not clang) + + static stbir__inline void stbir__half_to_float_SIMD(float * output, stbir__FP16 const * input) + { + float16x4_t in0 = vld1_f16(input + 0); + float16x4_t in1 = vld1_f16(input + 4); + vst1q_f32(output + 0, vcvt_f32_f16(in0)); + vst1q_f32(output + 4, vcvt_f32_f16(in1)); + } + + static stbir__inline void stbir__float_to_half_SIMD(stbir__FP16 * output, float const * input) + { + float16x4_t out0 = vcvt_f16_f32(vld1q_f32(input + 0)); + float16x4_t out1 = vcvt_f16_f32(vld1q_f32(input + 4)); + vst1_f16(output+0, out0); + vst1_f16(output+4, out1); + } + + static stbir__inline float stbir__half_to_float( stbir__FP16 h ) + { + return vgetq_lane_f32(vcvt_f32_f16(vld1_dup_f16(&h)), 0); + } + + static stbir__inline stbir__FP16 stbir__float_to_half( float f ) + { + return vget_lane_f16(vcvt_f16_f32(vdupq_n_f32(f)), 0).n16_u16[0]; + } + +#elif defined(STBIR_NEON) && ( defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) ) // 64-bit ARM + + static stbir__inline void stbir__half_to_float_SIMD(float * output, stbir__FP16 const * input) + { + float16x8_t in = vld1q_f16(input); + vst1q_f32(output + 0, vcvt_f32_f16(vget_low_f16(in))); + vst1q_f32(output + 4, vcvt_f32_f16(vget_high_f16(in))); + } + + static stbir__inline void stbir__float_to_half_SIMD(stbir__FP16 * output, float const * input) + { + float16x4_t out0 = vcvt_f16_f32(vld1q_f32(input + 0)); + float16x4_t out1 = vcvt_f16_f32(vld1q_f32(input + 4)); + vst1q_f16(output, vcombine_f16(out0, out1)); + } + + static stbir__inline float stbir__half_to_float( stbir__FP16 h ) + { + return vgetq_lane_f32(vcvt_f32_f16(vdup_n_f16(h)), 0); + } + + static stbir__inline stbir__FP16 stbir__float_to_half( float f ) + { + return vget_lane_f16(vcvt_f16_f32(vdupq_n_f32(f)), 0); + } + +#elif defined(STBIR_WASM) || (defined(STBIR_NEON) && (defined(_MSC_VER) || defined(_M_ARM) || defined(__arm__))) // WASM or 32-bit ARM on MSVC/clang + + static stbir__inline void stbir__half_to_float_SIMD(float * output, stbir__FP16 const * input) + { + for (int i=0; i<8; i++) + { + output[i] = stbir__half_to_float(input[i]); + } + } + static stbir__inline void stbir__float_to_half_SIMD(stbir__FP16 * output, float const * input) + { + for (int i=0; i<8; i++) + { + output[i] = stbir__float_to_half(input[i]); + } + } + +#endif + + +#ifdef STBIR_SIMD + +#define stbir__simdf_0123to3333( out, reg ) (out) = stbir__simdf_swiz( reg, 3,3,3,3 ) +#define stbir__simdf_0123to2222( out, reg ) (out) = stbir__simdf_swiz( reg, 2,2,2,2 ) +#define stbir__simdf_0123to1111( out, reg ) (out) = stbir__simdf_swiz( reg, 1,1,1,1 ) +#define stbir__simdf_0123to0000( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,0,0 ) +#define stbir__simdf_0123to0003( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,0,3 ) +#define stbir__simdf_0123to0001( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,0,1 ) +#define stbir__simdf_0123to1122( out, reg ) (out) = stbir__simdf_swiz( reg, 1,1,2,2 ) +#define stbir__simdf_0123to2333( out, reg ) (out) = stbir__simdf_swiz( reg, 2,3,3,3 ) +#define stbir__simdf_0123to0023( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,2,3 ) +#define stbir__simdf_0123to1230( out, reg ) (out) = stbir__simdf_swiz( reg, 1,2,3,0 ) +#define stbir__simdf_0123to2103( out, reg ) (out) = stbir__simdf_swiz( reg, 2,1,0,3 ) +#define stbir__simdf_0123to3210( out, reg ) (out) = stbir__simdf_swiz( reg, 3,2,1,0 ) +#define stbir__simdf_0123to2301( out, reg ) (out) = stbir__simdf_swiz( reg, 2,3,0,1 ) +#define stbir__simdf_0123to3012( out, reg ) (out) = stbir__simdf_swiz( reg, 3,0,1,2 ) +#define stbir__simdf_0123to0011( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,1,1 ) +#define stbir__simdf_0123to1100( out, reg ) (out) = stbir__simdf_swiz( reg, 1,1,0,0 ) +#define stbir__simdf_0123to2233( out, reg ) (out) = stbir__simdf_swiz( reg, 2,2,3,3 ) +#define stbir__simdf_0123to1133( out, reg ) (out) = stbir__simdf_swiz( reg, 1,1,3,3 ) +#define stbir__simdf_0123to0022( out, reg ) (out) = stbir__simdf_swiz( reg, 0,0,2,2 ) +#define stbir__simdf_0123to1032( out, reg ) (out) = stbir__simdf_swiz( reg, 1,0,3,2 ) + +typedef union stbir__simdi_u32 +{ + stbir_uint32 m128i_u32[4]; + int m128i_i32[4]; + stbir__simdi m128i_i128; +} stbir__simdi_u32; + +static const int STBIR_mask[9] = { 0,0,0,-1,-1,-1,0,0,0 }; + +static const STBIR__SIMDF_CONST(STBIR_max_uint8_as_float, stbir__max_uint8_as_float); +static const STBIR__SIMDF_CONST(STBIR_max_uint16_as_float, stbir__max_uint16_as_float); +static const STBIR__SIMDF_CONST(STBIR_max_uint8_as_float_inverted, stbir__max_uint8_as_float_inverted); +static const STBIR__SIMDF_CONST(STBIR_max_uint16_as_float_inverted, stbir__max_uint16_as_float_inverted); + +static const STBIR__SIMDF_CONST(STBIR_simd_point5, 0.5f); +static const STBIR__SIMDF_CONST(STBIR_ones, 1.0f); +static const STBIR__SIMDI_CONST(STBIR_almost_zero, (127 - 13) << 23); +static const STBIR__SIMDI_CONST(STBIR_almost_one, 0x3f7fffff); +static const STBIR__SIMDI_CONST(STBIR_mantissa_mask, 0xff); +static const STBIR__SIMDI_CONST(STBIR_topscale, 0x02000000); + +// Basically, in simd mode, we unroll the proper amount, and we don't want +// the non-simd remnant loops to be unroll because they only run a few times +// Adding this switch saves about 5K on clang which is Captain Unroll the 3rd. +#define STBIR_SIMD_STREAMOUT_PTR( star ) STBIR_STREAMOUT_PTR( star ) +#define STBIR_SIMD_NO_UNROLL(ptr) STBIR_NO_UNROLL(ptr) +#define STBIR_SIMD_NO_UNROLL_LOOP_START STBIR_NO_UNROLL_LOOP_START +#define STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR STBIR_NO_UNROLL_LOOP_START_INF_FOR + +#ifdef STBIR_MEMCPY +#undef STBIR_MEMCPY +#endif +#define STBIR_MEMCPY stbir_simd_memcpy + +// override normal use of memcpy with much simpler copy (faster and smaller with our sized copies) +static void stbir_simd_memcpy( void * dest, void const * src, size_t bytes ) +{ + char STBIR_SIMD_STREAMOUT_PTR (*) d = (char*) dest; + char STBIR_SIMD_STREAMOUT_PTR( * ) d_end = ((char*) dest) + bytes; + ptrdiff_t ofs_to_src = (char*)src - (char*)dest; + + // check overlaps + STBIR_ASSERT( ( ( d >= ( (char*)src) + bytes ) ) || ( ( d + bytes ) <= (char*)src ) ); + + if ( bytes < (16*stbir__simdfX_float_count) ) + { + if ( bytes < 16 ) + { + if ( bytes ) + { + STBIR_SIMD_NO_UNROLL_LOOP_START + do + { + STBIR_SIMD_NO_UNROLL(d); + d[ 0 ] = d[ ofs_to_src ]; + ++d; + } while ( d < d_end ); + } + } + else + { + stbir__simdf x; + // do one unaligned to get us aligned for the stream out below + stbir__simdf_load( x, ( d + ofs_to_src ) ); + stbir__simdf_store( d, x ); + d = (char*)( ( ( (size_t)d ) + 16 ) & ~15 ); + + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + STBIR_SIMD_NO_UNROLL(d); + + if ( d > ( d_end - 16 ) ) + { + if ( d == d_end ) + return; + d = d_end - 16; + } + + stbir__simdf_load( x, ( d + ofs_to_src ) ); + stbir__simdf_store( d, x ); + d += 16; + } + } + } + else + { + stbir__simdfX x0,x1,x2,x3; + + // do one unaligned to get us aligned for the stream out below + stbir__simdfX_load( x0, ( d + ofs_to_src ) + 0*stbir__simdfX_float_count ); + stbir__simdfX_load( x1, ( d + ofs_to_src ) + 4*stbir__simdfX_float_count ); + stbir__simdfX_load( x2, ( d + ofs_to_src ) + 8*stbir__simdfX_float_count ); + stbir__simdfX_load( x3, ( d + ofs_to_src ) + 12*stbir__simdfX_float_count ); + stbir__simdfX_store( d + 0*stbir__simdfX_float_count, x0 ); + stbir__simdfX_store( d + 4*stbir__simdfX_float_count, x1 ); + stbir__simdfX_store( d + 8*stbir__simdfX_float_count, x2 ); + stbir__simdfX_store( d + 12*stbir__simdfX_float_count, x3 ); + d = (char*)( ( ( (size_t)d ) + (16*stbir__simdfX_float_count) ) & ~((16*stbir__simdfX_float_count)-1) ); + + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + STBIR_SIMD_NO_UNROLL(d); + + if ( d > ( d_end - (16*stbir__simdfX_float_count) ) ) + { + if ( d == d_end ) + return; + d = d_end - (16*stbir__simdfX_float_count); + } + + stbir__simdfX_load( x0, ( d + ofs_to_src ) + 0*stbir__simdfX_float_count ); + stbir__simdfX_load( x1, ( d + ofs_to_src ) + 4*stbir__simdfX_float_count ); + stbir__simdfX_load( x2, ( d + ofs_to_src ) + 8*stbir__simdfX_float_count ); + stbir__simdfX_load( x3, ( d + ofs_to_src ) + 12*stbir__simdfX_float_count ); + stbir__simdfX_store( d + 0*stbir__simdfX_float_count, x0 ); + stbir__simdfX_store( d + 4*stbir__simdfX_float_count, x1 ); + stbir__simdfX_store( d + 8*stbir__simdfX_float_count, x2 ); + stbir__simdfX_store( d + 12*stbir__simdfX_float_count, x3 ); + d += (16*stbir__simdfX_float_count); + } + } +} + +// memcpy that is specically intentionally overlapping (src is smaller then dest, so can be +// a normal forward copy, bytes is divisible by 4 and bytes is greater than or equal to +// the diff between dest and src) +static void stbir_overlapping_memcpy( void * dest, void const * src, size_t bytes ) +{ + char STBIR_SIMD_STREAMOUT_PTR (*) sd = (char*) src; + char STBIR_SIMD_STREAMOUT_PTR( * ) s_end = ((char*) src) + bytes; + ptrdiff_t ofs_to_dest = (char*)dest - (char*)src; + + if ( ofs_to_dest >= 16 ) // is the overlap more than 16 away? + { + char STBIR_SIMD_STREAMOUT_PTR( * ) s_end16 = ((char*) src) + (bytes&~15); + STBIR_SIMD_NO_UNROLL_LOOP_START + do + { + stbir__simdf x; + STBIR_SIMD_NO_UNROLL(sd); + stbir__simdf_load( x, sd ); + stbir__simdf_store( ( sd + ofs_to_dest ), x ); + sd += 16; + } while ( sd < s_end16 ); + + if ( sd == s_end ) + return; + } + + do + { + STBIR_SIMD_NO_UNROLL(sd); + *(int*)( sd + ofs_to_dest ) = *(int*) sd; + sd += 4; + } while ( sd < s_end ); +} + +#else // no SSE2 + +// when in scalar mode, we let unrolling happen, so this macro just does the __restrict +#define STBIR_SIMD_STREAMOUT_PTR( star ) STBIR_STREAMOUT_PTR( star ) +#define STBIR_SIMD_NO_UNROLL(ptr) +#define STBIR_SIMD_NO_UNROLL_LOOP_START +#define STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + +#endif // SSE2 + + +#ifdef STBIR_PROFILE + +#ifndef STBIR_PROFILE_FUNC + +#if defined(_x86_64) || defined( __x86_64__ ) || defined( _M_X64 ) || defined(__x86_64) || defined(__SSE2__) || defined(STBIR_SSE) || defined( _M_IX86_FP ) || defined(__i386) || defined( __i386__ ) || defined( _M_IX86 ) || defined( _X86_ ) + +#ifdef _MSC_VER + + STBIRDEF stbir_uint64 __rdtsc(); + #define STBIR_PROFILE_FUNC() __rdtsc() + +#else // non msvc + + static stbir__inline stbir_uint64 STBIR_PROFILE_FUNC() + { + stbir_uint32 lo, hi; + asm volatile ("rdtsc" : "=a" (lo), "=d" (hi) ); + return ( ( (stbir_uint64) hi ) << 32 ) | ( (stbir_uint64) lo ); + } + +#endif // msvc + +#elif defined( _M_ARM64 ) || defined( __aarch64__ ) || defined( __arm64__ ) || defined(__ARM_NEON__) + +#if defined( _MSC_VER ) && !defined(__clang__) + + #define STBIR_PROFILE_FUNC() _ReadStatusReg(ARM64_CNTVCT) + +#else + + static stbir__inline stbir_uint64 STBIR_PROFILE_FUNC() + { + stbir_uint64 tsc; + asm volatile("mrs %0, cntvct_el0" : "=r" (tsc)); + return tsc; + } + +#endif + +#else // x64, arm + +#error Unknown platform for profiling. + +#endif // x64, arm + +#endif // STBIR_PROFILE_FUNC + +#define STBIR_ONLY_PROFILE_GET_SPLIT_INFO ,stbir__per_split_info * split_info +#define STBIR_ONLY_PROFILE_SET_SPLIT_INFO ,split_info + +#define STBIR_ONLY_PROFILE_BUILD_GET_INFO ,stbir__info * profile_info +#define STBIR_ONLY_PROFILE_BUILD_SET_INFO ,profile_info + +// super light-weight micro profiler +#define STBIR_PROFILE_START_ll( info, wh ) { stbir_uint64 wh##thiszonetime = STBIR_PROFILE_FUNC(); stbir_uint64 * wh##save_parent_excluded_ptr = info->current_zone_excluded_ptr; stbir_uint64 wh##current_zone_excluded = 0; info->current_zone_excluded_ptr = &wh##current_zone_excluded; +#define STBIR_PROFILE_END_ll( info, wh ) wh##thiszonetime = STBIR_PROFILE_FUNC() - wh##thiszonetime; info->profile.named.wh += wh##thiszonetime - wh##current_zone_excluded; *wh##save_parent_excluded_ptr += wh##thiszonetime; info->current_zone_excluded_ptr = wh##save_parent_excluded_ptr; } +#define STBIR_PROFILE_FIRST_START_ll( info, wh ) { int i; info->current_zone_excluded_ptr = &info->profile.named.total; for(i=0;i<STBIR__ARRAY_SIZE(info->profile.array);i++) info->profile.array[i]=0; } STBIR_PROFILE_START_ll( info, wh ); +#define STBIR_PROFILE_CLEAR_EXTRAS_ll( info, num ) { int extra; for(extra=1;extra<(num);extra++) { int i; for(i=0;i<STBIR__ARRAY_SIZE((info)->profile.array);i++) (info)[extra].profile.array[i]=0; } } + +// for thread data +#define STBIR_PROFILE_START( wh ) STBIR_PROFILE_START_ll( split_info, wh ) +#define STBIR_PROFILE_END( wh ) STBIR_PROFILE_END_ll( split_info, wh ) +#define STBIR_PROFILE_FIRST_START( wh ) STBIR_PROFILE_FIRST_START_ll( split_info, wh ) +#define STBIR_PROFILE_CLEAR_EXTRAS() STBIR_PROFILE_CLEAR_EXTRAS_ll( split_info, split_count ) + +// for build data +#define STBIR_PROFILE_BUILD_START( wh ) STBIR_PROFILE_START_ll( profile_info, wh ) +#define STBIR_PROFILE_BUILD_END( wh ) STBIR_PROFILE_END_ll( profile_info, wh ) +#define STBIR_PROFILE_BUILD_FIRST_START( wh ) STBIR_PROFILE_FIRST_START_ll( profile_info, wh ) +#define STBIR_PROFILE_BUILD_CLEAR( info ) { int i; for(i=0;i<STBIR__ARRAY_SIZE(info->profile.array);i++) info->profile.array[i]=0; } + +#else // no profile + +#define STBIR_ONLY_PROFILE_GET_SPLIT_INFO +#define STBIR_ONLY_PROFILE_SET_SPLIT_INFO + +#define STBIR_ONLY_PROFILE_BUILD_GET_INFO +#define STBIR_ONLY_PROFILE_BUILD_SET_INFO + +#define STBIR_PROFILE_START( wh ) +#define STBIR_PROFILE_END( wh ) +#define STBIR_PROFILE_FIRST_START( wh ) +#define STBIR_PROFILE_CLEAR_EXTRAS( ) + +#define STBIR_PROFILE_BUILD_START( wh ) +#define STBIR_PROFILE_BUILD_END( wh ) +#define STBIR_PROFILE_BUILD_FIRST_START( wh ) +#define STBIR_PROFILE_BUILD_CLEAR( info ) + +#endif // stbir_profile + +#ifndef STBIR_CEILF +#include <math.h> +#if _MSC_VER <= 1200 // support VC6 for Sean +#define STBIR_CEILF(x) ((float)ceil((float)(x))) +#define STBIR_FLOORF(x) ((float)floor((float)(x))) +#else +#define STBIR_CEILF(x) ceilf(x) +#define STBIR_FLOORF(x) floorf(x) +#endif +#endif + +#ifndef STBIR_MEMCPY +// For memcpy +#include <string.h> +#define STBIR_MEMCPY( dest, src, len ) memcpy( dest, src, len ) +#endif + +#ifndef STBIR_SIMD + +// memcpy that is specifically intentionally overlapping (src is smaller then dest, so can be +// a normal forward copy, bytes is divisible by 4 and bytes is greater than or equal to +// the diff between dest and src) +static void stbir_overlapping_memcpy( void * dest, void const * src, size_t bytes ) +{ + char STBIR_SIMD_STREAMOUT_PTR (*) sd = (char*) src; + char STBIR_SIMD_STREAMOUT_PTR( * ) s_end = ((char*) src) + bytes; + ptrdiff_t ofs_to_dest = (char*)dest - (char*)src; + + if ( ofs_to_dest >= 8 ) // is the overlap more than 8 away + { + char STBIR_SIMD_STREAMOUT_PTR( * ) s_end8 = ((char*) src) + (bytes&~7); + + if ( ( ( ((ptrdiff_t)dest)|((ptrdiff_t)src) ) & 7 ) == 0 ) // is it 8byte aligned? + { + STBIR_NO_UNROLL_LOOP_START + do + { + STBIR_NO_UNROLL(sd); + *(stbir_uint64*)( sd + ofs_to_dest ) = *(stbir_uint64*) sd; + sd += 8; + } while ( sd < s_end8 ); + } + else + { + STBIR_NO_UNROLL_LOOP_START + do + { + int a,b; + STBIR_NO_UNROLL(sd); + a = ((int*)sd)[0]; + b = ((int*)sd)[1]; + ((int*)( sd + ofs_to_dest ))[0] = a; + ((int*)( sd + ofs_to_dest ))[1] = b; + sd += 8; + } while ( sd < s_end8 ); + } + + if ( sd == s_end ) + return; + } + + STBIR_NO_UNROLL_LOOP_START + do + { + STBIR_NO_UNROLL(sd); + *(int*)( sd + ofs_to_dest ) = *(int*) sd; + sd += 4; + } while ( sd < s_end ); +} + +#endif + +static float stbir__filter_trapezoid(float x, float scale, void * user_data) +{ + float halfscale = scale / 2; + float t = 0.5f + halfscale; + STBIR_ASSERT(scale <= 1); + STBIR__UNUSED(user_data); + + if ( x < 0.0f ) x = -x; + + if (x >= t) + return 0.0f; + else + { + float r = 0.5f - halfscale; + if (x <= r) + return 1.0f; + else + return (t - x) / scale; + } +} + +static float stbir__support_trapezoid(float scale, void * user_data) +{ + STBIR__UNUSED(user_data); + return 0.5f + scale / 2.0f; +} + +static float stbir__filter_triangle(float x, float s, void * user_data) +{ + STBIR__UNUSED(s); + STBIR__UNUSED(user_data); + + if ( x < 0.0f ) x = -x; + + if (x <= 1.0f) + return 1.0f - x; + else + return 0.0f; +} + +static float stbir__filter_point(float x, float s, void * user_data) +{ + STBIR__UNUSED(x); + STBIR__UNUSED(s); + STBIR__UNUSED(user_data); + + return 1.0f; +} + +static float stbir__filter_cubic(float x, float s, void * user_data) +{ + STBIR__UNUSED(s); + STBIR__UNUSED(user_data); + + if ( x < 0.0f ) x = -x; + + if (x < 1.0f) + return (4.0f + x*x*(3.0f*x - 6.0f))/6.0f; + else if (x < 2.0f) + return (8.0f + x*(-12.0f + x*(6.0f - x)))/6.0f; + + return (0.0f); +} + +static float stbir__filter_catmullrom(float x, float s, void * user_data) +{ + STBIR__UNUSED(s); + STBIR__UNUSED(user_data); + + if ( x < 0.0f ) x = -x; + + if (x < 1.0f) + return 1.0f - x*x*(2.5f - 1.5f*x); + else if (x < 2.0f) + return 2.0f - x*(4.0f + x*(0.5f*x - 2.5f)); + + return (0.0f); +} + +static float stbir__filter_mitchell(float x, float s, void * user_data) +{ + STBIR__UNUSED(s); + STBIR__UNUSED(user_data); + + if ( x < 0.0f ) x = -x; + + if (x < 1.0f) + return (16.0f + x*x*(21.0f * x - 36.0f))/18.0f; + else if (x < 2.0f) + return (32.0f + x*(-60.0f + x*(36.0f - 7.0f*x)))/18.0f; + + return (0.0f); +} + +static float stbir__support_zeropoint5(float s, void * user_data) +{ + STBIR__UNUSED(s); + STBIR__UNUSED(user_data); + return 0.5f; +} + +static float stbir__support_one(float s, void * user_data) +{ + STBIR__UNUSED(s); + STBIR__UNUSED(user_data); + return 1; +} + +static float stbir__support_two(float s, void * user_data) +{ + STBIR__UNUSED(s); + STBIR__UNUSED(user_data); + return 2; +} + +// This is the maximum number of input samples that can affect an output sample +// with the given filter from the output pixel's perspective +static int stbir__get_filter_pixel_width(stbir__support_callback * support, float scale, void * user_data) +{ + STBIR_ASSERT(support != 0); + + if ( scale >= ( 1.0f-stbir__small_float ) ) // upscale + return (int)STBIR_CEILF(support(1.0f/scale,user_data) * 2.0f); + else + return (int)STBIR_CEILF(support(scale,user_data) * 2.0f / scale); +} + +// this is how many coefficents per run of the filter (which is different +// from the filter_pixel_width depending on if we are scattering or gathering) +static int stbir__get_coefficient_width(stbir__sampler * samp, int is_gather, void * user_data) +{ + float scale = samp->scale_info.scale; + stbir__support_callback * support = samp->filter_support; + + switch( is_gather ) + { + case 1: + return (int)STBIR_CEILF(support(1.0f / scale, user_data) * 2.0f); + case 2: + return (int)STBIR_CEILF(support(scale, user_data) * 2.0f / scale); + case 0: + return (int)STBIR_CEILF(support(scale, user_data) * 2.0f); + default: + STBIR_ASSERT( (is_gather >= 0 ) && (is_gather <= 2 ) ); + return 0; + } +} + +static int stbir__get_contributors(stbir__sampler * samp, int is_gather) +{ + if (is_gather) + return samp->scale_info.output_sub_size; + else + return (samp->scale_info.input_full_size + samp->filter_pixel_margin * 2); +} + +static int stbir__edge_zero_full( int n, int max ) +{ + STBIR__UNUSED(n); + STBIR__UNUSED(max); + return 0; // NOTREACHED +} + +static int stbir__edge_clamp_full( int n, int max ) +{ + if (n < 0) + return 0; + + if (n >= max) + return max - 1; + + return n; // NOTREACHED +} + +static int stbir__edge_reflect_full( int n, int max ) +{ + if (n < 0) + { + if (n > -max) + return -n; + else + return max - 1; + } + + if (n >= max) + { + int max2 = max * 2; + if (n >= max2) + return 0; + else + return max2 - n - 1; + } + + return n; // NOTREACHED +} + +static int stbir__edge_wrap_full( int n, int max ) +{ + if (n >= 0) + return (n % max); + else + { + int m = (-n) % max; + + if (m != 0) + m = max - m; + + return (m); + } +} + +typedef int stbir__edge_wrap_func( int n, int max ); +static stbir__edge_wrap_func * stbir__edge_wrap_slow[] = +{ + stbir__edge_clamp_full, // STBIR_EDGE_CLAMP + stbir__edge_reflect_full, // STBIR_EDGE_REFLECT + stbir__edge_wrap_full, // STBIR_EDGE_WRAP + stbir__edge_zero_full, // STBIR_EDGE_ZERO +}; + +stbir__inline static int stbir__edge_wrap(stbir_edge edge, int n, int max) +{ + // avoid per-pixel switch + if (n >= 0 && n < max) + return n; + return stbir__edge_wrap_slow[edge]( n, max ); +} + +#define STBIR__MERGE_RUNS_PIXEL_THRESHOLD 16 + +// get information on the extents of a sampler +static void stbir__get_extents( stbir__sampler * samp, stbir__extents * scanline_extents ) +{ + int j, stop; + int left_margin, right_margin; + int min_n = 0x7fffffff, max_n = -0x7fffffff; + int min_left = 0x7fffffff, max_left = -0x7fffffff; + int min_right = 0x7fffffff, max_right = -0x7fffffff; + stbir_edge edge = samp->edge; + stbir__contributors* contributors = samp->contributors; + int output_sub_size = samp->scale_info.output_sub_size; + int input_full_size = samp->scale_info.input_full_size; + int filter_pixel_margin = samp->filter_pixel_margin; + + STBIR_ASSERT( samp->is_gather ); + + stop = output_sub_size; + for (j = 0; j < stop; j++ ) + { + STBIR_ASSERT( contributors[j].n1 >= contributors[j].n0 ); + if ( contributors[j].n0 < min_n ) + { + min_n = contributors[j].n0; + stop = j + filter_pixel_margin; // if we find a new min, only scan another filter width + if ( stop > output_sub_size ) stop = output_sub_size; + } + } + + stop = 0; + for (j = output_sub_size - 1; j >= stop; j-- ) + { + STBIR_ASSERT( contributors[j].n1 >= contributors[j].n0 ); + if ( contributors[j].n1 > max_n ) + { + max_n = contributors[j].n1; + stop = j - filter_pixel_margin; // if we find a new max, only scan another filter width + if (stop<0) stop = 0; + } + } + + STBIR_ASSERT( scanline_extents->conservative.n0 <= min_n ); + STBIR_ASSERT( scanline_extents->conservative.n1 >= max_n ); + + // now calculate how much into the margins we really read + left_margin = 0; + if ( min_n < 0 ) + { + left_margin = -min_n; + min_n = 0; + } + + right_margin = 0; + if ( max_n >= input_full_size ) + { + right_margin = max_n - input_full_size + 1; + max_n = input_full_size - 1; + } + + // index 1 is margin pixel extents (how many pixels we hang over the edge) + scanline_extents->edge_sizes[0] = left_margin; + scanline_extents->edge_sizes[1] = right_margin; + + // index 2 is pixels read from the input + scanline_extents->spans[0].n0 = min_n; + scanline_extents->spans[0].n1 = max_n; + scanline_extents->spans[0].pixel_offset_for_input = min_n; + + // default to no other input range + scanline_extents->spans[1].n0 = 0; + scanline_extents->spans[1].n1 = -1; + scanline_extents->spans[1].pixel_offset_for_input = 0; + + // don't have to do edge calc for zero clamp + if ( edge == STBIR_EDGE_ZERO ) + return; + + // convert margin pixels to the pixels within the input (min and max) + for( j = -left_margin ; j < 0 ; j++ ) + { + int p = stbir__edge_wrap( edge, j, input_full_size ); + if ( p < min_left ) + min_left = p; + if ( p > max_left ) + max_left = p; + } + + for( j = input_full_size ; j < (input_full_size + right_margin) ; j++ ) + { + int p = stbir__edge_wrap( edge, j, input_full_size ); + if ( p < min_right ) + min_right = p; + if ( p > max_right ) + max_right = p; + } + + // merge the left margin pixel region if it connects within 4 pixels of main pixel region + if ( min_left != 0x7fffffff ) + { + if ( ( ( min_left <= min_n ) && ( ( max_left + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= min_n ) ) || + ( ( min_n <= min_left ) && ( ( max_n + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= max_left ) ) ) + { + scanline_extents->spans[0].n0 = min_n = stbir__min( min_n, min_left ); + scanline_extents->spans[0].n1 = max_n = stbir__max( max_n, max_left ); + scanline_extents->spans[0].pixel_offset_for_input = min_n; + left_margin = 0; + } + } + + // merge the right margin pixel region if it connects within 4 pixels of main pixel region + if ( min_right != 0x7fffffff ) + { + if ( ( ( min_right <= min_n ) && ( ( max_right + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= min_n ) ) || + ( ( min_n <= min_right ) && ( ( max_n + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= max_right ) ) ) + { + scanline_extents->spans[0].n0 = min_n = stbir__min( min_n, min_right ); + scanline_extents->spans[0].n1 = max_n = stbir__max( max_n, max_right ); + scanline_extents->spans[0].pixel_offset_for_input = min_n; + right_margin = 0; + } + } + + STBIR_ASSERT( scanline_extents->conservative.n0 <= min_n ); + STBIR_ASSERT( scanline_extents->conservative.n1 >= max_n ); + + // you get two ranges when you have the WRAP edge mode and you are doing just the a piece of the resize + // so you need to get a second run of pixels from the opposite side of the scanline (which you + // wouldn't need except for WRAP) + + + // if we can't merge the min_left range, add it as a second range + if ( ( left_margin ) && ( min_left != 0x7fffffff ) ) + { + stbir__span * newspan = scanline_extents->spans + 1; + STBIR_ASSERT( right_margin == 0 ); + if ( min_left < scanline_extents->spans[0].n0 ) + { + scanline_extents->spans[1].pixel_offset_for_input = scanline_extents->spans[0].n0; + scanline_extents->spans[1].n0 = scanline_extents->spans[0].n0; + scanline_extents->spans[1].n1 = scanline_extents->spans[0].n1; + --newspan; + } + newspan->pixel_offset_for_input = min_left; + newspan->n0 = -left_margin; + newspan->n1 = ( max_left - min_left ) - left_margin; + scanline_extents->edge_sizes[0] = 0; // don't need to copy the left margin, since we are directly decoding into the margin + } + // if we can't merge the min_right range, add it as a second range + else + if ( ( right_margin ) && ( min_right != 0x7fffffff ) ) + { + stbir__span * newspan = scanline_extents->spans + 1; + if ( min_right < scanline_extents->spans[0].n0 ) + { + scanline_extents->spans[1].pixel_offset_for_input = scanline_extents->spans[0].n0; + scanline_extents->spans[1].n0 = scanline_extents->spans[0].n0; + scanline_extents->spans[1].n1 = scanline_extents->spans[0].n1; + --newspan; + } + newspan->pixel_offset_for_input = min_right; + newspan->n0 = scanline_extents->spans[1].n1 + 1; + newspan->n1 = scanline_extents->spans[1].n1 + 1 + ( max_right - min_right ); + scanline_extents->edge_sizes[1] = 0; // don't need to copy the right margin, since we are directly decoding into the margin + } + + // sort the spans into write output order + if ( ( scanline_extents->spans[1].n1 > scanline_extents->spans[1].n0 ) && ( scanline_extents->spans[0].n0 > scanline_extents->spans[1].n0 ) ) + { + stbir__span tspan = scanline_extents->spans[0]; + scanline_extents->spans[0] = scanline_extents->spans[1]; + scanline_extents->spans[1] = tspan; + } +} + +static void stbir__calculate_in_pixel_range( int * first_pixel, int * last_pixel, float out_pixel_center, float out_filter_radius, float inv_scale, float out_shift, int input_size, stbir_edge edge ) +{ + int first, last; + float out_pixel_influence_lowerbound = out_pixel_center - out_filter_radius; + float out_pixel_influence_upperbound = out_pixel_center + out_filter_radius; + + float in_pixel_influence_lowerbound = (out_pixel_influence_lowerbound + out_shift) * inv_scale; + float in_pixel_influence_upperbound = (out_pixel_influence_upperbound + out_shift) * inv_scale; + + first = (int)(STBIR_FLOORF(in_pixel_influence_lowerbound + 0.5f)); + last = (int)(STBIR_FLOORF(in_pixel_influence_upperbound - 0.5f)); + if ( last < first ) last = first; // point sample mode can span a value *right* at 0.5, and cause these to cross + + if ( edge == STBIR_EDGE_WRAP ) + { + if ( first < -input_size ) + first = -input_size; + if ( last >= (input_size*2)) + last = (input_size*2) - 1; + } + + *first_pixel = first; + *last_pixel = last; +} + +static void stbir__calculate_coefficients_for_gather_upsample( float out_filter_radius, stbir__kernel_callback * kernel, stbir__scale_info * scale_info, int num_contributors, stbir__contributors* contributors, float* coefficient_group, int coefficient_width, stbir_edge edge, void * user_data ) +{ + int n, end; + float inv_scale = scale_info->inv_scale; + float out_shift = scale_info->pixel_shift; + int input_size = scale_info->input_full_size; + int numerator = scale_info->scale_numerator; + int polyphase = ( ( scale_info->scale_is_rational ) && ( numerator < num_contributors ) ); + + // Looping through out pixels + end = num_contributors; if ( polyphase ) end = numerator; + for (n = 0; n < end; n++) + { + int i; + int last_non_zero; + float out_pixel_center = (float)n + 0.5f; + float in_center_of_out = (out_pixel_center + out_shift) * inv_scale; + + int in_first_pixel, in_last_pixel; + + stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, out_pixel_center, out_filter_radius, inv_scale, out_shift, input_size, edge ); + + // make sure we never generate a range larger than our precalculated coeff width + // this only happens in point sample mode, but it's a good safe thing to do anyway + if ( ( in_last_pixel - in_first_pixel + 1 ) > coefficient_width ) + in_last_pixel = in_first_pixel + coefficient_width - 1; + + last_non_zero = -1; + for (i = 0; i <= in_last_pixel - in_first_pixel; i++) + { + float in_pixel_center = (float)(i + in_first_pixel) + 0.5f; + float coeff = kernel(in_center_of_out - in_pixel_center, inv_scale, user_data); + + // kill denormals + if ( ( ( coeff < stbir__small_float ) && ( coeff > -stbir__small_float ) ) ) + { + if ( i == 0 ) // if we're at the front, just eat zero contributors + { + STBIR_ASSERT ( ( in_last_pixel - in_first_pixel ) != 0 ); // there should be at least one contrib + ++in_first_pixel; + i--; + continue; + } + coeff = 0; // make sure is fully zero (should keep denormals away) + } + else + last_non_zero = i; + + coefficient_group[i] = coeff; + } + + in_last_pixel = last_non_zero+in_first_pixel; // kills trailing zeros + contributors->n0 = in_first_pixel; + contributors->n1 = in_last_pixel; + + STBIR_ASSERT(contributors->n1 >= contributors->n0); + + ++contributors; + coefficient_group += coefficient_width; + } +} + +static void stbir__insert_coeff( stbir__contributors * contribs, float * coeffs, int new_pixel, float new_coeff, int max_width ) +{ + if ( contribs->n1 < contribs->n0 ) // this first clause should never happen, but handle in case + { + contribs->n0 = contribs->n1 = new_pixel; + coeffs[0] = new_coeff; + } + else if ( new_pixel <= contribs->n1 ) // before the end + { + if ( new_pixel < contribs->n0 ) // before the front? + { + if ( ( contribs->n1 - new_pixel + 1 ) <= max_width ) + { + int j, o = contribs->n0 - new_pixel; + for ( j = contribs->n1 - contribs->n0 ; j >= 0 ; j-- ) + coeffs[ j + o ] = coeffs[ j ]; + for ( j = 1 ; j < o ; j++ ) + coeffs[ j ] = 0; + coeffs[ 0 ] = new_coeff; + contribs->n0 = new_pixel; + } + } + else + { + // add new weight to existing coeff if already there + coeffs[ new_pixel - contribs->n0 ] += new_coeff; + } + } + else + { + if ( ( new_pixel - contribs->n0 + 1 ) <= max_width ) + { + int j, e = new_pixel - contribs->n0; + for( j = ( contribs->n1 - contribs->n0 ) + 1 ; j < e ; j++ ) // clear in-betweens coeffs if there are any + coeffs[j] = 0; + + coeffs[ e ] = new_coeff; + contribs->n1 = new_pixel; + } + } +} + +static void stbir__calculate_out_pixel_range( int * first_pixel, int * last_pixel, float in_pixel_center, float in_pixels_radius, float scale, float out_shift, int out_size ) +{ + float in_pixel_influence_lowerbound = in_pixel_center - in_pixels_radius; + float in_pixel_influence_upperbound = in_pixel_center + in_pixels_radius; + float out_pixel_influence_lowerbound = in_pixel_influence_lowerbound * scale - out_shift; + float out_pixel_influence_upperbound = in_pixel_influence_upperbound * scale - out_shift; + int out_first_pixel = (int)(STBIR_FLOORF(out_pixel_influence_lowerbound + 0.5f)); + int out_last_pixel = (int)(STBIR_FLOORF(out_pixel_influence_upperbound - 0.5f)); + + if ( out_first_pixel < 0 ) + out_first_pixel = 0; + if ( out_last_pixel >= out_size ) + out_last_pixel = out_size - 1; + *first_pixel = out_first_pixel; + *last_pixel = out_last_pixel; +} + +static void stbir__calculate_coefficients_for_gather_downsample( int start, int end, float in_pixels_radius, stbir__kernel_callback * kernel, stbir__scale_info * scale_info, int coefficient_width, int num_contributors, stbir__contributors * contributors, float * coefficient_group, void * user_data ) +{ + int in_pixel; + int i; + int first_out_inited = -1; + float scale = scale_info->scale; + float out_shift = scale_info->pixel_shift; + int out_size = scale_info->output_sub_size; + int numerator = scale_info->scale_numerator; + int polyphase = ( ( scale_info->scale_is_rational ) && ( numerator < out_size ) ); + + STBIR__UNUSED(num_contributors); + + // Loop through the input pixels + for (in_pixel = start; in_pixel < end; in_pixel++) + { + float in_pixel_center = (float)in_pixel + 0.5f; + float out_center_of_in = in_pixel_center * scale - out_shift; + int out_first_pixel, out_last_pixel; + + stbir__calculate_out_pixel_range( &out_first_pixel, &out_last_pixel, in_pixel_center, in_pixels_radius, scale, out_shift, out_size ); + + if ( out_first_pixel > out_last_pixel ) + continue; + + // clamp or exit if we are using polyphase filtering, and the limit is up + if ( polyphase ) + { + // when polyphase, you only have to do coeffs up to the numerator count + if ( out_first_pixel == numerator ) + break; + + // don't do any extra work, clamp last pixel at numerator too + if ( out_last_pixel >= numerator ) + out_last_pixel = numerator - 1; + } + + for (i = 0; i <= out_last_pixel - out_first_pixel; i++) + { + float out_pixel_center = (float)(i + out_first_pixel) + 0.5f; + float x = out_pixel_center - out_center_of_in; + float coeff = kernel(x, scale, user_data) * scale; + + // kill the coeff if it's too small (avoid denormals) + if ( ( ( coeff < stbir__small_float ) && ( coeff > -stbir__small_float ) ) ) + coeff = 0.0f; + + { + int out = i + out_first_pixel; + float * coeffs = coefficient_group + out * coefficient_width; + stbir__contributors * contribs = contributors + out; + + // is this the first time this output pixel has been seen? Init it. + if ( out > first_out_inited ) + { + STBIR_ASSERT( out == ( first_out_inited + 1 ) ); // ensure we have only advanced one at time + first_out_inited = out; + contribs->n0 = in_pixel; + contribs->n1 = in_pixel; + coeffs[0] = coeff; + } + else + { + // insert on end (always in order) + if ( coeffs[0] == 0.0f ) // if the first coefficent is zero, then zap it for this coeffs + { + STBIR_ASSERT( ( in_pixel - contribs->n0 ) == 1 ); // ensure that when we zap, we're at the 2nd pos + contribs->n0 = in_pixel; + } + contribs->n1 = in_pixel; + STBIR_ASSERT( ( in_pixel - contribs->n0 ) < coefficient_width ); + coeffs[in_pixel - contribs->n0] = coeff; + } + } + } + } +} + +#ifdef STBIR_RENORMALIZE_IN_FLOAT +#define STBIR_RENORM_TYPE float +#else +#define STBIR_RENORM_TYPE double +#endif + +static void stbir__cleanup_gathered_coefficients( stbir_edge edge, stbir__filter_extent_info* filter_info, stbir__scale_info * scale_info, int num_contributors, stbir__contributors* contributors, float * coefficient_group, int coefficient_width ) +{ + int input_size = scale_info->input_full_size; + int input_last_n1 = input_size - 1; + int n, end; + int lowest = 0x7fffffff; + int highest = -0x7fffffff; + int widest = -1; + int numerator = scale_info->scale_numerator; + int denominator = scale_info->scale_denominator; + int polyphase = ( ( scale_info->scale_is_rational ) && ( numerator < num_contributors ) ); + float * coeffs; + stbir__contributors * contribs; + + // weight all the coeffs for each sample + coeffs = coefficient_group; + contribs = contributors; + end = num_contributors; if ( polyphase ) end = numerator; + for (n = 0; n < end; n++) + { + int i; + STBIR_RENORM_TYPE filter_scale, total_filter = 0; + int e; + + // add all contribs + e = contribs->n1 - contribs->n0; + for( i = 0 ; i <= e ; i++ ) + { + total_filter += (STBIR_RENORM_TYPE) coeffs[i]; + STBIR_ASSERT( ( coeffs[i] >= -2.0f ) && ( coeffs[i] <= 2.0f ) ); // check for wonky weights + } + + // rescale + if ( ( total_filter < stbir__small_float ) && ( total_filter > -stbir__small_float ) ) + { + // all coeffs are extremely small, just zero it + contribs->n1 = contribs->n0; + coeffs[0] = 0.0f; + } + else + { + // if the total isn't 1.0, rescale everything + if ( ( total_filter < (1.0f-stbir__small_float) ) || ( total_filter > (1.0f+stbir__small_float) ) ) + { + filter_scale = ((STBIR_RENORM_TYPE)1.0) / total_filter; + + // scale them all + for (i = 0; i <= e; i++) + coeffs[i] = (float) ( coeffs[i] * filter_scale ); + } + } + ++contribs; + coeffs += coefficient_width; + } + + // if we have a rational for the scale, we can exploit the polyphaseness to not calculate + // most of the coefficients, so we copy them here + if ( polyphase ) + { + stbir__contributors * prev_contribs = contributors; + stbir__contributors * cur_contribs = contributors + numerator; + + for( n = numerator ; n < num_contributors ; n++ ) + { + cur_contribs->n0 = prev_contribs->n0 + denominator; + cur_contribs->n1 = prev_contribs->n1 + denominator; + ++cur_contribs; + ++prev_contribs; + } + stbir_overlapping_memcpy( coefficient_group + numerator * coefficient_width, coefficient_group, ( num_contributors - numerator ) * coefficient_width * sizeof( coeffs[ 0 ] ) ); + } + + coeffs = coefficient_group; + contribs = contributors; + + for (n = 0; n < num_contributors; n++) + { + int i; + + // in zero edge mode, just remove out of bounds contribs completely (since their weights are accounted for now) + if ( edge == STBIR_EDGE_ZERO ) + { + // shrink the right side if necessary + if ( contribs->n1 > input_last_n1 ) + contribs->n1 = input_last_n1; + + // shrink the left side + if ( contribs->n0 < 0 ) + { + int j, left, skips = 0; + + skips = -contribs->n0; + contribs->n0 = 0; + + // now move down the weights + left = contribs->n1 - contribs->n0 + 1; + if ( left > 0 ) + { + for( j = 0 ; j < left ; j++ ) + coeffs[ j ] = coeffs[ j + skips ]; + } + } + } + else if ( ( edge == STBIR_EDGE_CLAMP ) || ( edge == STBIR_EDGE_REFLECT ) ) + { + // for clamp and reflect, calculate the true inbounds position (based on edge type) and just add that to the existing weight + + // right hand side first + if ( contribs->n1 > input_last_n1 ) + { + int start = contribs->n0; + int endi = contribs->n1; + contribs->n1 = input_last_n1; + for( i = input_size; i <= endi; i++ ) + stbir__insert_coeff( contribs, coeffs, stbir__edge_wrap_slow[edge]( i, input_size ), coeffs[i-start], coefficient_width ); + } + + // now check left hand edge + if ( contribs->n0 < 0 ) + { + int save_n0; + float save_n0_coeff; + float * c = coeffs - ( contribs->n0 + 1 ); + + // reinsert the coeffs with it reflected or clamped (insert accumulates, if the coeffs exist) + for( i = -1 ; i > contribs->n0 ; i-- ) + stbir__insert_coeff( contribs, coeffs, stbir__edge_wrap_slow[edge]( i, input_size ), *c--, coefficient_width ); + save_n0 = contribs->n0; + save_n0_coeff = c[0]; // save it, since we didn't do the final one (i==n0), because there might be too many coeffs to hold (before we resize)! + + // now slide all the coeffs down (since we have accumulated them in the positive contribs) and reset the first contrib + contribs->n0 = 0; + for(i = 0 ; i <= contribs->n1 ; i++ ) + coeffs[i] = coeffs[i-save_n0]; + + // now that we have shrunk down the contribs, we insert the first one safely + stbir__insert_coeff( contribs, coeffs, stbir__edge_wrap_slow[edge]( save_n0, input_size ), save_n0_coeff, coefficient_width ); + } + } + + if ( contribs->n0 <= contribs->n1 ) + { + int diff = contribs->n1 - contribs->n0 + 1; + while ( diff && ( coeffs[ diff-1 ] == 0.0f ) ) + --diff; + + contribs->n1 = contribs->n0 + diff - 1; + + if ( contribs->n0 <= contribs->n1 ) + { + if ( contribs->n0 < lowest ) + lowest = contribs->n0; + if ( contribs->n1 > highest ) + highest = contribs->n1; + if ( diff > widest ) + widest = diff; + } + + // re-zero out unused coefficients (if any) + for( i = diff ; i < coefficient_width ; i++ ) + coeffs[i] = 0.0f; + } + + ++contribs; + coeffs += coefficient_width; + } + filter_info->lowest = lowest; + filter_info->highest = highest; + filter_info->widest = widest; +} + +#undef STBIR_RENORM_TYPE + +static int stbir__pack_coefficients( int num_contributors, stbir__contributors* contributors, float * coefficents, int coefficient_width, int widest, int row0, int row1 ) +{ + #define STBIR_MOVE_1( dest, src ) { STBIR_NO_UNROLL(dest); ((stbir_uint32*)(dest))[0] = ((stbir_uint32*)(src))[0]; } + #define STBIR_MOVE_2( dest, src ) { STBIR_NO_UNROLL(dest); ((stbir_uint64*)(dest))[0] = ((stbir_uint64*)(src))[0]; } + #ifdef STBIR_SIMD + #define STBIR_MOVE_4( dest, src ) { stbir__simdf t; STBIR_NO_UNROLL(dest); stbir__simdf_load( t, src ); stbir__simdf_store( dest, t ); } + #else + #define STBIR_MOVE_4( dest, src ) { STBIR_NO_UNROLL(dest); ((stbir_uint64*)(dest))[0] = ((stbir_uint64*)(src))[0]; ((stbir_uint64*)(dest))[1] = ((stbir_uint64*)(src))[1]; } + #endif + + int row_end = row1 + 1; + STBIR__UNUSED( row0 ); // only used in an assert + + if ( coefficient_width != widest ) + { + float * pc = coefficents; + float * coeffs = coefficents; + float * pc_end = coefficents + num_contributors * widest; + switch( widest ) + { + case 1: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_1( pc, coeffs ); + ++pc; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 2: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_2( pc, coeffs ); + pc += 2; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 3: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_2( pc, coeffs ); + STBIR_MOVE_1( pc+2, coeffs+2 ); + pc += 3; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 4: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_4( pc, coeffs ); + pc += 4; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 5: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_4( pc, coeffs ); + STBIR_MOVE_1( pc+4, coeffs+4 ); + pc += 5; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 6: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_4( pc, coeffs ); + STBIR_MOVE_2( pc+4, coeffs+4 ); + pc += 6; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 7: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_4( pc, coeffs ); + STBIR_MOVE_2( pc+4, coeffs+4 ); + STBIR_MOVE_1( pc+6, coeffs+6 ); + pc += 7; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 8: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_4( pc, coeffs ); + STBIR_MOVE_4( pc+4, coeffs+4 ); + pc += 8; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 9: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_4( pc, coeffs ); + STBIR_MOVE_4( pc+4, coeffs+4 ); + STBIR_MOVE_1( pc+8, coeffs+8 ); + pc += 9; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 10: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_4( pc, coeffs ); + STBIR_MOVE_4( pc+4, coeffs+4 ); + STBIR_MOVE_2( pc+8, coeffs+8 ); + pc += 10; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 11: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_4( pc, coeffs ); + STBIR_MOVE_4( pc+4, coeffs+4 ); + STBIR_MOVE_2( pc+8, coeffs+8 ); + STBIR_MOVE_1( pc+10, coeffs+10 ); + pc += 11; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + case 12: + STBIR_NO_UNROLL_LOOP_START + do { + STBIR_MOVE_4( pc, coeffs ); + STBIR_MOVE_4( pc+4, coeffs+4 ); + STBIR_MOVE_4( pc+8, coeffs+8 ); + pc += 12; + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + default: + STBIR_NO_UNROLL_LOOP_START + do { + float * copy_end = pc + widest - 4; + float * c = coeffs; + do { + STBIR_NO_UNROLL( pc ); + STBIR_MOVE_4( pc, c ); + pc += 4; + c += 4; + } while ( pc <= copy_end ); + copy_end += 4; + STBIR_NO_UNROLL_LOOP_START + while ( pc < copy_end ) + { + STBIR_MOVE_1( pc, c ); + ++pc; ++c; + } + coeffs += coefficient_width; + } while ( pc < pc_end ); + break; + } + } + + // some horizontal routines read one float off the end (which is then masked off), so put in a sentinel so we don't read an snan or denormal + coefficents[ widest * num_contributors ] = 8888.0f; + + // the minimum we might read for unrolled filters widths is 12. So, we need to + // make sure we never read outside the decode buffer, by possibly moving + // the sample area back into the scanline, and putting zeros weights first. + // we start on the right edge and check until we're well past the possible + // clip area (2*widest). + { + stbir__contributors * contribs = contributors + num_contributors - 1; + float * coeffs = coefficents + widest * ( num_contributors - 1 ); + + // go until no chance of clipping (this is usually less than 8 lops) + while ( ( contribs >= contributors ) && ( ( contribs->n0 + widest*2 ) >= row_end ) ) + { + // might we clip?? + if ( ( contribs->n0 + widest ) > row_end ) + { + int stop_range = widest; + + // if range is larger than 12, it will be handled by generic loops that can terminate on the exact length + // of this contrib n1, instead of a fixed widest amount - so calculate this + if ( widest > 12 ) + { + int mod; + + // how far will be read in the n_coeff loop (which depends on the widest count mod4); + mod = widest & 3; + stop_range = ( ( ( contribs->n1 - contribs->n0 + 1 ) - mod + 3 ) & ~3 ) + mod; + + // the n_coeff loops do a minimum amount of coeffs, so factor that in! + if ( stop_range < ( 8 + mod ) ) stop_range = 8 + mod; + } + + // now see if we still clip with the refined range + if ( ( contribs->n0 + stop_range ) > row_end ) + { + int new_n0 = row_end - stop_range; + int num = contribs->n1 - contribs->n0 + 1; + int backup = contribs->n0 - new_n0; + float * from_co = coeffs + num - 1; + float * to_co = from_co + backup; + + STBIR_ASSERT( ( new_n0 >= row0 ) && ( new_n0 < contribs->n0 ) ); + + // move the coeffs over + while( num ) + { + *to_co-- = *from_co--; + --num; + } + // zero new positions + while ( to_co >= coeffs ) + *to_co-- = 0; + // set new start point + contribs->n0 = new_n0; + if ( widest > 12 ) + { + int mod; + + // how far will be read in the n_coeff loop (which depends on the widest count mod4); + mod = widest & 3; + stop_range = ( ( ( contribs->n1 - contribs->n0 + 1 ) - mod + 3 ) & ~3 ) + mod; + + // the n_coeff loops do a minimum amount of coeffs, so factor that in! + if ( stop_range < ( 8 + mod ) ) stop_range = 8 + mod; + } + } + } + --contribs; + coeffs -= widest; + } + } + + return widest; + #undef STBIR_MOVE_1 + #undef STBIR_MOVE_2 + #undef STBIR_MOVE_4 +} + +static void stbir__calculate_filters( stbir__sampler * samp, stbir__sampler * other_axis_for_pivot, void * user_data STBIR_ONLY_PROFILE_BUILD_GET_INFO ) +{ + int n; + float scale = samp->scale_info.scale; + stbir__kernel_callback * kernel = samp->filter_kernel; + stbir__support_callback * support = samp->filter_support; + float inv_scale = samp->scale_info.inv_scale; + int input_full_size = samp->scale_info.input_full_size; + int gather_num_contributors = samp->num_contributors; + stbir__contributors* gather_contributors = samp->contributors; + float * gather_coeffs = samp->coefficients; + int gather_coefficient_width = samp->coefficient_width; + + switch ( samp->is_gather ) + { + case 1: // gather upsample + { + float out_pixels_radius = support(inv_scale,user_data) * scale; + + stbir__calculate_coefficients_for_gather_upsample( out_pixels_radius, kernel, &samp->scale_info, gather_num_contributors, gather_contributors, gather_coeffs, gather_coefficient_width, samp->edge, user_data ); + + STBIR_PROFILE_BUILD_START( cleanup ); + stbir__cleanup_gathered_coefficients( samp->edge, &samp->extent_info, &samp->scale_info, gather_num_contributors, gather_contributors, gather_coeffs, gather_coefficient_width ); + STBIR_PROFILE_BUILD_END( cleanup ); + } + break; + + case 0: // scatter downsample (only on vertical) + case 2: // gather downsample + { + float in_pixels_radius = support(scale,user_data) * inv_scale; + int filter_pixel_margin = samp->filter_pixel_margin; + int input_end = input_full_size + filter_pixel_margin; + + // if this is a scatter, we do a downsample gather to get the coeffs, and then pivot after + if ( !samp->is_gather ) + { + // check if we are using the same gather downsample on the horizontal as this vertical, + // if so, then we don't have to generate them, we can just pivot from the horizontal. + if ( other_axis_for_pivot ) + { + gather_contributors = other_axis_for_pivot->contributors; + gather_coeffs = other_axis_for_pivot->coefficients; + gather_coefficient_width = other_axis_for_pivot->coefficient_width; + gather_num_contributors = other_axis_for_pivot->num_contributors; + samp->extent_info.lowest = other_axis_for_pivot->extent_info.lowest; + samp->extent_info.highest = other_axis_for_pivot->extent_info.highest; + samp->extent_info.widest = other_axis_for_pivot->extent_info.widest; + goto jump_right_to_pivot; + } + + gather_contributors = samp->gather_prescatter_contributors; + gather_coeffs = samp->gather_prescatter_coefficients; + gather_coefficient_width = samp->gather_prescatter_coefficient_width; + gather_num_contributors = samp->gather_prescatter_num_contributors; + } + + stbir__calculate_coefficients_for_gather_downsample( -filter_pixel_margin, input_end, in_pixels_radius, kernel, &samp->scale_info, gather_coefficient_width, gather_num_contributors, gather_contributors, gather_coeffs, user_data ); + + STBIR_PROFILE_BUILD_START( cleanup ); + stbir__cleanup_gathered_coefficients( samp->edge, &samp->extent_info, &samp->scale_info, gather_num_contributors, gather_contributors, gather_coeffs, gather_coefficient_width ); + STBIR_PROFILE_BUILD_END( cleanup ); + + if ( !samp->is_gather ) + { + // if this is a scatter (vertical only), then we need to pivot the coeffs + stbir__contributors * scatter_contributors; + int highest_set; + + jump_right_to_pivot: + + STBIR_PROFILE_BUILD_START( pivot ); + + highest_set = (-filter_pixel_margin) - 1; + for (n = 0; n < gather_num_contributors; n++) + { + int k; + int gn0 = gather_contributors->n0, gn1 = gather_contributors->n1; + int scatter_coefficient_width = samp->coefficient_width; + float * scatter_coeffs = samp->coefficients + ( gn0 + filter_pixel_margin ) * scatter_coefficient_width; + float * g_coeffs = gather_coeffs; + scatter_contributors = samp->contributors + ( gn0 + filter_pixel_margin ); + + for (k = gn0 ; k <= gn1 ; k++ ) + { + float gc = *g_coeffs++; + + // skip zero and denormals - must skip zeros to avoid adding coeffs beyond scatter_coefficient_width + // (which happens when pivoting from horizontal, which might have dummy zeros) + if ( ( ( gc >= stbir__small_float ) || ( gc <= -stbir__small_float ) ) ) + { + if ( ( k > highest_set ) || ( scatter_contributors->n0 > scatter_contributors->n1 ) ) + { + { + // if we are skipping over several contributors, we need to clear the skipped ones + stbir__contributors * clear_contributors = samp->contributors + ( highest_set + filter_pixel_margin + 1); + while ( clear_contributors < scatter_contributors ) + { + clear_contributors->n0 = 0; + clear_contributors->n1 = -1; + ++clear_contributors; + } + } + scatter_contributors->n0 = n; + scatter_contributors->n1 = n; + scatter_coeffs[0] = gc; + highest_set = k; + } + else + { + stbir__insert_coeff( scatter_contributors, scatter_coeffs, n, gc, scatter_coefficient_width ); + } + STBIR_ASSERT( ( scatter_contributors->n1 - scatter_contributors->n0 + 1 ) <= scatter_coefficient_width ); + } + ++scatter_contributors; + scatter_coeffs += scatter_coefficient_width; + } + + ++gather_contributors; + gather_coeffs += gather_coefficient_width; + } + + // now clear any unset contribs + { + stbir__contributors * clear_contributors = samp->contributors + ( highest_set + filter_pixel_margin + 1); + stbir__contributors * end_contributors = samp->contributors + samp->num_contributors; + while ( clear_contributors < end_contributors ) + { + clear_contributors->n0 = 0; + clear_contributors->n1 = -1; + ++clear_contributors; + } + } + + STBIR_PROFILE_BUILD_END( pivot ); + } + } + break; + } +} + + +//======================================================================================================== +// scanline decoders and encoders + +#define stbir__coder_min_num 1 +#define STB_IMAGE_RESIZE_DO_CODERS +#include STBIR__HEADER_FILENAME + +#define stbir__decode_suffix BGRA +#define stbir__decode_swizzle +#define stbir__decode_order0 2 +#define stbir__decode_order1 1 +#define stbir__decode_order2 0 +#define stbir__decode_order3 3 +#define stbir__encode_order0 2 +#define stbir__encode_order1 1 +#define stbir__encode_order2 0 +#define stbir__encode_order3 3 +#define stbir__coder_min_num 4 +#define STB_IMAGE_RESIZE_DO_CODERS +#include STBIR__HEADER_FILENAME + +#define stbir__decode_suffix ARGB +#define stbir__decode_swizzle +#define stbir__decode_order0 1 +#define stbir__decode_order1 2 +#define stbir__decode_order2 3 +#define stbir__decode_order3 0 +#define stbir__encode_order0 3 +#define stbir__encode_order1 0 +#define stbir__encode_order2 1 +#define stbir__encode_order3 2 +#define stbir__coder_min_num 4 +#define STB_IMAGE_RESIZE_DO_CODERS +#include STBIR__HEADER_FILENAME + +#define stbir__decode_suffix ABGR +#define stbir__decode_swizzle +#define stbir__decode_order0 3 +#define stbir__decode_order1 2 +#define stbir__decode_order2 1 +#define stbir__decode_order3 0 +#define stbir__encode_order0 3 +#define stbir__encode_order1 2 +#define stbir__encode_order2 1 +#define stbir__encode_order3 0 +#define stbir__coder_min_num 4 +#define STB_IMAGE_RESIZE_DO_CODERS +#include STBIR__HEADER_FILENAME + +#define stbir__decode_suffix AR +#define stbir__decode_swizzle +#define stbir__decode_order0 1 +#define stbir__decode_order1 0 +#define stbir__decode_order2 3 +#define stbir__decode_order3 2 +#define stbir__encode_order0 1 +#define stbir__encode_order1 0 +#define stbir__encode_order2 3 +#define stbir__encode_order3 2 +#define stbir__coder_min_num 2 +#define STB_IMAGE_RESIZE_DO_CODERS +#include STBIR__HEADER_FILENAME + + +// fancy alpha means we expand to keep both premultipied and non-premultiplied color channels +static void stbir__fancy_alpha_weight_4ch( float * out_buffer, int width_times_channels ) +{ + float STBIR_STREAMOUT_PTR(*) out = out_buffer; + float const * end_decode = out_buffer + ( width_times_channels / 4 ) * 7; // decode buffer aligned to end of out_buffer + float STBIR_STREAMOUT_PTR(*) decode = (float*)end_decode - width_times_channels; + + // fancy alpha is stored internally as R G B A Rpm Gpm Bpm + + #ifdef STBIR_SIMD + + #ifdef STBIR_SIMD8 + decode += 16; + STBIR_NO_UNROLL_LOOP_START + while ( decode <= end_decode ) + { + stbir__simdf8 d0,d1,a0,a1,p0,p1; + STBIR_NO_UNROLL(decode); + stbir__simdf8_load( d0, decode-16 ); + stbir__simdf8_load( d1, decode-16+8 ); + stbir__simdf8_0123to33333333( a0, d0 ); + stbir__simdf8_0123to33333333( a1, d1 ); + stbir__simdf8_mult( p0, a0, d0 ); + stbir__simdf8_mult( p1, a1, d1 ); + stbir__simdf8_bot4s( a0, d0, p0 ); + stbir__simdf8_bot4s( a1, d1, p1 ); + stbir__simdf8_top4s( d0, d0, p0 ); + stbir__simdf8_top4s( d1, d1, p1 ); + stbir__simdf8_store ( out, a0 ); + stbir__simdf8_store ( out+7, d0 ); + stbir__simdf8_store ( out+14, a1 ); + stbir__simdf8_store ( out+21, d1 ); + decode += 16; + out += 28; + } + decode -= 16; + #else + decode += 8; + STBIR_NO_UNROLL_LOOP_START + while ( decode <= end_decode ) + { + stbir__simdf d0,a0,d1,a1,p0,p1; + STBIR_NO_UNROLL(decode); + stbir__simdf_load( d0, decode-8 ); + stbir__simdf_load( d1, decode-8+4 ); + stbir__simdf_0123to3333( a0, d0 ); + stbir__simdf_0123to3333( a1, d1 ); + stbir__simdf_mult( p0, a0, d0 ); + stbir__simdf_mult( p1, a1, d1 ); + stbir__simdf_store ( out, d0 ); + stbir__simdf_store ( out+4, p0 ); + stbir__simdf_store ( out+7, d1 ); + stbir__simdf_store ( out+7+4, p1 ); + decode += 8; + out += 14; + } + decode -= 8; + #endif + + // might be one last odd pixel + #ifdef STBIR_SIMD8 + STBIR_NO_UNROLL_LOOP_START + while ( decode < end_decode ) + #else + if ( decode < end_decode ) + #endif + { + stbir__simdf d,a,p; + STBIR_NO_UNROLL(decode); + stbir__simdf_load( d, decode ); + stbir__simdf_0123to3333( a, d ); + stbir__simdf_mult( p, a, d ); + stbir__simdf_store ( out, d ); + stbir__simdf_store ( out+4, p ); + decode += 4; + out += 7; + } + + #else + + while( decode < end_decode ) + { + float r = decode[0], g = decode[1], b = decode[2], alpha = decode[3]; + out[0] = r; + out[1] = g; + out[2] = b; + out[3] = alpha; + out[4] = r * alpha; + out[5] = g * alpha; + out[6] = b * alpha; + out += 7; + decode += 4; + } + + #endif +} + +static void stbir__fancy_alpha_weight_2ch( float * out_buffer, int width_times_channels ) +{ + float STBIR_STREAMOUT_PTR(*) out = out_buffer; + float const * end_decode = out_buffer + ( width_times_channels / 2 ) * 3; + float STBIR_STREAMOUT_PTR(*) decode = (float*)end_decode - width_times_channels; + + // for fancy alpha, turns into: [X A Xpm][X A Xpm],etc + + #ifdef STBIR_SIMD + + decode += 8; + if ( decode <= end_decode ) + { + STBIR_NO_UNROLL_LOOP_START + do { + #ifdef STBIR_SIMD8 + stbir__simdf8 d0,a0,p0; + STBIR_NO_UNROLL(decode); + stbir__simdf8_load( d0, decode-8 ); + stbir__simdf8_0123to11331133( p0, d0 ); + stbir__simdf8_0123to00220022( a0, d0 ); + stbir__simdf8_mult( p0, p0, a0 ); + + stbir__simdf_store2( out, stbir__if_simdf8_cast_to_simdf4( d0 ) ); + stbir__simdf_store( out+2, stbir__if_simdf8_cast_to_simdf4( p0 ) ); + stbir__simdf_store2h( out+3, stbir__if_simdf8_cast_to_simdf4( d0 ) ); + + stbir__simdf_store2( out+6, stbir__simdf8_gettop4( d0 ) ); + stbir__simdf_store( out+8, stbir__simdf8_gettop4( p0 ) ); + stbir__simdf_store2h( out+9, stbir__simdf8_gettop4( d0 ) ); + #else + stbir__simdf d0,a0,d1,a1,p0,p1; + STBIR_NO_UNROLL(decode); + stbir__simdf_load( d0, decode-8 ); + stbir__simdf_load( d1, decode-8+4 ); + stbir__simdf_0123to1133( p0, d0 ); + stbir__simdf_0123to1133( p1, d1 ); + stbir__simdf_0123to0022( a0, d0 ); + stbir__simdf_0123to0022( a1, d1 ); + stbir__simdf_mult( p0, p0, a0 ); + stbir__simdf_mult( p1, p1, a1 ); + + stbir__simdf_store2( out, d0 ); + stbir__simdf_store( out+2, p0 ); + stbir__simdf_store2h( out+3, d0 ); + + stbir__simdf_store2( out+6, d1 ); + stbir__simdf_store( out+8, p1 ); + stbir__simdf_store2h( out+9, d1 ); + #endif + decode += 8; + out += 12; + } while ( decode <= end_decode ); + } + decode -= 8; + #endif + + STBIR_SIMD_NO_UNROLL_LOOP_START + while( decode < end_decode ) + { + float x = decode[0], y = decode[1]; + STBIR_SIMD_NO_UNROLL(decode); + out[0] = x; + out[1] = y; + out[2] = x * y; + out += 3; + decode += 2; + } +} + +static void stbir__fancy_alpha_unweight_4ch( float * encode_buffer, int width_times_channels ) +{ + float STBIR_SIMD_STREAMOUT_PTR(*) encode = encode_buffer; + float STBIR_SIMD_STREAMOUT_PTR(*) input = encode_buffer; + float const * end_output = encode_buffer + width_times_channels; + + // fancy RGBA is stored internally as R G B A Rpm Gpm Bpm + + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float alpha = input[3]; +#ifdef STBIR_SIMD + stbir__simdf i,ia; + STBIR_SIMD_NO_UNROLL(encode); + if ( alpha < stbir__small_float ) + { + stbir__simdf_load( i, input ); + stbir__simdf_store( encode, i ); + } + else + { + stbir__simdf_load1frep4( ia, 1.0f / alpha ); + stbir__simdf_load( i, input+4 ); + stbir__simdf_mult( i, i, ia ); + stbir__simdf_store( encode, i ); + encode[3] = alpha; + } +#else + if ( alpha < stbir__small_float ) + { + encode[0] = input[0]; + encode[1] = input[1]; + encode[2] = input[2]; + } + else + { + float ialpha = 1.0f / alpha; + encode[0] = input[4] * ialpha; + encode[1] = input[5] * ialpha; + encode[2] = input[6] * ialpha; + } + encode[3] = alpha; +#endif + + input += 7; + encode += 4; + } while ( encode < end_output ); +} + +// format: [X A Xpm][X A Xpm] etc +static void stbir__fancy_alpha_unweight_2ch( float * encode_buffer, int width_times_channels ) +{ + float STBIR_SIMD_STREAMOUT_PTR(*) encode = encode_buffer; + float STBIR_SIMD_STREAMOUT_PTR(*) input = encode_buffer; + float const * end_output = encode_buffer + width_times_channels; + + do { + float alpha = input[1]; + encode[0] = input[0]; + if ( alpha >= stbir__small_float ) + encode[0] = input[2] / alpha; + encode[1] = alpha; + + input += 3; + encode += 2; + } while ( encode < end_output ); +} + +static void stbir__simple_alpha_weight_4ch( float * decode_buffer, int width_times_channels ) +{ + float STBIR_STREAMOUT_PTR(*) decode = decode_buffer; + float const * end_decode = decode_buffer + width_times_channels; + + #ifdef STBIR_SIMD + { + decode += 2 * stbir__simdfX_float_count; + STBIR_NO_UNROLL_LOOP_START + while ( decode <= end_decode ) + { + stbir__simdfX d0,a0,d1,a1; + STBIR_NO_UNROLL(decode); + stbir__simdfX_load( d0, decode-2*stbir__simdfX_float_count ); + stbir__simdfX_load( d1, decode-2*stbir__simdfX_float_count+stbir__simdfX_float_count ); + stbir__simdfX_aaa1( a0, d0, STBIR_onesX ); + stbir__simdfX_aaa1( a1, d1, STBIR_onesX ); + stbir__simdfX_mult( d0, d0, a0 ); + stbir__simdfX_mult( d1, d1, a1 ); + stbir__simdfX_store ( decode-2*stbir__simdfX_float_count, d0 ); + stbir__simdfX_store ( decode-2*stbir__simdfX_float_count+stbir__simdfX_float_count, d1 ); + decode += 2 * stbir__simdfX_float_count; + } + decode -= 2 * stbir__simdfX_float_count; + + // few last pixels remnants + #ifdef STBIR_SIMD8 + STBIR_NO_UNROLL_LOOP_START + while ( decode < end_decode ) + #else + if ( decode < end_decode ) + #endif + { + stbir__simdf d,a; + stbir__simdf_load( d, decode ); + stbir__simdf_aaa1( a, d, STBIR__CONSTF(STBIR_ones) ); + stbir__simdf_mult( d, d, a ); + stbir__simdf_store ( decode, d ); + decode += 4; + } + } + + #else + + while( decode < end_decode ) + { + float alpha = decode[3]; + decode[0] *= alpha; + decode[1] *= alpha; + decode[2] *= alpha; + decode += 4; + } + + #endif +} + +static void stbir__simple_alpha_weight_2ch( float * decode_buffer, int width_times_channels ) +{ + float STBIR_STREAMOUT_PTR(*) decode = decode_buffer; + float const * end_decode = decode_buffer + width_times_channels; + + #ifdef STBIR_SIMD + decode += 2 * stbir__simdfX_float_count; + STBIR_NO_UNROLL_LOOP_START + while ( decode <= end_decode ) + { + stbir__simdfX d0,a0,d1,a1; + STBIR_NO_UNROLL(decode); + stbir__simdfX_load( d0, decode-2*stbir__simdfX_float_count ); + stbir__simdfX_load( d1, decode-2*stbir__simdfX_float_count+stbir__simdfX_float_count ); + stbir__simdfX_a1a1( a0, d0, STBIR_onesX ); + stbir__simdfX_a1a1( a1, d1, STBIR_onesX ); + stbir__simdfX_mult( d0, d0, a0 ); + stbir__simdfX_mult( d1, d1, a1 ); + stbir__simdfX_store ( decode-2*stbir__simdfX_float_count, d0 ); + stbir__simdfX_store ( decode-2*stbir__simdfX_float_count+stbir__simdfX_float_count, d1 ); + decode += 2 * stbir__simdfX_float_count; + } + decode -= 2 * stbir__simdfX_float_count; + #endif + + STBIR_SIMD_NO_UNROLL_LOOP_START + while( decode < end_decode ) + { + float alpha = decode[1]; + STBIR_SIMD_NO_UNROLL(decode); + decode[0] *= alpha; + decode += 2; + } +} + +static void stbir__simple_alpha_unweight_4ch( float * encode_buffer, int width_times_channels ) +{ + float STBIR_SIMD_STREAMOUT_PTR(*) encode = encode_buffer; + float const * end_output = encode_buffer + width_times_channels; + + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float alpha = encode[3]; + +#ifdef STBIR_SIMD + stbir__simdf i,ia; + STBIR_SIMD_NO_UNROLL(encode); + if ( alpha >= stbir__small_float ) + { + stbir__simdf_load1frep4( ia, 1.0f / alpha ); + stbir__simdf_load( i, encode ); + stbir__simdf_mult( i, i, ia ); + stbir__simdf_store( encode, i ); + encode[3] = alpha; + } +#else + if ( alpha >= stbir__small_float ) + { + float ialpha = 1.0f / alpha; + encode[0] *= ialpha; + encode[1] *= ialpha; + encode[2] *= ialpha; + } +#endif + encode += 4; + } while ( encode < end_output ); +} + +static void stbir__simple_alpha_unweight_2ch( float * encode_buffer, int width_times_channels ) +{ + float STBIR_SIMD_STREAMOUT_PTR(*) encode = encode_buffer; + float const * end_output = encode_buffer + width_times_channels; + + do { + float alpha = encode[1]; + if ( alpha >= stbir__small_float ) + encode[0] /= alpha; + encode += 2; + } while ( encode < end_output ); +} + + +// only used in RGB->BGR or BGR->RGB +static void stbir__simple_flip_3ch( float * decode_buffer, int width_times_channels ) +{ + float STBIR_STREAMOUT_PTR(*) decode = decode_buffer; + float const * end_decode = decode_buffer + width_times_channels; + +#ifdef STBIR_SIMD + #ifdef stbir__simdf_swiz2 // do we have two argument swizzles? + end_decode -= 12; + STBIR_NO_UNROLL_LOOP_START + while( decode <= end_decode ) + { + // on arm64 8 instructions, no overlapping stores + stbir__simdf a,b,c,na,nb; + STBIR_SIMD_NO_UNROLL(decode); + stbir__simdf_load( a, decode ); + stbir__simdf_load( b, decode+4 ); + stbir__simdf_load( c, decode+8 ); + + na = stbir__simdf_swiz2( a, b, 2, 1, 0, 5 ); + b = stbir__simdf_swiz2( a, b, 4, 3, 6, 7 ); + nb = stbir__simdf_swiz2( b, c, 0, 1, 4, 3 ); + c = stbir__simdf_swiz2( b, c, 2, 7, 6, 5 ); + + stbir__simdf_store( decode, na ); + stbir__simdf_store( decode+4, nb ); + stbir__simdf_store( decode+8, c ); + decode += 12; + } + end_decode += 12; + #else + end_decode -= 24; + STBIR_NO_UNROLL_LOOP_START + while( decode <= end_decode ) + { + // 26 instructions on x64 + stbir__simdf a,b,c,d,e,f,g; + float i21, i23; + STBIR_SIMD_NO_UNROLL(decode); + stbir__simdf_load( a, decode ); + stbir__simdf_load( b, decode+3 ); + stbir__simdf_load( c, decode+6 ); + stbir__simdf_load( d, decode+9 ); + stbir__simdf_load( e, decode+12 ); + stbir__simdf_load( f, decode+15 ); + stbir__simdf_load( g, decode+18 ); + + a = stbir__simdf_swiz( a, 2, 1, 0, 3 ); + b = stbir__simdf_swiz( b, 2, 1, 0, 3 ); + c = stbir__simdf_swiz( c, 2, 1, 0, 3 ); + d = stbir__simdf_swiz( d, 2, 1, 0, 3 ); + e = stbir__simdf_swiz( e, 2, 1, 0, 3 ); + f = stbir__simdf_swiz( f, 2, 1, 0, 3 ); + g = stbir__simdf_swiz( g, 2, 1, 0, 3 ); + + // stores overlap, need to be in order, + stbir__simdf_store( decode, a ); + i21 = decode[21]; + stbir__simdf_store( decode+3, b ); + i23 = decode[23]; + stbir__simdf_store( decode+6, c ); + stbir__simdf_store( decode+9, d ); + stbir__simdf_store( decode+12, e ); + stbir__simdf_store( decode+15, f ); + stbir__simdf_store( decode+18, g ); + decode[21] = i23; + decode[23] = i21; + decode += 24; + } + end_decode += 24; + #endif +#else + end_decode -= 12; + STBIR_NO_UNROLL_LOOP_START + while( decode <= end_decode ) + { + // 16 instructions + float t0,t1,t2,t3; + STBIR_NO_UNROLL(decode); + t0 = decode[0]; t1 = decode[3]; t2 = decode[6]; t3 = decode[9]; + decode[0] = decode[2]; decode[3] = decode[5]; decode[6] = decode[8]; decode[9] = decode[11]; + decode[2] = t0; decode[5] = t1; decode[8] = t2; decode[11] = t3; + decode += 12; + } + end_decode += 12; +#endif + + STBIR_NO_UNROLL_LOOP_START + while( decode < end_decode ) + { + float t = decode[0]; + STBIR_NO_UNROLL(decode); + decode[0] = decode[2]; + decode[2] = t; + decode += 3; + } +} + + + +static void stbir__decode_scanline(stbir__info const * stbir_info, int n, float * output_buffer STBIR_ONLY_PROFILE_GET_SPLIT_INFO ) +{ + int channels = stbir_info->channels; + int effective_channels = stbir_info->effective_channels; + int input_sample_in_bytes = stbir__type_size[stbir_info->input_type] * channels; + stbir_edge edge_horizontal = stbir_info->horizontal.edge; + stbir_edge edge_vertical = stbir_info->vertical.edge; + int row = stbir__edge_wrap(edge_vertical, n, stbir_info->vertical.scale_info.input_full_size); + const void* input_plane_data = ( (char *) stbir_info->input_data ) + (size_t)row * (size_t) stbir_info->input_stride_bytes; + stbir__span const * spans = stbir_info->scanline_extents.spans; + float * full_decode_buffer = output_buffer - stbir_info->scanline_extents.conservative.n0 * effective_channels; + float * last_decoded = 0; + + // if we are on edge_zero, and we get in here with an out of bounds n, then the calculate filters has failed + STBIR_ASSERT( !(edge_vertical == STBIR_EDGE_ZERO && (n < 0 || n >= stbir_info->vertical.scale_info.input_full_size)) ); + + do + { + float * decode_buffer; + void const * input_data; + float * end_decode; + int width_times_channels; + int width; + + if ( spans->n1 < spans->n0 ) + break; + + width = spans->n1 + 1 - spans->n0; + decode_buffer = full_decode_buffer + spans->n0 * effective_channels; + end_decode = full_decode_buffer + ( spans->n1 + 1 ) * effective_channels; + width_times_channels = width * channels; + + // read directly out of input plane by default + input_data = ( (char*)input_plane_data ) + spans->pixel_offset_for_input * input_sample_in_bytes; + + // if we have an input callback, call it to get the input data + if ( stbir_info->in_pixels_cb ) + { + // call the callback with a temp buffer (that they can choose to use or not). the temp is just right aligned memory in the decode_buffer itself + input_data = stbir_info->in_pixels_cb( ( (char*) end_decode ) - ( width * input_sample_in_bytes ) + ( ( stbir_info->input_type != STBIR_TYPE_FLOAT ) ? ( sizeof(float)*STBIR_INPUT_CALLBACK_PADDING ) : 0 ), input_plane_data, width, spans->pixel_offset_for_input, row, stbir_info->user_data ); + } + + STBIR_PROFILE_START( decode ); + // convert the pixels info the float decode_buffer, (we index from end_decode, so that when channels<effective_channels, we are right justified in the buffer) + last_decoded = stbir_info->decode_pixels( (float*)end_decode - width_times_channels, width_times_channels, input_data ); + STBIR_PROFILE_END( decode ); + + if (stbir_info->alpha_weight) + { + STBIR_PROFILE_START( alpha ); + stbir_info->alpha_weight( decode_buffer, width_times_channels ); + STBIR_PROFILE_END( alpha ); + } + + ++spans; + } while ( spans <= ( &stbir_info->scanline_extents.spans[1] ) ); + + // handle the edge_wrap filter (all other types are handled back out at the calculate_filter stage) + // basically the idea here is that if we have the whole scanline in memory, we don't redecode the + // wrapped edge pixels, and instead just memcpy them from the scanline into the edge positions + if ( ( edge_horizontal == STBIR_EDGE_WRAP ) && ( stbir_info->scanline_extents.edge_sizes[0] | stbir_info->scanline_extents.edge_sizes[1] ) ) + { + // this code only runs if we're in edge_wrap, and we're doing the entire scanline + int e, start_x[2]; + int input_full_size = stbir_info->horizontal.scale_info.input_full_size; + + start_x[0] = -stbir_info->scanline_extents.edge_sizes[0]; // left edge start x + start_x[1] = input_full_size; // right edge + + for( e = 0; e < 2 ; e++ ) + { + // do each margin + int margin = stbir_info->scanline_extents.edge_sizes[e]; + if ( margin ) + { + int x = start_x[e]; + float * marg = full_decode_buffer + x * effective_channels; + float const * src = full_decode_buffer + stbir__edge_wrap(edge_horizontal, x, input_full_size) * effective_channels; + STBIR_MEMCPY( marg, src, margin * effective_channels * sizeof(float) ); + if ( e == 1 ) last_decoded = marg + margin * effective_channels; + } + } + } + + // some of the horizontal gathers read one float off the edge (which is masked out), but we force a zero here to make sure no NaNs leak in + // (we can't pre-zero it, because the input callback can use that area as padding) + last_decoded[0] = 0.0f; + + // we clear this extra float, because the final output pixel filter kernel might have used one less coeff than the max filter width + // when this happens, we do read that pixel from the input, so it too could be Nan, so just zero an extra one. + // this fits because each scanline is padded by three floats (STBIR_INPUT_CALLBACK_PADDING) + last_decoded[1] = 0.0f; +} + + +//================= +// Do 1 channel horizontal routines + +#ifdef STBIR_SIMD + +#define stbir__1_coeff_only() \ + stbir__simdf tot,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1( c, hc ); \ + stbir__simdf_mult1_mem( tot, c, decode ); + +#define stbir__2_coeff_only() \ + stbir__simdf tot,c,d; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load2z( c, hc ); \ + stbir__simdf_load2( d, decode ); \ + stbir__simdf_mult( tot, c, d ); \ + stbir__simdf_0123to1230( c, tot ); \ + stbir__simdf_add1( tot, tot, c ); + +#define stbir__3_coeff_only() \ + stbir__simdf tot,c,t; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( c, hc ); \ + stbir__simdf_mult_mem( tot, c, decode ); \ + stbir__simdf_0123to1230( c, tot ); \ + stbir__simdf_0123to2301( t, tot ); \ + stbir__simdf_add1( tot, tot, c ); \ + stbir__simdf_add1( tot, tot, t ); + +#define stbir__store_output_tiny() \ + stbir__simdf_store1( output, tot ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 1; + +#define stbir__4_coeff_start() \ + stbir__simdf tot,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( c, hc ); \ + stbir__simdf_mult_mem( tot, c, decode ); \ + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( c, hc + (ofs) ); \ + stbir__simdf_madd_mem( tot, tot, c, decode+(ofs) ); + +#define stbir__1_coeff_remnant( ofs ) \ + { stbir__simdf d; \ + stbir__simdf_load1z( c, hc + (ofs) ); \ + stbir__simdf_load1( d, decode + (ofs) ); \ + stbir__simdf_madd( tot, tot, d, c ); } + +#define stbir__2_coeff_remnant( ofs ) \ + { stbir__simdf d; \ + stbir__simdf_load2z( c, hc+(ofs) ); \ + stbir__simdf_load2( d, decode+(ofs) ); \ + stbir__simdf_madd( tot, tot, d, c ); } + +#define stbir__3_coeff_setup() \ + stbir__simdf mask; \ + stbir__simdf_load( mask, STBIR_mask + 3 ); + +#define stbir__3_coeff_remnant( ofs ) \ + stbir__simdf_load( c, hc+(ofs) ); \ + stbir__simdf_and( c, c, mask ); \ + stbir__simdf_madd_mem( tot, tot, c, decode+(ofs) ); + +#define stbir__store_output() \ + stbir__simdf_0123to2301( c, tot ); \ + stbir__simdf_add( tot, tot, c ); \ + stbir__simdf_0123to1230( c, tot ); \ + stbir__simdf_add1( tot, tot, c ); \ + stbir__simdf_store1( output, tot ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 1; + +#else + +#define stbir__1_coeff_only() \ + float tot; \ + tot = decode[0]*hc[0]; + +#define stbir__2_coeff_only() \ + float tot; \ + tot = decode[0] * hc[0]; \ + tot += decode[1] * hc[1]; + +#define stbir__3_coeff_only() \ + float tot; \ + tot = decode[0] * hc[0]; \ + tot += decode[1] * hc[1]; \ + tot += decode[2] * hc[2]; + +#define stbir__store_output_tiny() \ + output[0] = tot; \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 1; + +#define stbir__4_coeff_start() \ + float tot0,tot1,tot2,tot3; \ + tot0 = decode[0] * hc[0]; \ + tot1 = decode[1] * hc[1]; \ + tot2 = decode[2] * hc[2]; \ + tot3 = decode[3] * hc[3]; + +#define stbir__4_coeff_continue_from_4( ofs ) \ + tot0 += decode[0+(ofs)] * hc[0+(ofs)]; \ + tot1 += decode[1+(ofs)] * hc[1+(ofs)]; \ + tot2 += decode[2+(ofs)] * hc[2+(ofs)]; \ + tot3 += decode[3+(ofs)] * hc[3+(ofs)]; + +#define stbir__1_coeff_remnant( ofs ) \ + tot0 += decode[0+(ofs)] * hc[0+(ofs)]; + +#define stbir__2_coeff_remnant( ofs ) \ + tot0 += decode[0+(ofs)] * hc[0+(ofs)]; \ + tot1 += decode[1+(ofs)] * hc[1+(ofs)]; \ + +#define stbir__3_coeff_remnant( ofs ) \ + tot0 += decode[0+(ofs)] * hc[0+(ofs)]; \ + tot1 += decode[1+(ofs)] * hc[1+(ofs)]; \ + tot2 += decode[2+(ofs)] * hc[2+(ofs)]; + +#define stbir__store_output() \ + output[0] = (tot0+tot2)+(tot1+tot3); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 1; + +#endif + +#define STBIR__horizontal_channels 1 +#define STB_IMAGE_RESIZE_DO_HORIZONTALS +#include STBIR__HEADER_FILENAME + + +//================= +// Do 2 channel horizontal routines + +#ifdef STBIR_SIMD + +#define stbir__1_coeff_only() \ + stbir__simdf tot,c,d; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1z( c, hc ); \ + stbir__simdf_0123to0011( c, c ); \ + stbir__simdf_load2( d, decode ); \ + stbir__simdf_mult( tot, d, c ); + +#define stbir__2_coeff_only() \ + stbir__simdf tot,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load2( c, hc ); \ + stbir__simdf_0123to0011( c, c ); \ + stbir__simdf_mult_mem( tot, c, decode ); + +#define stbir__3_coeff_only() \ + stbir__simdf tot,c,cs,d; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc ); \ + stbir__simdf_0123to0011( c, cs ); \ + stbir__simdf_mult_mem( tot, c, decode ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_load2z( d, decode+4 ); \ + stbir__simdf_madd( tot, tot, d, c ); + +#define stbir__store_output_tiny() \ + stbir__simdf_0123to2301( c, tot ); \ + stbir__simdf_add( tot, tot, c ); \ + stbir__simdf_store2( output, tot ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 2; + +#ifdef STBIR_SIMD8 + +#define stbir__4_coeff_start() \ + stbir__simdf8 tot0,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc ); \ + stbir__simdf8_0123to00112233( c, cs ); \ + stbir__simdf8_mult_mem( tot0, c, decode ); + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc + (ofs) ); \ + stbir__simdf8_0123to00112233( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*2 ); + +#define stbir__1_coeff_remnant( ofs ) \ + { stbir__simdf t,d; \ + stbir__simdf_load1z( t, hc + (ofs) ); \ + stbir__simdf_load2( d, decode + (ofs) * 2 ); \ + stbir__simdf_0123to0011( t, t ); \ + stbir__simdf_mult( t, t, d ); \ + stbir__simdf8_add4( tot0, tot0, t ); } + +#define stbir__2_coeff_remnant( ofs ) \ + { stbir__simdf t; \ + stbir__simdf_load2( t, hc + (ofs) ); \ + stbir__simdf_0123to0011( t, t ); \ + stbir__simdf_mult_mem( t, t, decode+(ofs)*2 ); \ + stbir__simdf8_add4( tot0, tot0, t ); } + +#define stbir__3_coeff_remnant( ofs ) \ + { stbir__simdf8 d; \ + stbir__simdf8_load4b( cs, hc + (ofs) ); \ + stbir__simdf8_0123to00112233( c, cs ); \ + stbir__simdf8_load6z( d, decode+(ofs)*2 ); \ + stbir__simdf8_madd( tot0, tot0, c, d ); } + +#define stbir__store_output() \ + { stbir__simdf t,d; \ + stbir__simdf8_add4halves( t, stbir__if_simdf8_cast_to_simdf4(tot0), tot0 ); \ + stbir__simdf_0123to2301( d, t ); \ + stbir__simdf_add( t, t, d ); \ + stbir__simdf_store2( output, t ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 2; } + +#else + +#define stbir__4_coeff_start() \ + stbir__simdf tot0,tot1,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc ); \ + stbir__simdf_0123to0011( c, cs ); \ + stbir__simdf_mult_mem( tot0, c, decode ); \ + stbir__simdf_0123to2233( c, cs ); \ + stbir__simdf_mult_mem( tot1, c, decode+4 ); + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc + (ofs) ); \ + stbir__simdf_0123to0011( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*2 ); \ + stbir__simdf_0123to2233( c, cs ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*2+4 ); + +#define stbir__1_coeff_remnant( ofs ) \ + { stbir__simdf d; \ + stbir__simdf_load1z( cs, hc + (ofs) ); \ + stbir__simdf_0123to0011( c, cs ); \ + stbir__simdf_load2( d, decode + (ofs) * 2 ); \ + stbir__simdf_madd( tot0, tot0, d, c ); } + +#define stbir__2_coeff_remnant( ofs ) \ + stbir__simdf_load2( cs, hc + (ofs) ); \ + stbir__simdf_0123to0011( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*2 ); + +#define stbir__3_coeff_remnant( ofs ) \ + { stbir__simdf d; \ + stbir__simdf_load( cs, hc + (ofs) ); \ + stbir__simdf_0123to0011( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*2 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_load2z( d, decode + (ofs) * 2 + 4 ); \ + stbir__simdf_madd( tot1, tot1, d, c ); } + +#define stbir__store_output() \ + stbir__simdf_add( tot0, tot0, tot1 ); \ + stbir__simdf_0123to2301( c, tot0 ); \ + stbir__simdf_add( tot0, tot0, c ); \ + stbir__simdf_store2( output, tot0 ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 2; + +#endif + +#else + +#define stbir__1_coeff_only() \ + float tota,totb,c; \ + c = hc[0]; \ + tota = decode[0]*c; \ + totb = decode[1]*c; + +#define stbir__2_coeff_only() \ + float tota,totb,c; \ + c = hc[0]; \ + tota = decode[0]*c; \ + totb = decode[1]*c; \ + c = hc[1]; \ + tota += decode[2]*c; \ + totb += decode[3]*c; + +// this weird order of add matches the simd +#define stbir__3_coeff_only() \ + float tota,totb,c; \ + c = hc[0]; \ + tota = decode[0]*c; \ + totb = decode[1]*c; \ + c = hc[2]; \ + tota += decode[4]*c; \ + totb += decode[5]*c; \ + c = hc[1]; \ + tota += decode[2]*c; \ + totb += decode[3]*c; + +#define stbir__store_output_tiny() \ + output[0] = tota; \ + output[1] = totb; \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 2; + +#define stbir__4_coeff_start() \ + float tota0,tota1,tota2,tota3,totb0,totb1,totb2,totb3,c; \ + c = hc[0]; \ + tota0 = decode[0]*c; \ + totb0 = decode[1]*c; \ + c = hc[1]; \ + tota1 = decode[2]*c; \ + totb1 = decode[3]*c; \ + c = hc[2]; \ + tota2 = decode[4]*c; \ + totb2 = decode[5]*c; \ + c = hc[3]; \ + tota3 = decode[6]*c; \ + totb3 = decode[7]*c; + +#define stbir__4_coeff_continue_from_4( ofs ) \ + c = hc[0+(ofs)]; \ + tota0 += decode[0+(ofs)*2]*c; \ + totb0 += decode[1+(ofs)*2]*c; \ + c = hc[1+(ofs)]; \ + tota1 += decode[2+(ofs)*2]*c; \ + totb1 += decode[3+(ofs)*2]*c; \ + c = hc[2+(ofs)]; \ + tota2 += decode[4+(ofs)*2]*c; \ + totb2 += decode[5+(ofs)*2]*c; \ + c = hc[3+(ofs)]; \ + tota3 += decode[6+(ofs)*2]*c; \ + totb3 += decode[7+(ofs)*2]*c; + +#define stbir__1_coeff_remnant( ofs ) \ + c = hc[0+(ofs)]; \ + tota0 += decode[0+(ofs)*2] * c; \ + totb0 += decode[1+(ofs)*2] * c; + +#define stbir__2_coeff_remnant( ofs ) \ + c = hc[0+(ofs)]; \ + tota0 += decode[0+(ofs)*2] * c; \ + totb0 += decode[1+(ofs)*2] * c; \ + c = hc[1+(ofs)]; \ + tota1 += decode[2+(ofs)*2] * c; \ + totb1 += decode[3+(ofs)*2] * c; + +#define stbir__3_coeff_remnant( ofs ) \ + c = hc[0+(ofs)]; \ + tota0 += decode[0+(ofs)*2] * c; \ + totb0 += decode[1+(ofs)*2] * c; \ + c = hc[1+(ofs)]; \ + tota1 += decode[2+(ofs)*2] * c; \ + totb1 += decode[3+(ofs)*2] * c; \ + c = hc[2+(ofs)]; \ + tota2 += decode[4+(ofs)*2] * c; \ + totb2 += decode[5+(ofs)*2] * c; + +#define stbir__store_output() \ + output[0] = (tota0+tota2)+(tota1+tota3); \ + output[1] = (totb0+totb2)+(totb1+totb3); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 2; + +#endif + +#define STBIR__horizontal_channels 2 +#define STB_IMAGE_RESIZE_DO_HORIZONTALS +#include STBIR__HEADER_FILENAME + + +//================= +// Do 3 channel horizontal routines + +#ifdef STBIR_SIMD + +#define stbir__1_coeff_only() \ + stbir__simdf tot,c,d; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1z( c, hc ); \ + stbir__simdf_0123to0001( c, c ); \ + stbir__simdf_load( d, decode ); \ + stbir__simdf_mult( tot, d, c ); + +#define stbir__2_coeff_only() \ + stbir__simdf tot,c,cs,d; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load2( cs, hc ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_load( d, decode ); \ + stbir__simdf_mult( tot, d, c ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_load( d, decode+3 ); \ + stbir__simdf_madd( tot, tot, d, c ); + +#define stbir__3_coeff_only() \ + stbir__simdf tot,c,d,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_load( d, decode ); \ + stbir__simdf_mult( tot, d, c ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_load( d, decode+3 ); \ + stbir__simdf_madd( tot, tot, d, c ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_load( d, decode+6 ); \ + stbir__simdf_madd( tot, tot, d, c ); + +#define stbir__store_output_tiny() \ + stbir__simdf_store2( output, tot ); \ + stbir__simdf_0123to2301( tot, tot ); \ + stbir__simdf_store1( output+2, tot ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 3; + +#ifdef STBIR_SIMD8 + +// we're loading from the XXXYYY decode by -1 to get the XXXYYY into different halves of the AVX reg fyi +#define stbir__4_coeff_start() \ + stbir__simdf8 tot0,tot1,c,cs; stbir__simdf t; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc ); \ + stbir__simdf8_0123to00001111( c, cs ); \ + stbir__simdf8_mult_mem( tot0, c, decode - 1 ); \ + stbir__simdf8_0123to22223333( c, cs ); \ + stbir__simdf8_mult_mem( tot1, c, decode+6 - 1 ); + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc + (ofs) ); \ + stbir__simdf8_0123to00001111( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*3 - 1 ); \ + stbir__simdf8_0123to22223333( c, cs ); \ + stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*3 + 6 - 1 ); + +#define stbir__1_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1rep4( t, hc + (ofs) ); \ + stbir__simdf8_madd_mem4( tot0, tot0, t, decode+(ofs)*3 - 1 ); + +#define stbir__2_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc + (ofs) - 2 ); \ + stbir__simdf8_0123to22223333( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*3 - 1 ); + + #define stbir__3_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc + (ofs) ); \ + stbir__simdf8_0123to00001111( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*3 - 1 ); \ + stbir__simdf8_0123to2222( t, cs ); \ + stbir__simdf8_madd_mem4( tot1, tot1, t, decode+(ofs)*3 + 6 - 1 ); + +#define stbir__store_output() \ + stbir__simdf8_add( tot0, tot0, tot1 ); \ + stbir__simdf_0123to1230( t, stbir__if_simdf8_cast_to_simdf4( tot0 ) ); \ + stbir__simdf8_add4halves( t, t, tot0 ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 3; \ + if ( output < output_end ) \ + { \ + stbir__simdf_store( output-3, t ); \ + continue; \ + } \ + { stbir__simdf tt; stbir__simdf_0123to2301( tt, t ); \ + stbir__simdf_store2( output-3, t ); \ + stbir__simdf_store1( output+2-3, tt ); } \ + break; + + +#else + +#define stbir__4_coeff_start() \ + stbir__simdf tot0,tot1,tot2,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc ); \ + stbir__simdf_0123to0001( c, cs ); \ + stbir__simdf_mult_mem( tot0, c, decode ); \ + stbir__simdf_0123to1122( c, cs ); \ + stbir__simdf_mult_mem( tot1, c, decode+4 ); \ + stbir__simdf_0123to2333( c, cs ); \ + stbir__simdf_mult_mem( tot2, c, decode+8 ); + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc + (ofs) ); \ + stbir__simdf_0123to0001( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*3 ); \ + stbir__simdf_0123to1122( c, cs ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*3+4 ); \ + stbir__simdf_0123to2333( c, cs ); \ + stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*3+8 ); + +#define stbir__1_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1z( c, hc + (ofs) ); \ + stbir__simdf_0123to0001( c, c ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*3 ); + +#define stbir__2_coeff_remnant( ofs ) \ + { stbir__simdf d; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load2z( cs, hc + (ofs) ); \ + stbir__simdf_0123to0001( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*3 ); \ + stbir__simdf_0123to1122( c, cs ); \ + stbir__simdf_load2z( d, decode+(ofs)*3+4 ); \ + stbir__simdf_madd( tot1, tot1, c, d ); } + +#define stbir__3_coeff_remnant( ofs ) \ + { stbir__simdf d; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc + (ofs) ); \ + stbir__simdf_0123to0001( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*3 ); \ + stbir__simdf_0123to1122( c, cs ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*3+4 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_load1z( d, decode+(ofs)*3+8 ); \ + stbir__simdf_madd( tot2, tot2, c, d ); } + +#define stbir__store_output() \ + stbir__simdf_0123ABCDto3ABx( c, tot0, tot1 ); \ + stbir__simdf_0123ABCDto23Ax( cs, tot1, tot2 ); \ + stbir__simdf_0123to1230( tot2, tot2 ); \ + stbir__simdf_add( tot0, tot0, cs ); \ + stbir__simdf_add( c, c, tot2 ); \ + stbir__simdf_add( tot0, tot0, c ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 3; \ + if ( output < output_end ) \ + { \ + stbir__simdf_store( output-3, tot0 ); \ + continue; \ + } \ + stbir__simdf_0123to2301( tot1, tot0 ); \ + stbir__simdf_store2( output-3, tot0 ); \ + stbir__simdf_store1( output+2-3, tot1 ); \ + break; + +#endif + +#else + +#define stbir__1_coeff_only() \ + float tot0, tot1, tot2, c; \ + c = hc[0]; \ + tot0 = decode[0]*c; \ + tot1 = decode[1]*c; \ + tot2 = decode[2]*c; + +#define stbir__2_coeff_only() \ + float tot0, tot1, tot2, c; \ + c = hc[0]; \ + tot0 = decode[0]*c; \ + tot1 = decode[1]*c; \ + tot2 = decode[2]*c; \ + c = hc[1]; \ + tot0 += decode[3]*c; \ + tot1 += decode[4]*c; \ + tot2 += decode[5]*c; + +#define stbir__3_coeff_only() \ + float tot0, tot1, tot2, c; \ + c = hc[0]; \ + tot0 = decode[0]*c; \ + tot1 = decode[1]*c; \ + tot2 = decode[2]*c; \ + c = hc[1]; \ + tot0 += decode[3]*c; \ + tot1 += decode[4]*c; \ + tot2 += decode[5]*c; \ + c = hc[2]; \ + tot0 += decode[6]*c; \ + tot1 += decode[7]*c; \ + tot2 += decode[8]*c; + +#define stbir__store_output_tiny() \ + output[0] = tot0; \ + output[1] = tot1; \ + output[2] = tot2; \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 3; + +#define stbir__4_coeff_start() \ + float tota0,tota1,tota2,totb0,totb1,totb2,totc0,totc1,totc2,totd0,totd1,totd2,c; \ + c = hc[0]; \ + tota0 = decode[0]*c; \ + tota1 = decode[1]*c; \ + tota2 = decode[2]*c; \ + c = hc[1]; \ + totb0 = decode[3]*c; \ + totb1 = decode[4]*c; \ + totb2 = decode[5]*c; \ + c = hc[2]; \ + totc0 = decode[6]*c; \ + totc1 = decode[7]*c; \ + totc2 = decode[8]*c; \ + c = hc[3]; \ + totd0 = decode[9]*c; \ + totd1 = decode[10]*c; \ + totd2 = decode[11]*c; + +#define stbir__4_coeff_continue_from_4( ofs ) \ + c = hc[0+(ofs)]; \ + tota0 += decode[0+(ofs)*3]*c; \ + tota1 += decode[1+(ofs)*3]*c; \ + tota2 += decode[2+(ofs)*3]*c; \ + c = hc[1+(ofs)]; \ + totb0 += decode[3+(ofs)*3]*c; \ + totb1 += decode[4+(ofs)*3]*c; \ + totb2 += decode[5+(ofs)*3]*c; \ + c = hc[2+(ofs)]; \ + totc0 += decode[6+(ofs)*3]*c; \ + totc1 += decode[7+(ofs)*3]*c; \ + totc2 += decode[8+(ofs)*3]*c; \ + c = hc[3+(ofs)]; \ + totd0 += decode[9+(ofs)*3]*c; \ + totd1 += decode[10+(ofs)*3]*c; \ + totd2 += decode[11+(ofs)*3]*c; + +#define stbir__1_coeff_remnant( ofs ) \ + c = hc[0+(ofs)]; \ + tota0 += decode[0+(ofs)*3]*c; \ + tota1 += decode[1+(ofs)*3]*c; \ + tota2 += decode[2+(ofs)*3]*c; + +#define stbir__2_coeff_remnant( ofs ) \ + c = hc[0+(ofs)]; \ + tota0 += decode[0+(ofs)*3]*c; \ + tota1 += decode[1+(ofs)*3]*c; \ + tota2 += decode[2+(ofs)*3]*c; \ + c = hc[1+(ofs)]; \ + totb0 += decode[3+(ofs)*3]*c; \ + totb1 += decode[4+(ofs)*3]*c; \ + totb2 += decode[5+(ofs)*3]*c; \ + +#define stbir__3_coeff_remnant( ofs ) \ + c = hc[0+(ofs)]; \ + tota0 += decode[0+(ofs)*3]*c; \ + tota1 += decode[1+(ofs)*3]*c; \ + tota2 += decode[2+(ofs)*3]*c; \ + c = hc[1+(ofs)]; \ + totb0 += decode[3+(ofs)*3]*c; \ + totb1 += decode[4+(ofs)*3]*c; \ + totb2 += decode[5+(ofs)*3]*c; \ + c = hc[2+(ofs)]; \ + totc0 += decode[6+(ofs)*3]*c; \ + totc1 += decode[7+(ofs)*3]*c; \ + totc2 += decode[8+(ofs)*3]*c; + +#define stbir__store_output() \ + output[0] = (tota0+totc0)+(totb0+totd0); \ + output[1] = (tota1+totc1)+(totb1+totd1); \ + output[2] = (tota2+totc2)+(totb2+totd2); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 3; + +#endif + +#define STBIR__horizontal_channels 3 +#define STB_IMAGE_RESIZE_DO_HORIZONTALS +#include STBIR__HEADER_FILENAME + +//================= +// Do 4 channel horizontal routines + +#ifdef STBIR_SIMD + +#define stbir__1_coeff_only() \ + stbir__simdf tot,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1( c, hc ); \ + stbir__simdf_0123to0000( c, c ); \ + stbir__simdf_mult_mem( tot, c, decode ); + +#define stbir__2_coeff_only() \ + stbir__simdf tot,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load2( cs, hc ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_mult_mem( tot, c, decode ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot, tot, c, decode+4 ); + +#define stbir__3_coeff_only() \ + stbir__simdf tot,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_mult_mem( tot, c, decode ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot, tot, c, decode+4 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_madd_mem( tot, tot, c, decode+8 ); + +#define stbir__store_output_tiny() \ + stbir__simdf_store( output, tot ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 4; + +#ifdef STBIR_SIMD8 + +#define stbir__4_coeff_start() \ + stbir__simdf8 tot0,c,cs; stbir__simdf t; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc ); \ + stbir__simdf8_0123to00001111( c, cs ); \ + stbir__simdf8_mult_mem( tot0, c, decode ); \ + stbir__simdf8_0123to22223333( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+8 ); + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc + (ofs) ); \ + stbir__simdf8_0123to00001111( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ + stbir__simdf8_0123to22223333( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*4+8 ); + +#define stbir__1_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1rep4( t, hc + (ofs) ); \ + stbir__simdf8_madd_mem4( tot0, tot0, t, decode+(ofs)*4 ); + +#define stbir__2_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc + (ofs) - 2 ); \ + stbir__simdf8_0123to22223333( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); + + #define stbir__3_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc + (ofs) ); \ + stbir__simdf8_0123to00001111( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ + stbir__simdf8_0123to2222( t, cs ); \ + stbir__simdf8_madd_mem4( tot0, tot0, t, decode+(ofs)*4+8 ); + +#define stbir__store_output() \ + stbir__simdf8_add4halves( t, stbir__if_simdf8_cast_to_simdf4(tot0), tot0 ); \ + stbir__simdf_store( output, t ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 4; + +#else + +#define stbir__4_coeff_start() \ + stbir__simdf tot0,tot1,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_mult_mem( tot0, c, decode ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_mult_mem( tot1, c, decode+4 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+8 ); \ + stbir__simdf_0123to3333( c, cs ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+12 ); + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc + (ofs) ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*4+4 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4+8 ); \ + stbir__simdf_0123to3333( c, cs ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*4+12 ); + +#define stbir__1_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1( c, hc + (ofs) ); \ + stbir__simdf_0123to0000( c, c ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); + +#define stbir__2_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load2( cs, hc + (ofs) ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*4+4 ); + +#define stbir__3_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc + (ofs) ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4 ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*4+4 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*4+8 ); + +#define stbir__store_output() \ + stbir__simdf_add( tot0, tot0, tot1 ); \ + stbir__simdf_store( output, tot0 ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 4; + +#endif + +#else + +#define stbir__1_coeff_only() \ + float p0,p1,p2,p3,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0]; \ + p0 = decode[0] * c; \ + p1 = decode[1] * c; \ + p2 = decode[2] * c; \ + p3 = decode[3] * c; + +#define stbir__2_coeff_only() \ + float p0,p1,p2,p3,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0]; \ + p0 = decode[0] * c; \ + p1 = decode[1] * c; \ + p2 = decode[2] * c; \ + p3 = decode[3] * c; \ + c = hc[1]; \ + p0 += decode[4] * c; \ + p1 += decode[5] * c; \ + p2 += decode[6] * c; \ + p3 += decode[7] * c; + +#define stbir__3_coeff_only() \ + float p0,p1,p2,p3,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0]; \ + p0 = decode[0] * c; \ + p1 = decode[1] * c; \ + p2 = decode[2] * c; \ + p3 = decode[3] * c; \ + c = hc[1]; \ + p0 += decode[4] * c; \ + p1 += decode[5] * c; \ + p2 += decode[6] * c; \ + p3 += decode[7] * c; \ + c = hc[2]; \ + p0 += decode[8] * c; \ + p1 += decode[9] * c; \ + p2 += decode[10] * c; \ + p3 += decode[11] * c; + +#define stbir__store_output_tiny() \ + output[0] = p0; \ + output[1] = p1; \ + output[2] = p2; \ + output[3] = p3; \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 4; + +#define stbir__4_coeff_start() \ + float x0,x1,x2,x3,y0,y1,y2,y3,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0]; \ + x0 = decode[0] * c; \ + x1 = decode[1] * c; \ + x2 = decode[2] * c; \ + x3 = decode[3] * c; \ + c = hc[1]; \ + y0 = decode[4] * c; \ + y1 = decode[5] * c; \ + y2 = decode[6] * c; \ + y3 = decode[7] * c; \ + c = hc[2]; \ + x0 += decode[8] * c; \ + x1 += decode[9] * c; \ + x2 += decode[10] * c; \ + x3 += decode[11] * c; \ + c = hc[3]; \ + y0 += decode[12] * c; \ + y1 += decode[13] * c; \ + y2 += decode[14] * c; \ + y3 += decode[15] * c; + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0+(ofs)]; \ + x0 += decode[0+(ofs)*4] * c; \ + x1 += decode[1+(ofs)*4] * c; \ + x2 += decode[2+(ofs)*4] * c; \ + x3 += decode[3+(ofs)*4] * c; \ + c = hc[1+(ofs)]; \ + y0 += decode[4+(ofs)*4] * c; \ + y1 += decode[5+(ofs)*4] * c; \ + y2 += decode[6+(ofs)*4] * c; \ + y3 += decode[7+(ofs)*4] * c; \ + c = hc[2+(ofs)]; \ + x0 += decode[8+(ofs)*4] * c; \ + x1 += decode[9+(ofs)*4] * c; \ + x2 += decode[10+(ofs)*4] * c; \ + x3 += decode[11+(ofs)*4] * c; \ + c = hc[3+(ofs)]; \ + y0 += decode[12+(ofs)*4] * c; \ + y1 += decode[13+(ofs)*4] * c; \ + y2 += decode[14+(ofs)*4] * c; \ + y3 += decode[15+(ofs)*4] * c; + +#define stbir__1_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0+(ofs)]; \ + x0 += decode[0+(ofs)*4] * c; \ + x1 += decode[1+(ofs)*4] * c; \ + x2 += decode[2+(ofs)*4] * c; \ + x3 += decode[3+(ofs)*4] * c; + +#define stbir__2_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0+(ofs)]; \ + x0 += decode[0+(ofs)*4] * c; \ + x1 += decode[1+(ofs)*4] * c; \ + x2 += decode[2+(ofs)*4] * c; \ + x3 += decode[3+(ofs)*4] * c; \ + c = hc[1+(ofs)]; \ + y0 += decode[4+(ofs)*4] * c; \ + y1 += decode[5+(ofs)*4] * c; \ + y2 += decode[6+(ofs)*4] * c; \ + y3 += decode[7+(ofs)*4] * c; + +#define stbir__3_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0+(ofs)]; \ + x0 += decode[0+(ofs)*4] * c; \ + x1 += decode[1+(ofs)*4] * c; \ + x2 += decode[2+(ofs)*4] * c; \ + x3 += decode[3+(ofs)*4] * c; \ + c = hc[1+(ofs)]; \ + y0 += decode[4+(ofs)*4] * c; \ + y1 += decode[5+(ofs)*4] * c; \ + y2 += decode[6+(ofs)*4] * c; \ + y3 += decode[7+(ofs)*4] * c; \ + c = hc[2+(ofs)]; \ + x0 += decode[8+(ofs)*4] * c; \ + x1 += decode[9+(ofs)*4] * c; \ + x2 += decode[10+(ofs)*4] * c; \ + x3 += decode[11+(ofs)*4] * c; + +#define stbir__store_output() \ + output[0] = x0 + y0; \ + output[1] = x1 + y1; \ + output[2] = x2 + y2; \ + output[3] = x3 + y3; \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 4; + +#endif + +#define STBIR__horizontal_channels 4 +#define STB_IMAGE_RESIZE_DO_HORIZONTALS +#include STBIR__HEADER_FILENAME + + + +//================= +// Do 7 channel horizontal routines + +#ifdef STBIR_SIMD + +#define stbir__1_coeff_only() \ + stbir__simdf tot0,tot1,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1( c, hc ); \ + stbir__simdf_0123to0000( c, c ); \ + stbir__simdf_mult_mem( tot0, c, decode ); \ + stbir__simdf_mult_mem( tot1, c, decode+3 ); + +#define stbir__2_coeff_only() \ + stbir__simdf tot0,tot1,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load2( cs, hc ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_mult_mem( tot0, c, decode ); \ + stbir__simdf_mult_mem( tot1, c, decode+3 ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+7 ); \ + stbir__simdf_madd_mem( tot1, tot1, c,decode+10 ); + +#define stbir__3_coeff_only() \ + stbir__simdf tot0,tot1,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_mult_mem( tot0, c, decode ); \ + stbir__simdf_mult_mem( tot1, c, decode+3 ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+7 ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+10 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+14 ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+17 ); + +#define stbir__store_output_tiny() \ + stbir__simdf_store( output+3, tot1 ); \ + stbir__simdf_store( output, tot0 ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 7; + +#ifdef STBIR_SIMD8 + +#define stbir__4_coeff_start() \ + stbir__simdf8 tot0,tot1,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc ); \ + stbir__simdf8_0123to00000000( c, cs ); \ + stbir__simdf8_mult_mem( tot0, c, decode ); \ + stbir__simdf8_0123to11111111( c, cs ); \ + stbir__simdf8_mult_mem( tot1, c, decode+7 ); \ + stbir__simdf8_0123to22222222( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+14 ); \ + stbir__simdf8_0123to33333333( c, cs ); \ + stbir__simdf8_madd_mem( tot1, tot1, c, decode+21 ); + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc + (ofs) ); \ + stbir__simdf8_0123to00000000( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ + stbir__simdf8_0123to11111111( c, cs ); \ + stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*7+7 ); \ + stbir__simdf8_0123to22222222( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7+14 ); \ + stbir__simdf8_0123to33333333( c, cs ); \ + stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*7+21 ); + +#define stbir__1_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load1b( c, hc + (ofs) ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); + +#define stbir__2_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load1b( c, hc + (ofs) ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ + stbir__simdf8_load1b( c, hc + (ofs)+1 ); \ + stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*7+7 ); + +#define stbir__3_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf8_load4b( cs, hc + (ofs) ); \ + stbir__simdf8_0123to00000000( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ + stbir__simdf8_0123to11111111( c, cs ); \ + stbir__simdf8_madd_mem( tot1, tot1, c, decode+(ofs)*7+7 ); \ + stbir__simdf8_0123to22222222( c, cs ); \ + stbir__simdf8_madd_mem( tot0, tot0, c, decode+(ofs)*7+14 ); + +#define stbir__store_output() \ + stbir__simdf8_add( tot0, tot0, tot1 ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 7; \ + if ( output < output_end ) \ + { \ + stbir__simdf8_store( output-7, tot0 ); \ + continue; \ + } \ + stbir__simdf_store( output-7+3, stbir__simdf_swiz(stbir__simdf8_gettop4(tot0),0,0,1,2) ); \ + stbir__simdf_store( output-7, stbir__if_simdf8_cast_to_simdf4(tot0) ); \ + break; + +#else + +#define stbir__4_coeff_start() \ + stbir__simdf tot0,tot1,tot2,tot3,c,cs; \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_mult_mem( tot0, c, decode ); \ + stbir__simdf_mult_mem( tot1, c, decode+3 ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_mult_mem( tot2, c, decode+7 ); \ + stbir__simdf_mult_mem( tot3, c, decode+10 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+14 ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+17 ); \ + stbir__simdf_0123to3333( c, cs ); \ + stbir__simdf_madd_mem( tot2, tot2, c, decode+21 ); \ + stbir__simdf_madd_mem( tot3, tot3, c, decode+24 ); + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc + (ofs) ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+3 ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*7+7 ); \ + stbir__simdf_madd_mem( tot3, tot3, c, decode+(ofs)*7+10 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7+14 ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+17 ); \ + stbir__simdf_0123to3333( c, cs ); \ + stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*7+21 ); \ + stbir__simdf_madd_mem( tot3, tot3, c, decode+(ofs)*7+24 ); + +#define stbir__1_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load1( c, hc + (ofs) ); \ + stbir__simdf_0123to0000( c, c ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+3 ); \ + +#define stbir__2_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load2( cs, hc + (ofs) ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+3 ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*7+7 ); \ + stbir__simdf_madd_mem( tot3, tot3, c, decode+(ofs)*7+10 ); + +#define stbir__3_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + stbir__simdf_load( cs, hc + (ofs) ); \ + stbir__simdf_0123to0000( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7 ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+3 ); \ + stbir__simdf_0123to1111( c, cs ); \ + stbir__simdf_madd_mem( tot2, tot2, c, decode+(ofs)*7+7 ); \ + stbir__simdf_madd_mem( tot3, tot3, c, decode+(ofs)*7+10 ); \ + stbir__simdf_0123to2222( c, cs ); \ + stbir__simdf_madd_mem( tot0, tot0, c, decode+(ofs)*7+14 ); \ + stbir__simdf_madd_mem( tot1, tot1, c, decode+(ofs)*7+17 ); + +#define stbir__store_output() \ + stbir__simdf_add( tot0, tot0, tot2 ); \ + stbir__simdf_add( tot1, tot1, tot3 ); \ + stbir__simdf_store( output+3, tot1 ); \ + stbir__simdf_store( output, tot0 ); \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 7; + +#endif + +#else + +#define stbir__1_coeff_only() \ + float tot0, tot1, tot2, tot3, tot4, tot5, tot6, c; \ + c = hc[0]; \ + tot0 = decode[0]*c; \ + tot1 = decode[1]*c; \ + tot2 = decode[2]*c; \ + tot3 = decode[3]*c; \ + tot4 = decode[4]*c; \ + tot5 = decode[5]*c; \ + tot6 = decode[6]*c; + +#define stbir__2_coeff_only() \ + float tot0, tot1, tot2, tot3, tot4, tot5, tot6, c; \ + c = hc[0]; \ + tot0 = decode[0]*c; \ + tot1 = decode[1]*c; \ + tot2 = decode[2]*c; \ + tot3 = decode[3]*c; \ + tot4 = decode[4]*c; \ + tot5 = decode[5]*c; \ + tot6 = decode[6]*c; \ + c = hc[1]; \ + tot0 += decode[7]*c; \ + tot1 += decode[8]*c; \ + tot2 += decode[9]*c; \ + tot3 += decode[10]*c; \ + tot4 += decode[11]*c; \ + tot5 += decode[12]*c; \ + tot6 += decode[13]*c; \ + +#define stbir__3_coeff_only() \ + float tot0, tot1, tot2, tot3, tot4, tot5, tot6, c; \ + c = hc[0]; \ + tot0 = decode[0]*c; \ + tot1 = decode[1]*c; \ + tot2 = decode[2]*c; \ + tot3 = decode[3]*c; \ + tot4 = decode[4]*c; \ + tot5 = decode[5]*c; \ + tot6 = decode[6]*c; \ + c = hc[1]; \ + tot0 += decode[7]*c; \ + tot1 += decode[8]*c; \ + tot2 += decode[9]*c; \ + tot3 += decode[10]*c; \ + tot4 += decode[11]*c; \ + tot5 += decode[12]*c; \ + tot6 += decode[13]*c; \ + c = hc[2]; \ + tot0 += decode[14]*c; \ + tot1 += decode[15]*c; \ + tot2 += decode[16]*c; \ + tot3 += decode[17]*c; \ + tot4 += decode[18]*c; \ + tot5 += decode[19]*c; \ + tot6 += decode[20]*c; \ + +#define stbir__store_output_tiny() \ + output[0] = tot0; \ + output[1] = tot1; \ + output[2] = tot2; \ + output[3] = tot3; \ + output[4] = tot4; \ + output[5] = tot5; \ + output[6] = tot6; \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 7; + +#define stbir__4_coeff_start() \ + float x0,x1,x2,x3,x4,x5,x6,y0,y1,y2,y3,y4,y5,y6,c; \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0]; \ + x0 = decode[0] * c; \ + x1 = decode[1] * c; \ + x2 = decode[2] * c; \ + x3 = decode[3] * c; \ + x4 = decode[4] * c; \ + x5 = decode[5] * c; \ + x6 = decode[6] * c; \ + c = hc[1]; \ + y0 = decode[7] * c; \ + y1 = decode[8] * c; \ + y2 = decode[9] * c; \ + y3 = decode[10] * c; \ + y4 = decode[11] * c; \ + y5 = decode[12] * c; \ + y6 = decode[13] * c; \ + c = hc[2]; \ + x0 += decode[14] * c; \ + x1 += decode[15] * c; \ + x2 += decode[16] * c; \ + x3 += decode[17] * c; \ + x4 += decode[18] * c; \ + x5 += decode[19] * c; \ + x6 += decode[20] * c; \ + c = hc[3]; \ + y0 += decode[21] * c; \ + y1 += decode[22] * c; \ + y2 += decode[23] * c; \ + y3 += decode[24] * c; \ + y4 += decode[25] * c; \ + y5 += decode[26] * c; \ + y6 += decode[27] * c; + +#define stbir__4_coeff_continue_from_4( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0+(ofs)]; \ + x0 += decode[0+(ofs)*7] * c; \ + x1 += decode[1+(ofs)*7] * c; \ + x2 += decode[2+(ofs)*7] * c; \ + x3 += decode[3+(ofs)*7] * c; \ + x4 += decode[4+(ofs)*7] * c; \ + x5 += decode[5+(ofs)*7] * c; \ + x6 += decode[6+(ofs)*7] * c; \ + c = hc[1+(ofs)]; \ + y0 += decode[7+(ofs)*7] * c; \ + y1 += decode[8+(ofs)*7] * c; \ + y2 += decode[9+(ofs)*7] * c; \ + y3 += decode[10+(ofs)*7] * c; \ + y4 += decode[11+(ofs)*7] * c; \ + y5 += decode[12+(ofs)*7] * c; \ + y6 += decode[13+(ofs)*7] * c; \ + c = hc[2+(ofs)]; \ + x0 += decode[14+(ofs)*7] * c; \ + x1 += decode[15+(ofs)*7] * c; \ + x2 += decode[16+(ofs)*7] * c; \ + x3 += decode[17+(ofs)*7] * c; \ + x4 += decode[18+(ofs)*7] * c; \ + x5 += decode[19+(ofs)*7] * c; \ + x6 += decode[20+(ofs)*7] * c; \ + c = hc[3+(ofs)]; \ + y0 += decode[21+(ofs)*7] * c; \ + y1 += decode[22+(ofs)*7] * c; \ + y2 += decode[23+(ofs)*7] * c; \ + y3 += decode[24+(ofs)*7] * c; \ + y4 += decode[25+(ofs)*7] * c; \ + y5 += decode[26+(ofs)*7] * c; \ + y6 += decode[27+(ofs)*7] * c; + +#define stbir__1_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0+(ofs)]; \ + x0 += decode[0+(ofs)*7] * c; \ + x1 += decode[1+(ofs)*7] * c; \ + x2 += decode[2+(ofs)*7] * c; \ + x3 += decode[3+(ofs)*7] * c; \ + x4 += decode[4+(ofs)*7] * c; \ + x5 += decode[5+(ofs)*7] * c; \ + x6 += decode[6+(ofs)*7] * c; \ + +#define stbir__2_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0+(ofs)]; \ + x0 += decode[0+(ofs)*7] * c; \ + x1 += decode[1+(ofs)*7] * c; \ + x2 += decode[2+(ofs)*7] * c; \ + x3 += decode[3+(ofs)*7] * c; \ + x4 += decode[4+(ofs)*7] * c; \ + x5 += decode[5+(ofs)*7] * c; \ + x6 += decode[6+(ofs)*7] * c; \ + c = hc[1+(ofs)]; \ + y0 += decode[7+(ofs)*7] * c; \ + y1 += decode[8+(ofs)*7] * c; \ + y2 += decode[9+(ofs)*7] * c; \ + y3 += decode[10+(ofs)*7] * c; \ + y4 += decode[11+(ofs)*7] * c; \ + y5 += decode[12+(ofs)*7] * c; \ + y6 += decode[13+(ofs)*7] * c; \ + +#define stbir__3_coeff_remnant( ofs ) \ + STBIR_SIMD_NO_UNROLL(decode); \ + c = hc[0+(ofs)]; \ + x0 += decode[0+(ofs)*7] * c; \ + x1 += decode[1+(ofs)*7] * c; \ + x2 += decode[2+(ofs)*7] * c; \ + x3 += decode[3+(ofs)*7] * c; \ + x4 += decode[4+(ofs)*7] * c; \ + x5 += decode[5+(ofs)*7] * c; \ + x6 += decode[6+(ofs)*7] * c; \ + c = hc[1+(ofs)]; \ + y0 += decode[7+(ofs)*7] * c; \ + y1 += decode[8+(ofs)*7] * c; \ + y2 += decode[9+(ofs)*7] * c; \ + y3 += decode[10+(ofs)*7] * c; \ + y4 += decode[11+(ofs)*7] * c; \ + y5 += decode[12+(ofs)*7] * c; \ + y6 += decode[13+(ofs)*7] * c; \ + c = hc[2+(ofs)]; \ + x0 += decode[14+(ofs)*7] * c; \ + x1 += decode[15+(ofs)*7] * c; \ + x2 += decode[16+(ofs)*7] * c; \ + x3 += decode[17+(ofs)*7] * c; \ + x4 += decode[18+(ofs)*7] * c; \ + x5 += decode[19+(ofs)*7] * c; \ + x6 += decode[20+(ofs)*7] * c; \ + +#define stbir__store_output() \ + output[0] = x0 + y0; \ + output[1] = x1 + y1; \ + output[2] = x2 + y2; \ + output[3] = x3 + y3; \ + output[4] = x4 + y4; \ + output[5] = x5 + y5; \ + output[6] = x6 + y6; \ + horizontal_coefficients += coefficient_width; \ + ++horizontal_contributors; \ + output += 7; + +#endif + +#define STBIR__horizontal_channels 7 +#define STB_IMAGE_RESIZE_DO_HORIZONTALS +#include STBIR__HEADER_FILENAME + + +// include all of the vertical resamplers (both scatter and gather versions) + +#define STBIR__vertical_channels 1 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 1 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 2 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 2 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 3 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 3 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 4 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 4 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 5 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 5 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 6 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 6 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 7 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 7 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 8 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#include STBIR__HEADER_FILENAME + +#define STBIR__vertical_channels 8 +#define STB_IMAGE_RESIZE_DO_VERTICALS +#define STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#include STBIR__HEADER_FILENAME + +typedef void STBIR_VERTICAL_GATHERFUNC( float * output, float const * coeffs, float const ** inputs, float const * input0_end ); + +static STBIR_VERTICAL_GATHERFUNC * stbir__vertical_gathers[ 8 ] = +{ + stbir__vertical_gather_with_1_coeffs,stbir__vertical_gather_with_2_coeffs,stbir__vertical_gather_with_3_coeffs,stbir__vertical_gather_with_4_coeffs,stbir__vertical_gather_with_5_coeffs,stbir__vertical_gather_with_6_coeffs,stbir__vertical_gather_with_7_coeffs,stbir__vertical_gather_with_8_coeffs +}; + +static STBIR_VERTICAL_GATHERFUNC * stbir__vertical_gathers_continues[ 8 ] = +{ + stbir__vertical_gather_with_1_coeffs_cont,stbir__vertical_gather_with_2_coeffs_cont,stbir__vertical_gather_with_3_coeffs_cont,stbir__vertical_gather_with_4_coeffs_cont,stbir__vertical_gather_with_5_coeffs_cont,stbir__vertical_gather_with_6_coeffs_cont,stbir__vertical_gather_with_7_coeffs_cont,stbir__vertical_gather_with_8_coeffs_cont +}; + +typedef void STBIR_VERTICAL_SCATTERFUNC( float ** outputs, float const * coeffs, float const * input, float const * input_end ); + +static STBIR_VERTICAL_SCATTERFUNC * stbir__vertical_scatter_sets[ 8 ] = +{ + stbir__vertical_scatter_with_1_coeffs,stbir__vertical_scatter_with_2_coeffs,stbir__vertical_scatter_with_3_coeffs,stbir__vertical_scatter_with_4_coeffs,stbir__vertical_scatter_with_5_coeffs,stbir__vertical_scatter_with_6_coeffs,stbir__vertical_scatter_with_7_coeffs,stbir__vertical_scatter_with_8_coeffs +}; + +static STBIR_VERTICAL_SCATTERFUNC * stbir__vertical_scatter_blends[ 8 ] = +{ + stbir__vertical_scatter_with_1_coeffs_cont,stbir__vertical_scatter_with_2_coeffs_cont,stbir__vertical_scatter_with_3_coeffs_cont,stbir__vertical_scatter_with_4_coeffs_cont,stbir__vertical_scatter_with_5_coeffs_cont,stbir__vertical_scatter_with_6_coeffs_cont,stbir__vertical_scatter_with_7_coeffs_cont,stbir__vertical_scatter_with_8_coeffs_cont +}; + + +static void stbir__encode_scanline( stbir__info const * stbir_info, void *output_buffer_data, float * encode_buffer, int row STBIR_ONLY_PROFILE_GET_SPLIT_INFO ) +{ + int num_pixels = stbir_info->horizontal.scale_info.output_sub_size; + int channels = stbir_info->channels; + int width_times_channels = num_pixels * channels; + void * output_buffer; + + // un-alpha weight if we need to + if ( stbir_info->alpha_unweight ) + { + STBIR_PROFILE_START( unalpha ); + stbir_info->alpha_unweight( encode_buffer, width_times_channels ); + STBIR_PROFILE_END( unalpha ); + } + + // write directly into output by default + output_buffer = output_buffer_data; + + // if we have an output callback, we first convert the decode buffer in place (and then hand that to the callback) + if ( stbir_info->out_pixels_cb ) + output_buffer = encode_buffer; + + STBIR_PROFILE_START( encode ); + // convert into the output buffer + stbir_info->encode_pixels( output_buffer, width_times_channels, encode_buffer ); + STBIR_PROFILE_END( encode ); + + // if we have an output callback, call it to send the data + if ( stbir_info->out_pixels_cb ) + stbir_info->out_pixels_cb( output_buffer, num_pixels, row, stbir_info->user_data ); +} + + +// Get the ring buffer pointer for an index +static float* stbir__get_ring_buffer_entry(stbir__info const * stbir_info, stbir__per_split_info const * split_info, int index ) +{ + STBIR_ASSERT( index < stbir_info->ring_buffer_num_entries ); + + #ifdef STBIR__SEPARATE_ALLOCATIONS + return split_info->ring_buffers[ index ]; + #else + return (float*) ( ( (char*) split_info->ring_buffer ) + ( index * stbir_info->ring_buffer_length_bytes ) ); + #endif +} + +// Get the specified scan line from the ring buffer +static float* stbir__get_ring_buffer_scanline(stbir__info const * stbir_info, stbir__per_split_info const * split_info, int get_scanline) +{ + int ring_buffer_index = (split_info->ring_buffer_begin_index + (get_scanline - split_info->ring_buffer_first_scanline)) % stbir_info->ring_buffer_num_entries; + return stbir__get_ring_buffer_entry( stbir_info, split_info, ring_buffer_index ); +} + +static void stbir__resample_horizontal_gather(stbir__info const * stbir_info, float* output_buffer, float const * input_buffer STBIR_ONLY_PROFILE_GET_SPLIT_INFO ) +{ + float const * decode_buffer = input_buffer - ( stbir_info->scanline_extents.conservative.n0 * stbir_info->effective_channels ); + + STBIR_PROFILE_START( horizontal ); + if ( ( stbir_info->horizontal.filter_enum == STBIR_FILTER_POINT_SAMPLE ) && ( stbir_info->horizontal.scale_info.scale == 1.0f ) ) + STBIR_MEMCPY( output_buffer, input_buffer, stbir_info->horizontal.scale_info.output_sub_size * sizeof( float ) * stbir_info->effective_channels ); + else + stbir_info->horizontal_gather_channels( output_buffer, stbir_info->horizontal.scale_info.output_sub_size, decode_buffer, stbir_info->horizontal.contributors, stbir_info->horizontal.coefficients, stbir_info->horizontal.coefficient_width ); + STBIR_PROFILE_END( horizontal ); +} + +static void stbir__resample_vertical_gather(stbir__info const * stbir_info, stbir__per_split_info* split_info, int n, int contrib_n0, int contrib_n1, float const * vertical_coefficients ) +{ + float* encode_buffer = split_info->vertical_buffer; + float* decode_buffer = split_info->decode_buffer; + int vertical_first = stbir_info->vertical_first; + int width = (vertical_first) ? ( stbir_info->scanline_extents.conservative.n1-stbir_info->scanline_extents.conservative.n0+1 ) : stbir_info->horizontal.scale_info.output_sub_size; + int width_times_channels = stbir_info->effective_channels * width; + + STBIR_ASSERT( stbir_info->vertical.is_gather ); + + // loop over the contributing scanlines and scale into the buffer + STBIR_PROFILE_START( vertical ); + { + int k = 0, total = contrib_n1 - contrib_n0 + 1; + STBIR_ASSERT( total > 0 ); + do { + float const * inputs[8]; + int i, cnt = total; if ( cnt > 8 ) cnt = 8; + for( i = 0 ; i < cnt ; i++ ) + inputs[ i ] = stbir__get_ring_buffer_scanline(stbir_info, split_info, k+i+contrib_n0 ); + + // call the N scanlines at a time function (up to 8 scanlines of blending at once) + ((k==0)?stbir__vertical_gathers:stbir__vertical_gathers_continues)[cnt-1]( (vertical_first) ? decode_buffer : encode_buffer, vertical_coefficients + k, inputs, inputs[0] + width_times_channels ); + k += cnt; + total -= cnt; + } while ( total ); + } + STBIR_PROFILE_END( vertical ); + + if ( vertical_first ) + { + // Now resample the gathered vertical data in the horizontal axis into the encode buffer + decode_buffer[ width_times_channels ] = 0.0f; // clear two over for horizontals with a remnant of 3 + decode_buffer[ width_times_channels+1 ] = 0.0f; + stbir__resample_horizontal_gather(stbir_info, encode_buffer, decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); + } + + stbir__encode_scanline( stbir_info, ( (char *) stbir_info->output_data ) + ((size_t)n * (size_t)stbir_info->output_stride_bytes), + encode_buffer, n STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); +} + +static void stbir__decode_and_resample_for_vertical_gather_loop(stbir__info const * stbir_info, stbir__per_split_info* split_info, int n) +{ + int ring_buffer_index; + float* ring_buffer; + + // Decode the nth scanline from the source image into the decode buffer. + stbir__decode_scanline( stbir_info, n, split_info->decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); + + // update new end scanline + split_info->ring_buffer_last_scanline = n; + + // get ring buffer + ring_buffer_index = (split_info->ring_buffer_begin_index + (split_info->ring_buffer_last_scanline - split_info->ring_buffer_first_scanline)) % stbir_info->ring_buffer_num_entries; + ring_buffer = stbir__get_ring_buffer_entry(stbir_info, split_info, ring_buffer_index); + + // Now resample it into the ring buffer. + stbir__resample_horizontal_gather( stbir_info, ring_buffer, split_info->decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); + + // Now it's sitting in the ring buffer ready to be used as source for the vertical sampling. +} + +static void stbir__vertical_gather_loop( stbir__info const * stbir_info, stbir__per_split_info* split_info, int split_count ) +{ + int y, start_output_y, end_output_y; + stbir__contributors* vertical_contributors = stbir_info->vertical.contributors; + float const * vertical_coefficients = stbir_info->vertical.coefficients; + + STBIR_ASSERT( stbir_info->vertical.is_gather ); + + start_output_y = split_info->start_output_y; + end_output_y = split_info[split_count-1].end_output_y; + + vertical_contributors += start_output_y; + vertical_coefficients += start_output_y * stbir_info->vertical.coefficient_width; + + // initialize the ring buffer for gathering + split_info->ring_buffer_begin_index = 0; + split_info->ring_buffer_first_scanline = vertical_contributors->n0; + split_info->ring_buffer_last_scanline = split_info->ring_buffer_first_scanline - 1; // means "empty" + + for (y = start_output_y; y < end_output_y; y++) + { + int in_first_scanline, in_last_scanline; + + in_first_scanline = vertical_contributors->n0; + in_last_scanline = vertical_contributors->n1; + + // make sure the indexing hasn't broken + STBIR_ASSERT( in_first_scanline >= split_info->ring_buffer_first_scanline ); + + // Load in new scanlines + while (in_last_scanline > split_info->ring_buffer_last_scanline) + { + STBIR_ASSERT( ( split_info->ring_buffer_last_scanline - split_info->ring_buffer_first_scanline + 1 ) <= stbir_info->ring_buffer_num_entries ); + + // make sure there was room in the ring buffer when we add new scanlines + if ( ( split_info->ring_buffer_last_scanline - split_info->ring_buffer_first_scanline + 1 ) == stbir_info->ring_buffer_num_entries ) + { + split_info->ring_buffer_first_scanline++; + split_info->ring_buffer_begin_index++; + } + + if ( stbir_info->vertical_first ) + { + float * ring_buffer = stbir__get_ring_buffer_scanline( stbir_info, split_info, ++split_info->ring_buffer_last_scanline ); + // Decode the nth scanline from the source image into the decode buffer. + stbir__decode_scanline( stbir_info, split_info->ring_buffer_last_scanline, ring_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); + } + else + { + stbir__decode_and_resample_for_vertical_gather_loop(stbir_info, split_info, split_info->ring_buffer_last_scanline + 1); + } + } + + // Now all buffers should be ready to write a row of vertical sampling, so do it. + stbir__resample_vertical_gather(stbir_info, split_info, y, in_first_scanline, in_last_scanline, vertical_coefficients ); + + ++vertical_contributors; + vertical_coefficients += stbir_info->vertical.coefficient_width; + } +} + +#define STBIR__FLOAT_EMPTY_MARKER 3.0e+38F +#define STBIR__FLOAT_BUFFER_IS_EMPTY(ptr) ((ptr)[0]==STBIR__FLOAT_EMPTY_MARKER) + +static void stbir__encode_first_scanline_from_scatter(stbir__info const * stbir_info, stbir__per_split_info* split_info) +{ + // evict a scanline out into the output buffer + float* ring_buffer_entry = stbir__get_ring_buffer_entry(stbir_info, split_info, split_info->ring_buffer_begin_index ); + + // dump the scanline out + stbir__encode_scanline( stbir_info, ( (char *)stbir_info->output_data ) + ( (size_t)split_info->ring_buffer_first_scanline * (size_t)stbir_info->output_stride_bytes ), ring_buffer_entry, split_info->ring_buffer_first_scanline STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); + + // mark it as empty + ring_buffer_entry[ 0 ] = STBIR__FLOAT_EMPTY_MARKER; + + // advance the first scanline + split_info->ring_buffer_first_scanline++; + if ( ++split_info->ring_buffer_begin_index == stbir_info->ring_buffer_num_entries ) + split_info->ring_buffer_begin_index = 0; +} + +static void stbir__horizontal_resample_and_encode_first_scanline_from_scatter(stbir__info const * stbir_info, stbir__per_split_info* split_info) +{ + // evict a scanline out into the output buffer + + float* ring_buffer_entry = stbir__get_ring_buffer_entry(stbir_info, split_info, split_info->ring_buffer_begin_index ); + + // Now resample it into the buffer. + stbir__resample_horizontal_gather( stbir_info, split_info->vertical_buffer, ring_buffer_entry STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); + + // dump the scanline out + stbir__encode_scanline( stbir_info, ( (char *)stbir_info->output_data ) + ( (size_t)split_info->ring_buffer_first_scanline * (size_t)stbir_info->output_stride_bytes ), split_info->vertical_buffer, split_info->ring_buffer_first_scanline STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); + + // mark it as empty + ring_buffer_entry[ 0 ] = STBIR__FLOAT_EMPTY_MARKER; + + // advance the first scanline + split_info->ring_buffer_first_scanline++; + if ( ++split_info->ring_buffer_begin_index == stbir_info->ring_buffer_num_entries ) + split_info->ring_buffer_begin_index = 0; +} + +static void stbir__resample_vertical_scatter(stbir__info const * stbir_info, stbir__per_split_info* split_info, int n0, int n1, float const * vertical_coefficients, float const * vertical_buffer, float const * vertical_buffer_end ) +{ + STBIR_ASSERT( !stbir_info->vertical.is_gather ); + + STBIR_PROFILE_START( vertical ); + { + int k = 0, total = n1 - n0 + 1; + STBIR_ASSERT( total > 0 ); + do { + float * outputs[8]; + int i, n = total; if ( n > 8 ) n = 8; + for( i = 0 ; i < n ; i++ ) + { + outputs[ i ] = stbir__get_ring_buffer_scanline(stbir_info, split_info, k+i+n0 ); + if ( ( i ) && ( STBIR__FLOAT_BUFFER_IS_EMPTY( outputs[i] ) != STBIR__FLOAT_BUFFER_IS_EMPTY( outputs[0] ) ) ) // make sure runs are of the same type + { + n = i; + break; + } + } + // call the scatter to N scanlines at a time function (up to 8 scanlines of scattering at once) + ((STBIR__FLOAT_BUFFER_IS_EMPTY( outputs[0] ))?stbir__vertical_scatter_sets:stbir__vertical_scatter_blends)[n-1]( outputs, vertical_coefficients + k, vertical_buffer, vertical_buffer_end ); + k += n; + total -= n; + } while ( total ); + } + + STBIR_PROFILE_END( vertical ); +} + +typedef void stbir__handle_scanline_for_scatter_func(stbir__info const * stbir_info, stbir__per_split_info* split_info); + +static void stbir__vertical_scatter_loop( stbir__info const * stbir_info, stbir__per_split_info* split_info, int split_count ) +{ + int y, start_output_y, end_output_y, start_input_y, end_input_y; + stbir__contributors* vertical_contributors = stbir_info->vertical.contributors; + float const * vertical_coefficients = stbir_info->vertical.coefficients; + stbir__handle_scanline_for_scatter_func * handle_scanline_for_scatter; + void * scanline_scatter_buffer; + void * scanline_scatter_buffer_end; + int on_first_input_y, last_input_y; + int width = (stbir_info->vertical_first) ? ( stbir_info->scanline_extents.conservative.n1-stbir_info->scanline_extents.conservative.n0+1 ) : stbir_info->horizontal.scale_info.output_sub_size; + int width_times_channels = stbir_info->effective_channels * width; + + STBIR_ASSERT( !stbir_info->vertical.is_gather ); + + start_output_y = split_info->start_output_y; + end_output_y = split_info[split_count-1].end_output_y; // may do multiple split counts + + start_input_y = split_info->start_input_y; + end_input_y = split_info[split_count-1].end_input_y; + + // adjust for starting offset start_input_y + y = start_input_y + stbir_info->vertical.filter_pixel_margin; + vertical_contributors += y ; + vertical_coefficients += stbir_info->vertical.coefficient_width * y; + + if ( stbir_info->vertical_first ) + { + handle_scanline_for_scatter = stbir__horizontal_resample_and_encode_first_scanline_from_scatter; + scanline_scatter_buffer = split_info->decode_buffer; + scanline_scatter_buffer_end = ( (char*) scanline_scatter_buffer ) + sizeof( float ) * stbir_info->effective_channels * (stbir_info->scanline_extents.conservative.n1-stbir_info->scanline_extents.conservative.n0+1); + } + else + { + handle_scanline_for_scatter = stbir__encode_first_scanline_from_scatter; + scanline_scatter_buffer = split_info->vertical_buffer; + scanline_scatter_buffer_end = ( (char*) scanline_scatter_buffer ) + sizeof( float ) * stbir_info->effective_channels * stbir_info->horizontal.scale_info.output_sub_size; + } + + // initialize the ring buffer for scattering + split_info->ring_buffer_first_scanline = start_output_y; + split_info->ring_buffer_last_scanline = -1; + split_info->ring_buffer_begin_index = -1; + + // mark all the buffers as empty to start + for( y = 0 ; y < stbir_info->ring_buffer_num_entries ; y++ ) + { + float * decode_buffer = stbir__get_ring_buffer_entry( stbir_info, split_info, y ); + decode_buffer[ width_times_channels ] = 0.0f; // clear two over for horizontals with a remnant of 3 + decode_buffer[ width_times_channels+1 ] = 0.0f; + decode_buffer[0] = STBIR__FLOAT_EMPTY_MARKER; // only used on scatter + } + + // do the loop in input space + on_first_input_y = 1; last_input_y = start_input_y; + for (y = start_input_y ; y < end_input_y; y++) + { + int out_first_scanline, out_last_scanline; + + out_first_scanline = vertical_contributors->n0; + out_last_scanline = vertical_contributors->n1; + + STBIR_ASSERT(out_last_scanline - out_first_scanline + 1 <= stbir_info->ring_buffer_num_entries); + + if ( ( out_last_scanline >= out_first_scanline ) && ( ( ( out_first_scanline >= start_output_y ) && ( out_first_scanline < end_output_y ) ) || ( ( out_last_scanline >= start_output_y ) && ( out_last_scanline < end_output_y ) ) ) ) + { + float const * vc = vertical_coefficients; + + // keep track of the range actually seen for the next resize + last_input_y = y; + if ( ( on_first_input_y ) && ( y > start_input_y ) ) + split_info->start_input_y = y; + on_first_input_y = 0; + + // clip the region + if ( out_first_scanline < start_output_y ) + { + vc += start_output_y - out_first_scanline; + out_first_scanline = start_output_y; + } + + if ( out_last_scanline >= end_output_y ) + out_last_scanline = end_output_y - 1; + + // if very first scanline, init the index + if (split_info->ring_buffer_begin_index < 0) + split_info->ring_buffer_begin_index = out_first_scanline - start_output_y; + + STBIR_ASSERT( split_info->ring_buffer_begin_index <= out_first_scanline ); + + // Decode the nth scanline from the source image into the decode buffer. + stbir__decode_scanline( stbir_info, y, split_info->decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); + + // When horizontal first, we resample horizontally into the vertical buffer before we scatter it out + if ( !stbir_info->vertical_first ) + stbir__resample_horizontal_gather( stbir_info, split_info->vertical_buffer, split_info->decode_buffer STBIR_ONLY_PROFILE_SET_SPLIT_INFO ); + + // Now it's sitting in the buffer ready to be distributed into the ring buffers. + + // evict from the ringbuffer, if we need are full + if ( ( ( split_info->ring_buffer_last_scanline - split_info->ring_buffer_first_scanline + 1 ) == stbir_info->ring_buffer_num_entries ) && + ( out_last_scanline > split_info->ring_buffer_last_scanline ) ) + handle_scanline_for_scatter( stbir_info, split_info ); + + // Now the horizontal buffer is ready to write to all ring buffer rows, so do it. + stbir__resample_vertical_scatter(stbir_info, split_info, out_first_scanline, out_last_scanline, vc, (float*)scanline_scatter_buffer, (float*)scanline_scatter_buffer_end ); + + // update the end of the buffer + if ( out_last_scanline > split_info->ring_buffer_last_scanline ) + split_info->ring_buffer_last_scanline = out_last_scanline; + } + ++vertical_contributors; + vertical_coefficients += stbir_info->vertical.coefficient_width; + } + + // now evict the scanlines that are left over in the ring buffer + while ( split_info->ring_buffer_first_scanline < end_output_y ) + handle_scanline_for_scatter(stbir_info, split_info); + + // update the end_input_y if we do multiple resizes with the same data + ++last_input_y; + for( y = 0 ; y < split_count; y++ ) + if ( split_info[y].end_input_y > last_input_y ) + split_info[y].end_input_y = last_input_y; +} + + +static stbir__kernel_callback * stbir__builtin_kernels[] = { 0, stbir__filter_trapezoid, stbir__filter_triangle, stbir__filter_cubic, stbir__filter_catmullrom, stbir__filter_mitchell, stbir__filter_point }; +static stbir__support_callback * stbir__builtin_supports[] = { 0, stbir__support_trapezoid, stbir__support_one, stbir__support_two, stbir__support_two, stbir__support_two, stbir__support_zeropoint5 }; + +static void stbir__set_sampler(stbir__sampler * samp, stbir_filter filter, stbir__kernel_callback * kernel, stbir__support_callback * support, stbir_edge edge, stbir__scale_info * scale_info, int always_gather, void * user_data ) +{ + // set filter + if (filter == 0) + { + filter = STBIR_DEFAULT_FILTER_DOWNSAMPLE; // default to downsample + if (scale_info->scale >= ( 1.0f - stbir__small_float ) ) + { + if ( (scale_info->scale <= ( 1.0f + stbir__small_float ) ) && ( STBIR_CEILF(scale_info->pixel_shift) == scale_info->pixel_shift ) ) + filter = STBIR_FILTER_POINT_SAMPLE; + else + filter = STBIR_DEFAULT_FILTER_UPSAMPLE; + } + } + samp->filter_enum = filter; + + STBIR_ASSERT(samp->filter_enum != 0); + STBIR_ASSERT((unsigned)samp->filter_enum < STBIR_FILTER_OTHER); + samp->filter_kernel = stbir__builtin_kernels[ filter ]; + samp->filter_support = stbir__builtin_supports[ filter ]; + + if ( kernel && support ) + { + samp->filter_kernel = kernel; + samp->filter_support = support; + samp->filter_enum = STBIR_FILTER_OTHER; + } + + samp->edge = edge; + samp->filter_pixel_width = stbir__get_filter_pixel_width (samp->filter_support, scale_info->scale, user_data ); + // Gather is always better, but in extreme downsamples, you have to most or all of the data in memory + // For horizontal, we always have all the pixels, so we always use gather here (always_gather==1). + // For vertical, we use gather if scaling up (which means we will have samp->filter_pixel_width + // scanlines in memory at once). + samp->is_gather = 0; + if ( scale_info->scale >= ( 1.0f - stbir__small_float ) ) + samp->is_gather = 1; + else if ( ( always_gather ) || ( samp->filter_pixel_width <= STBIR_FORCE_GATHER_FILTER_SCANLINES_AMOUNT ) ) + samp->is_gather = 2; + + // pre calculate stuff based on the above + samp->coefficient_width = stbir__get_coefficient_width(samp, samp->is_gather, user_data); + + // filter_pixel_width is the conservative size in pixels of input that affect an output pixel. + // In rare cases (only with 2 pix to 1 pix with the default filters), it's possible that the + // filter will extend before or after the scanline beyond just one extra entire copy of the + // scanline (we would hit the edge twice). We don't let you do that, so we clamp the total + // width to 3x the total of input pixel (once for the scanline, once for the left side + // overhang, and once for the right side). We only do this for edge mode, since the other + // modes can just re-edge clamp back in again. + if ( edge == STBIR_EDGE_WRAP ) + if ( samp->filter_pixel_width > ( scale_info->input_full_size * 3 ) ) + samp->filter_pixel_width = scale_info->input_full_size * 3; + + // This is how much to expand buffers to account for filters seeking outside + // the image boundaries. + samp->filter_pixel_margin = samp->filter_pixel_width / 2; + + // filter_pixel_margin is the amount that this filter can overhang on just one side of either + // end of the scanline (left or the right). Since we only allow you to overhang 1 scanline's + // worth of pixels, we clamp this one side of overhang to the input scanline size. Again, + // this clamping only happens in rare cases with the default filters (2 pix to 1 pix). + if ( edge == STBIR_EDGE_WRAP ) + if ( samp->filter_pixel_margin > scale_info->input_full_size ) + samp->filter_pixel_margin = scale_info->input_full_size; + + samp->num_contributors = stbir__get_contributors(samp, samp->is_gather); + + samp->contributors_size = samp->num_contributors * sizeof(stbir__contributors); + samp->coefficients_size = samp->num_contributors * samp->coefficient_width * sizeof(float) + sizeof(float)*STBIR_INPUT_CALLBACK_PADDING; // extra sizeof(float) is padding + + samp->gather_prescatter_contributors = 0; + samp->gather_prescatter_coefficients = 0; + if ( samp->is_gather == 0 ) + { + samp->gather_prescatter_coefficient_width = samp->filter_pixel_width; + samp->gather_prescatter_num_contributors = stbir__get_contributors(samp, 2); + samp->gather_prescatter_contributors_size = samp->gather_prescatter_num_contributors * sizeof(stbir__contributors); + samp->gather_prescatter_coefficients_size = samp->gather_prescatter_num_contributors * samp->gather_prescatter_coefficient_width * sizeof(float); + } +} + +static void stbir__get_conservative_extents( stbir__sampler * samp, stbir__contributors * range, void * user_data ) +{ + float scale = samp->scale_info.scale; + float out_shift = samp->scale_info.pixel_shift; + stbir__support_callback * support = samp->filter_support; + int input_full_size = samp->scale_info.input_full_size; + stbir_edge edge = samp->edge; + float inv_scale = samp->scale_info.inv_scale; + + STBIR_ASSERT( samp->is_gather != 0 ); + + if ( samp->is_gather == 1 ) + { + int in_first_pixel, in_last_pixel; + float out_filter_radius = support(inv_scale, user_data) * scale; + + stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, 0.5, out_filter_radius, inv_scale, out_shift, input_full_size, edge ); + range->n0 = in_first_pixel; + stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, ( (float)(samp->scale_info.output_sub_size-1) ) + 0.5f, out_filter_radius, inv_scale, out_shift, input_full_size, edge ); + range->n1 = in_last_pixel; + } + else if ( samp->is_gather == 2 ) // downsample gather, refine + { + float in_pixels_radius = support(scale, user_data) * inv_scale; + int filter_pixel_margin = samp->filter_pixel_margin; + int output_sub_size = samp->scale_info.output_sub_size; + int input_end; + int n; + int in_first_pixel, in_last_pixel; + + // get a conservative area of the input range + stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, 0, 0, inv_scale, out_shift, input_full_size, edge ); + range->n0 = in_first_pixel; + stbir__calculate_in_pixel_range( &in_first_pixel, &in_last_pixel, (float)output_sub_size, 0, inv_scale, out_shift, input_full_size, edge ); + range->n1 = in_last_pixel; + + // now go through the margin to the start of area to find bottom + n = range->n0 + 1; + input_end = -filter_pixel_margin; + while( n >= input_end ) + { + int out_first_pixel, out_last_pixel; + stbir__calculate_out_pixel_range( &out_first_pixel, &out_last_pixel, ((float)n)+0.5f, in_pixels_radius, scale, out_shift, output_sub_size ); + if ( out_first_pixel > out_last_pixel ) + break; + + if ( ( out_first_pixel < output_sub_size ) || ( out_last_pixel >= 0 ) ) + range->n0 = n; + --n; + } + + // now go through the end of the area through the margin to find top + n = range->n1 - 1; + input_end = n + 1 + filter_pixel_margin; + while( n <= input_end ) + { + int out_first_pixel, out_last_pixel; + stbir__calculate_out_pixel_range( &out_first_pixel, &out_last_pixel, ((float)n)+0.5f, in_pixels_radius, scale, out_shift, output_sub_size ); + if ( out_first_pixel > out_last_pixel ) + break; + if ( ( out_first_pixel < output_sub_size ) || ( out_last_pixel >= 0 ) ) + range->n1 = n; + ++n; + } + } + + if ( samp->edge == STBIR_EDGE_WRAP ) + { + // if we are wrapping, and we are very close to the image size (so the edges might merge), just use the scanline up to the edge + if ( ( range->n0 > 0 ) && ( range->n1 >= input_full_size ) ) + { + int marg = range->n1 - input_full_size + 1; + if ( ( marg + STBIR__MERGE_RUNS_PIXEL_THRESHOLD ) >= range->n0 ) + range->n0 = 0; + } + if ( ( range->n0 < 0 ) && ( range->n1 < (input_full_size-1) ) ) + { + int marg = -range->n0; + if ( ( input_full_size - marg - STBIR__MERGE_RUNS_PIXEL_THRESHOLD - 1 ) <= range->n1 ) + range->n1 = input_full_size - 1; + } + } + else + { + // for non-edge-wrap modes, we never read over the edge, so clamp + if ( range->n0 < 0 ) + range->n0 = 0; + if ( range->n1 >= input_full_size ) + range->n1 = input_full_size - 1; + } +} + +static void stbir__get_split_info( stbir__per_split_info* split_info, int splits, int output_height, int vertical_pixel_margin, int input_full_height, int is_gather, stbir__contributors * contribs ) +{ + int i, cur; + int left = output_height; + + cur = 0; + for( i = 0 ; i < splits ; i++ ) + { + int each; + + split_info[i].start_output_y = cur; + each = left / ( splits - i ); + split_info[i].end_output_y = cur + each; + + // ok, when we are gathering, we need to make sure we are starting on a y offset that doesn't have + // a "special" set of coefficients. Basically, with exactly the right filter at exactly the right + // resize at exactly the right phase, some of the coefficents can be zero. When they are zero, we + // don't process them at all. But this leads to a tricky thing with the thread splits, where we + // might have a set of two coeffs like this for example: (4,4) and (3,6). The 4,4 means there was + // just one single coeff because things worked out perfectly (normally, they all have 4 coeffs + // like the range 3,6. The problem is that if we start right on the (4,4) on a brand new thread, + // then when we get to (3,6), we don't have the "3" sample in memory (because we didn't load + // it on the initial (4,4) range because it didn't have a 3 (we only add new samples that are + // larger than our existing samples - it's just how the eviction works). So, our solution here + // is pretty simple, if we start right on a range that has samples that start earlier, then we + // simply bump up our previous thread split range to include it, and then start this threads + // range with the smaller sample. It just moves one scanline from one thread split to another, + // so that we end with the unusual one, instead of start with it. To do this, we check 2-4 + // sample at each thread split start and then occassionally move them. + + if ( ( is_gather ) && ( i ) ) + { + stbir__contributors * small_contribs; + int j, smallest, stop, start_n0; + stbir__contributors * split_contribs = contribs + cur; + + // scan for a max of 3x the filter width or until the next thread split + stop = vertical_pixel_margin * 3; + if ( each < stop ) + stop = each; + + // loops a few times before early out + smallest = 0; + small_contribs = split_contribs; + start_n0 = small_contribs->n0; + for( j = 1 ; j <= stop ; j++ ) + { + ++split_contribs; + if ( split_contribs->n0 > start_n0 ) + break; + if ( split_contribs->n0 < small_contribs->n0 ) + { + small_contribs = split_contribs; + smallest = j; + } + } + + split_info[i-1].end_output_y += smallest; + split_info[i].start_output_y += smallest; + } + + cur += each; + left -= each; + + // scatter range (updated to minimum as you run it) + split_info[i].start_input_y = -vertical_pixel_margin; + split_info[i].end_input_y = input_full_height + vertical_pixel_margin; + } +} + +static void stbir__free_internal_mem( stbir__info *info ) +{ + #define STBIR__FREE_AND_CLEAR( ptr ) { if ( ptr ) { void * p = (ptr); (ptr) = 0; STBIR_FREE( p, info->user_data); } } + + if ( info ) + { + #ifndef STBIR__SEPARATE_ALLOCATIONS + STBIR__FREE_AND_CLEAR( info->alloced_mem ); + #else + int i,j; + + if ( ( info->vertical.gather_prescatter_contributors ) && ( (void*)info->vertical.gather_prescatter_contributors != (void*)info->split_info[0].decode_buffer ) ) + { + STBIR__FREE_AND_CLEAR( info->vertical.gather_prescatter_coefficients ); + STBIR__FREE_AND_CLEAR( info->vertical.gather_prescatter_contributors ); + } + for( i = 0 ; i < info->splits ; i++ ) + { + for( j = 0 ; j < info->alloc_ring_buffer_num_entries ; j++ ) + { + #ifdef STBIR_SIMD8 + if ( info->effective_channels == 3 ) + --info->split_info[i].ring_buffers[j]; // avx in 3 channel mode needs one float at the start of the buffer + #endif + STBIR__FREE_AND_CLEAR( info->split_info[i].ring_buffers[j] ); + } + + #ifdef STBIR_SIMD8 + if ( info->effective_channels == 3 ) + --info->split_info[i].decode_buffer; // avx in 3 channel mode needs one float at the start of the buffer + #endif + STBIR__FREE_AND_CLEAR( info->split_info[i].decode_buffer ); + STBIR__FREE_AND_CLEAR( info->split_info[i].ring_buffers ); + STBIR__FREE_AND_CLEAR( info->split_info[i].vertical_buffer ); + } + STBIR__FREE_AND_CLEAR( info->split_info ); + if ( info->vertical.coefficients != info->horizontal.coefficients ) + { + STBIR__FREE_AND_CLEAR( info->vertical.coefficients ); + STBIR__FREE_AND_CLEAR( info->vertical.contributors ); + } + STBIR__FREE_AND_CLEAR( info->horizontal.coefficients ); + STBIR__FREE_AND_CLEAR( info->horizontal.contributors ); + STBIR__FREE_AND_CLEAR( info->alloced_mem ); + STBIR_FREE( info, info->user_data ); + #endif + } + + #undef STBIR__FREE_AND_CLEAR +} + +static int stbir__get_max_split( int splits, int height ) +{ + int i; + int max = 0; + + for( i = 0 ; i < splits ; i++ ) + { + int each = height / ( splits - i ); + if ( each > max ) + max = each; + height -= each; + } + return max; +} + +static stbir__horizontal_gather_channels_func ** stbir__horizontal_gather_n_coeffs_funcs[8] = +{ + 0, stbir__horizontal_gather_1_channels_with_n_coeffs_funcs, stbir__horizontal_gather_2_channels_with_n_coeffs_funcs, stbir__horizontal_gather_3_channels_with_n_coeffs_funcs, stbir__horizontal_gather_4_channels_with_n_coeffs_funcs, 0,0, stbir__horizontal_gather_7_channels_with_n_coeffs_funcs +}; + +static stbir__horizontal_gather_channels_func ** stbir__horizontal_gather_channels_funcs[8] = +{ + 0, stbir__horizontal_gather_1_channels_funcs, stbir__horizontal_gather_2_channels_funcs, stbir__horizontal_gather_3_channels_funcs, stbir__horizontal_gather_4_channels_funcs, 0,0, stbir__horizontal_gather_7_channels_funcs +}; + +// there are six resize classifications: 0 == vertical scatter, 1 == vertical gather < 1x scale, 2 == vertical gather 1x-2x scale, 4 == vertical gather < 3x scale, 4 == vertical gather > 3x scale, 5 == <=4 pixel height, 6 == <=4 pixel wide column +#define STBIR_RESIZE_CLASSIFICATIONS 8 + +static float stbir__compute_weights[5][STBIR_RESIZE_CLASSIFICATIONS][4]= // 5 = 0=1chan, 1=2chan, 2=3chan, 3=4chan, 4=7chan +{ + { + { 1.00000f, 1.00000f, 0.31250f, 1.00000f }, + { 0.56250f, 0.59375f, 0.00000f, 0.96875f }, + { 1.00000f, 0.06250f, 0.00000f, 1.00000f }, + { 0.00000f, 0.09375f, 1.00000f, 1.00000f }, + { 1.00000f, 1.00000f, 0.31250f, 1.00000f }, + { 0.03125f, 0.12500f, 1.00000f, 1.00000f }, + { 1.00000f, 1.00000f, 0.06250f, 1.00000f }, + { 0.00000f, 1.00000f, 0.00000f, 0.03125f }, + }, { + { 0.00000f, 0.84375f, 0.00000f, 0.03125f }, + { 0.09375f, 0.93750f, 0.00000f, 0.78125f }, + { 0.87500f, 0.21875f, 0.00000f, 0.96875f }, + { 0.09375f, 0.09375f, 1.00000f, 1.00000f }, + { 0.00000f, 0.84375f, 0.00000f, 0.03125f }, + { 0.03125f, 0.12500f, 1.00000f, 1.00000f }, + { 1.00000f, 1.00000f, 0.06250f, 1.00000f }, + { 0.00000f, 1.00000f, 0.00000f, 0.53125f }, + }, { + { 0.00000f, 0.53125f, 0.00000f, 0.03125f }, + { 0.06250f, 0.96875f, 0.00000f, 0.53125f }, + { 0.87500f, 0.18750f, 0.00000f, 0.93750f }, + { 0.00000f, 0.09375f, 1.00000f, 1.00000f }, + { 0.00000f, 0.53125f, 0.00000f, 0.03125f }, + { 0.03125f, 0.12500f, 1.00000f, 1.00000f }, + { 1.00000f, 1.00000f, 0.06250f, 1.00000f }, + { 0.00000f, 1.00000f, 0.00000f, 0.56250f }, + }, { + { 0.00000f, 0.50000f, 0.00000f, 0.71875f }, + { 0.06250f, 0.84375f, 0.00000f, 0.87500f }, + { 1.00000f, 0.50000f, 0.50000f, 0.96875f }, + { 1.00000f, 0.09375f, 0.31250f, 0.50000f }, + { 0.00000f, 0.50000f, 0.00000f, 0.71875f }, + { 1.00000f, 0.03125f, 0.03125f, 0.53125f }, + { 1.00000f, 1.00000f, 0.06250f, 1.00000f }, + { 0.00000f, 1.00000f, 0.03125f, 0.18750f }, + }, { + { 0.00000f, 0.59375f, 0.00000f, 0.96875f }, + { 0.06250f, 0.81250f, 0.06250f, 0.59375f }, + { 0.75000f, 0.43750f, 0.12500f, 0.96875f }, + { 0.87500f, 0.06250f, 0.18750f, 0.43750f }, + { 0.00000f, 0.59375f, 0.00000f, 0.96875f }, + { 0.15625f, 0.12500f, 1.00000f, 1.00000f }, + { 1.00000f, 1.00000f, 0.06250f, 1.00000f }, + { 0.00000f, 1.00000f, 0.03125f, 0.34375f }, + } +}; + +// structure that allow us to query and override info for training the costs +typedef struct STBIR__V_FIRST_INFO +{ + double v_cost, h_cost; + int control_v_first; // 0 = no control, 1 = force hori, 2 = force vert + int v_first; + int v_resize_classification; + int is_gather; +} STBIR__V_FIRST_INFO; + +#ifdef STBIR__V_FIRST_INFO_BUFFER +static STBIR__V_FIRST_INFO STBIR__V_FIRST_INFO_BUFFER = {0}; +#define STBIR__V_FIRST_INFO_POINTER &STBIR__V_FIRST_INFO_BUFFER +#else +#define STBIR__V_FIRST_INFO_POINTER 0 +#endif + +// Figure out whether to scale along the horizontal or vertical first. +// This only *super* important when you are scaling by a massively +// different amount in the vertical vs the horizontal (for example, if +// you are scaling by 2x in the width, and 0.5x in the height, then you +// want to do the vertical scale first, because it's around 3x faster +// in that order. +// +// In more normal circumstances, this makes a 20-40% differences, so +// it's good to get right, but not critical. The normal way that you +// decide which direction goes first is just figuring out which +// direction does more multiplies. But with modern CPUs with their +// fancy caches and SIMD and high IPC abilities, so there's just a lot +// more that goes into it. +// +// My handwavy sort of solution is to have an app that does a whole +// bunch of timing for both vertical and horizontal first modes, +// and then another app that can read lots of these timing files +// and try to search for the best weights to use. Dotimings.c +// is the app that does a bunch of timings, and vf_train.c is the +// app that solves for the best weights (and shows how well it +// does currently). + +static int stbir__should_do_vertical_first( float weights_table[STBIR_RESIZE_CLASSIFICATIONS][4], int horizontal_filter_pixel_width, float horizontal_scale, int horizontal_output_size, int vertical_filter_pixel_width, float vertical_scale, int vertical_output_size, int is_gather, STBIR__V_FIRST_INFO * info ) +{ + double v_cost, h_cost; + float * weights; + int vertical_first; + int v_classification; + + // categorize the resize into buckets + if ( ( vertical_output_size <= 4 ) || ( horizontal_output_size <= 4 ) ) + v_classification = ( vertical_output_size < horizontal_output_size ) ? 6 : 7; + else if ( ( !is_gather ) && ( ( vertical_output_size <= 16 ) || ( horizontal_output_size <= 16 ) ) ) + v_classification = 4; + else if ( vertical_scale <= 1.0f ) + v_classification = ( is_gather ) ? 1 : 0; + else if ( vertical_scale <= 2.0f) + v_classification = 2; + else if ( vertical_scale <= 3.0f) + v_classification = 3; + else + v_classification = 5; // everything bigger than 3x + + // use the right weights + weights = weights_table[ v_classification ]; + + // this is the costs when you don't take into account modern CPUs with high ipc and simd and caches - wish we had a better estimate + h_cost = (float)horizontal_filter_pixel_width * weights[0] + horizontal_scale * (float)vertical_filter_pixel_width * weights[1]; + v_cost = (float)vertical_filter_pixel_width * weights[2] + vertical_scale * (float)horizontal_filter_pixel_width * weights[3]; + + // use computation estimate to decide vertical first or not + vertical_first = ( v_cost <= h_cost ) ? 1 : 0; + + // save these, if requested + if ( info ) + { + info->h_cost = h_cost; + info->v_cost = v_cost; + info->v_resize_classification = v_classification; + info->v_first = vertical_first; + info->is_gather = is_gather; + } + + // and this allows us to override everything for testing (see dotiming.c) + if ( ( info ) && ( info->control_v_first ) ) + vertical_first = ( info->control_v_first == 2 ) ? 1 : 0; + + return vertical_first; +} + +// layout lookups - must match stbir_internal_pixel_layout +static unsigned char stbir__pixel_channels[] = { + 1,2,3,3,4, // 1ch, 2ch, rgb, bgr, 4ch + 4,4,4,4,2,2, // RGBA,BGRA,ARGB,ABGR,RA,AR + 4,4,4,4,2,2, // RGBA_PM,BGRA_PM,ARGB_PM,ABGR_PM,RA_PM,AR_PM +}; + +// the internal pixel layout enums are in a different order, so we can easily do range comparisons of types +// the public pixel layout is ordered in a way that if you cast num_channels (1-4) to the enum, you get something sensible +static stbir_internal_pixel_layout stbir__pixel_layout_convert_public_to_internal[] = { + STBIRI_BGR, STBIRI_1CHANNEL, STBIRI_2CHANNEL, STBIRI_RGB, STBIRI_RGBA, + STBIRI_4CHANNEL, STBIRI_BGRA, STBIRI_ARGB, STBIRI_ABGR, STBIRI_RA, STBIRI_AR, + STBIRI_RGBA_PM, STBIRI_BGRA_PM, STBIRI_ARGB_PM, STBIRI_ABGR_PM, STBIRI_RA_PM, STBIRI_AR_PM, +}; + +static stbir__info * stbir__alloc_internal_mem_and_build_samplers( stbir__sampler * horizontal, stbir__sampler * vertical, stbir__contributors * conservative, stbir_pixel_layout input_pixel_layout_public, stbir_pixel_layout output_pixel_layout_public, int splits, int new_x, int new_y, int fast_alpha, void * user_data STBIR_ONLY_PROFILE_BUILD_GET_INFO ) +{ + static char stbir_channel_count_index[8]={ 9,0,1,2, 3,9,9,4 }; + + stbir__info * info = 0; + void * alloced = 0; + size_t alloced_total = 0; + int vertical_first; + size_t decode_buffer_size, ring_buffer_length_bytes, ring_buffer_size, vertical_buffer_size; + int alloc_ring_buffer_num_entries; + + int alpha_weighting_type = 0; // 0=none, 1=simple, 2=fancy + int conservative_split_output_size = stbir__get_max_split( splits, vertical->scale_info.output_sub_size ); + stbir_internal_pixel_layout input_pixel_layout = stbir__pixel_layout_convert_public_to_internal[ input_pixel_layout_public ]; + stbir_internal_pixel_layout output_pixel_layout = stbir__pixel_layout_convert_public_to_internal[ output_pixel_layout_public ]; + int channels = stbir__pixel_channels[ input_pixel_layout ]; + int effective_channels = channels; + + // first figure out what type of alpha weighting to use (if any) + if ( ( horizontal->filter_enum != STBIR_FILTER_POINT_SAMPLE ) || ( vertical->filter_enum != STBIR_FILTER_POINT_SAMPLE ) ) // no alpha weighting on point sampling + { + if ( ( input_pixel_layout >= STBIRI_RGBA ) && ( input_pixel_layout <= STBIRI_AR ) && ( output_pixel_layout >= STBIRI_RGBA ) && ( output_pixel_layout <= STBIRI_AR ) ) + { + if ( fast_alpha ) + { + alpha_weighting_type = 4; + } + else + { + static int fancy_alpha_effective_cnts[6] = { 7, 7, 7, 7, 3, 3 }; + alpha_weighting_type = 2; + effective_channels = fancy_alpha_effective_cnts[ input_pixel_layout - STBIRI_RGBA ]; + } + } + else if ( ( input_pixel_layout >= STBIRI_RGBA_PM ) && ( input_pixel_layout <= STBIRI_AR_PM ) && ( output_pixel_layout >= STBIRI_RGBA ) && ( output_pixel_layout <= STBIRI_AR ) ) + { + // input premult, output non-premult + alpha_weighting_type = 3; + } + else if ( ( input_pixel_layout >= STBIRI_RGBA ) && ( input_pixel_layout <= STBIRI_AR ) && ( output_pixel_layout >= STBIRI_RGBA_PM ) && ( output_pixel_layout <= STBIRI_AR_PM ) ) + { + // input non-premult, output premult + alpha_weighting_type = 1; + } + } + + // channel in and out count must match currently + if ( channels != stbir__pixel_channels[ output_pixel_layout ] ) + return 0; + + // get vertical first + vertical_first = stbir__should_do_vertical_first( stbir__compute_weights[ (int)stbir_channel_count_index[ effective_channels ] ], horizontal->filter_pixel_width, horizontal->scale_info.scale, horizontal->scale_info.output_sub_size, vertical->filter_pixel_width, vertical->scale_info.scale, vertical->scale_info.output_sub_size, vertical->is_gather, STBIR__V_FIRST_INFO_POINTER ); + + // sometimes read one float off in some of the unrolled loops (with a weight of zero coeff, so it doesn't have an effect) + // we use a few extra floats instead of just 1, so that input callback buffer can overlap with the decode buffer without + // the conversion routines overwriting the callback input data. + decode_buffer_size = ( conservative->n1 - conservative->n0 + 1 ) * effective_channels * sizeof(float) + sizeof(float)*STBIR_INPUT_CALLBACK_PADDING; // extra floats for input callback stagger + +#if defined( STBIR__SEPARATE_ALLOCATIONS ) && defined(STBIR_SIMD8) + if ( effective_channels == 3 ) + decode_buffer_size += sizeof(float); // avx in 3 channel mode needs one float at the start of the buffer (only with separate allocations) +#endif + + ring_buffer_length_bytes = (size_t)horizontal->scale_info.output_sub_size * (size_t)effective_channels * sizeof(float) + sizeof(float)*STBIR_INPUT_CALLBACK_PADDING; // extra floats for padding + + // if we do vertical first, the ring buffer holds a whole decoded line + if ( vertical_first ) + ring_buffer_length_bytes = ( decode_buffer_size + 15 ) & ~15; + + if ( ( ring_buffer_length_bytes & 4095 ) == 0 ) ring_buffer_length_bytes += 64*3; // avoid 4k alias + + // One extra entry because floating point precision problems sometimes cause an extra to be necessary. + alloc_ring_buffer_num_entries = vertical->filter_pixel_width + 1; + + // we never need more ring buffer entries than the scanlines we're outputting when in scatter mode + if ( ( !vertical->is_gather ) && ( alloc_ring_buffer_num_entries > conservative_split_output_size ) ) + alloc_ring_buffer_num_entries = conservative_split_output_size; + + ring_buffer_size = (size_t)alloc_ring_buffer_num_entries * (size_t)ring_buffer_length_bytes; + + // The vertical buffer is used differently, depending on whether we are scattering + // the vertical scanlines, or gathering them. + // If scattering, it's used at the temp buffer to accumulate each output. + // If gathering, it's just the output buffer. + vertical_buffer_size = (size_t)horizontal->scale_info.output_sub_size * (size_t)effective_channels * sizeof(float) + sizeof(float); // extra float for padding + + // we make two passes through this loop, 1st to add everything up, 2nd to allocate and init + for(;;) + { + int i; + void * advance_mem = alloced; + int copy_horizontal = 0; + stbir__sampler * possibly_use_horizontal_for_pivot = 0; + +#ifdef STBIR__SEPARATE_ALLOCATIONS + #define STBIR__NEXT_PTR( ptr, size, ntype ) if ( alloced ) { void * p = STBIR_MALLOC( size, user_data); if ( p == 0 ) { stbir__free_internal_mem( info ); return 0; } (ptr) = (ntype*)p; } +#else + #define STBIR__NEXT_PTR( ptr, size, ntype ) advance_mem = (void*) ( ( ((size_t)advance_mem) + 15 ) & ~15 ); if ( alloced ) ptr = (ntype*)advance_mem; advance_mem = (char*)(((size_t)advance_mem) + (size)); +#endif + + STBIR__NEXT_PTR( info, sizeof( stbir__info ), stbir__info ); + + STBIR__NEXT_PTR( info->split_info, sizeof( stbir__per_split_info ) * splits, stbir__per_split_info ); + + if ( info ) + { + static stbir__alpha_weight_func * fancy_alpha_weights[6] = { stbir__fancy_alpha_weight_4ch, stbir__fancy_alpha_weight_4ch, stbir__fancy_alpha_weight_4ch, stbir__fancy_alpha_weight_4ch, stbir__fancy_alpha_weight_2ch, stbir__fancy_alpha_weight_2ch }; + static stbir__alpha_unweight_func * fancy_alpha_unweights[6] = { stbir__fancy_alpha_unweight_4ch, stbir__fancy_alpha_unweight_4ch, stbir__fancy_alpha_unweight_4ch, stbir__fancy_alpha_unweight_4ch, stbir__fancy_alpha_unweight_2ch, stbir__fancy_alpha_unweight_2ch }; + static stbir__alpha_weight_func * simple_alpha_weights[6] = { stbir__simple_alpha_weight_4ch, stbir__simple_alpha_weight_4ch, stbir__simple_alpha_weight_4ch, stbir__simple_alpha_weight_4ch, stbir__simple_alpha_weight_2ch, stbir__simple_alpha_weight_2ch }; + static stbir__alpha_unweight_func * simple_alpha_unweights[6] = { stbir__simple_alpha_unweight_4ch, stbir__simple_alpha_unweight_4ch, stbir__simple_alpha_unweight_4ch, stbir__simple_alpha_unweight_4ch, stbir__simple_alpha_unweight_2ch, stbir__simple_alpha_unweight_2ch }; + + // initialize info fields + info->alloced_mem = alloced; + info->alloced_total = alloced_total; + + info->channels = channels; + info->effective_channels = effective_channels; + + info->offset_x = new_x; + info->offset_y = new_y; + info->alloc_ring_buffer_num_entries = (int)alloc_ring_buffer_num_entries; + info->ring_buffer_num_entries = 0; + info->ring_buffer_length_bytes = (int)ring_buffer_length_bytes; + info->splits = splits; + info->vertical_first = vertical_first; + + info->input_pixel_layout_internal = input_pixel_layout; + info->output_pixel_layout_internal = output_pixel_layout; + + // setup alpha weight functions + info->alpha_weight = 0; + info->alpha_unweight = 0; + + // handle alpha weighting functions and overrides + if ( alpha_weighting_type == 2 ) + { + // high quality alpha multiplying on the way in, dividing on the way out + info->alpha_weight = fancy_alpha_weights[ input_pixel_layout - STBIRI_RGBA ]; + info->alpha_unweight = fancy_alpha_unweights[ output_pixel_layout - STBIRI_RGBA ]; + } + else if ( alpha_weighting_type == 4 ) + { + // fast alpha multiplying on the way in, dividing on the way out + info->alpha_weight = simple_alpha_weights[ input_pixel_layout - STBIRI_RGBA ]; + info->alpha_unweight = simple_alpha_unweights[ output_pixel_layout - STBIRI_RGBA ]; + } + else if ( alpha_weighting_type == 1 ) + { + // fast alpha on the way in, leave in premultiplied form on way out + info->alpha_weight = simple_alpha_weights[ input_pixel_layout - STBIRI_RGBA ]; + } + else if ( alpha_weighting_type == 3 ) + { + // incoming is premultiplied, fast alpha dividing on the way out - non-premultiplied output + info->alpha_unweight = simple_alpha_unweights[ output_pixel_layout - STBIRI_RGBA ]; + } + + // handle 3-chan color flipping, using the alpha weight path + if ( ( ( input_pixel_layout == STBIRI_RGB ) && ( output_pixel_layout == STBIRI_BGR ) ) || + ( ( input_pixel_layout == STBIRI_BGR ) && ( output_pixel_layout == STBIRI_RGB ) ) ) + { + // do the flipping on the smaller of the two ends + if ( horizontal->scale_info.scale < 1.0f ) + info->alpha_unweight = stbir__simple_flip_3ch; + else + info->alpha_weight = stbir__simple_flip_3ch; + } + + } + + // get all the per-split buffers + for( i = 0 ; i < splits ; i++ ) + { + STBIR__NEXT_PTR( info->split_info[i].decode_buffer, decode_buffer_size, float ); + +#ifdef STBIR__SEPARATE_ALLOCATIONS + + #ifdef STBIR_SIMD8 + if ( ( info ) && ( effective_channels == 3 ) ) + ++info->split_info[i].decode_buffer; // avx in 3 channel mode needs one float at the start of the buffer + #endif + + STBIR__NEXT_PTR( info->split_info[i].ring_buffers, alloc_ring_buffer_num_entries * sizeof(float*), float* ); + { + int j; + for( j = 0 ; j < alloc_ring_buffer_num_entries ; j++ ) + { + STBIR__NEXT_PTR( info->split_info[i].ring_buffers[j], ring_buffer_length_bytes, float ); + #ifdef STBIR_SIMD8 + if ( ( info ) && ( effective_channels == 3 ) ) + ++info->split_info[i].ring_buffers[j]; // avx in 3 channel mode needs one float at the start of the buffer + #endif + } + } +#else + STBIR__NEXT_PTR( info->split_info[i].ring_buffer, ring_buffer_size, float ); +#endif + STBIR__NEXT_PTR( info->split_info[i].vertical_buffer, vertical_buffer_size, float ); + } + + // alloc memory for to-be-pivoted coeffs (if necessary) + if ( vertical->is_gather == 0 ) + { + size_t both; + size_t temp_mem_amt; + + // when in vertical scatter mode, we first build the coefficients in gather mode, and then pivot after, + // that means we need two buffers, so we try to use the decode buffer and ring buffer for this. if that + // is too small, we just allocate extra memory to use as this temp. + + both = (size_t)vertical->gather_prescatter_contributors_size + (size_t)vertical->gather_prescatter_coefficients_size; + +#ifdef STBIR__SEPARATE_ALLOCATIONS + temp_mem_amt = decode_buffer_size; + + #ifdef STBIR_SIMD8 + if ( effective_channels == 3 ) + --temp_mem_amt; // avx in 3 channel mode needs one float at the start of the buffer + #endif +#else + temp_mem_amt = (size_t)( decode_buffer_size + ring_buffer_size + vertical_buffer_size ) * (size_t)splits; +#endif + if ( temp_mem_amt >= both ) + { + if ( info ) + { + vertical->gather_prescatter_contributors = (stbir__contributors*)info->split_info[0].decode_buffer; + vertical->gather_prescatter_coefficients = (float*) ( ( (char*)info->split_info[0].decode_buffer ) + vertical->gather_prescatter_contributors_size ); + } + } + else + { + // ring+decode memory is too small, so allocate temp memory + STBIR__NEXT_PTR( vertical->gather_prescatter_contributors, vertical->gather_prescatter_contributors_size, stbir__contributors ); + STBIR__NEXT_PTR( vertical->gather_prescatter_coefficients, vertical->gather_prescatter_coefficients_size, float ); + } + } + + STBIR__NEXT_PTR( horizontal->contributors, horizontal->contributors_size, stbir__contributors ); + STBIR__NEXT_PTR( horizontal->coefficients, horizontal->coefficients_size, float ); + + // are the two filters identical?? (happens a lot with mipmap generation) + if ( ( horizontal->filter_kernel == vertical->filter_kernel ) && ( horizontal->filter_support == vertical->filter_support ) && ( horizontal->edge == vertical->edge ) && ( horizontal->scale_info.output_sub_size == vertical->scale_info.output_sub_size ) ) + { + float diff_scale = horizontal->scale_info.scale - vertical->scale_info.scale; + float diff_shift = horizontal->scale_info.pixel_shift - vertical->scale_info.pixel_shift; + if ( diff_scale < 0.0f ) diff_scale = -diff_scale; + if ( diff_shift < 0.0f ) diff_shift = -diff_shift; + if ( ( diff_scale <= stbir__small_float ) && ( diff_shift <= stbir__small_float ) ) + { + if ( horizontal->is_gather == vertical->is_gather ) + { + copy_horizontal = 1; + goto no_vert_alloc; + } + // everything matches, but vertical is scatter, horizontal is gather, use horizontal coeffs for vertical pivot coeffs + possibly_use_horizontal_for_pivot = horizontal; + } + } + + STBIR__NEXT_PTR( vertical->contributors, vertical->contributors_size, stbir__contributors ); + STBIR__NEXT_PTR( vertical->coefficients, vertical->coefficients_size, float ); + + no_vert_alloc: + + if ( info ) + { + STBIR_PROFILE_BUILD_START( horizontal ); + + stbir__calculate_filters( horizontal, 0, user_data STBIR_ONLY_PROFILE_BUILD_SET_INFO ); + + // setup the horizontal gather functions + // start with defaulting to the n_coeffs functions (specialized on channels and remnant leftover) + info->horizontal_gather_channels = stbir__horizontal_gather_n_coeffs_funcs[ effective_channels ][ horizontal->extent_info.widest & 3 ]; + // but if the number of coeffs <= 12, use another set of special cases. <=12 coeffs is any enlarging resize, or shrinking resize down to about 1/3 size + if ( horizontal->extent_info.widest <= 12 ) + info->horizontal_gather_channels = stbir__horizontal_gather_channels_funcs[ effective_channels ][ horizontal->extent_info.widest - 1 ]; + + info->scanline_extents.conservative.n0 = conservative->n0; + info->scanline_extents.conservative.n1 = conservative->n1; + + // get exact extents + stbir__get_extents( horizontal, &info->scanline_extents ); + + // pack the horizontal coeffs + horizontal->coefficient_width = stbir__pack_coefficients(horizontal->num_contributors, horizontal->contributors, horizontal->coefficients, horizontal->coefficient_width, horizontal->extent_info.widest, info->scanline_extents.conservative.n0, info->scanline_extents.conservative.n1 ); + + STBIR_MEMCPY( &info->horizontal, horizontal, sizeof( stbir__sampler ) ); + + STBIR_PROFILE_BUILD_END( horizontal ); + + if ( copy_horizontal ) + { + STBIR_MEMCPY( &info->vertical, horizontal, sizeof( stbir__sampler ) ); + } + else + { + STBIR_PROFILE_BUILD_START( vertical ); + + stbir__calculate_filters( vertical, possibly_use_horizontal_for_pivot, user_data STBIR_ONLY_PROFILE_BUILD_SET_INFO ); + STBIR_MEMCPY( &info->vertical, vertical, sizeof( stbir__sampler ) ); + + STBIR_PROFILE_BUILD_END( vertical ); + } + + // setup the vertical split ranges + stbir__get_split_info( info->split_info, info->splits, info->vertical.scale_info.output_sub_size, info->vertical.filter_pixel_margin, info->vertical.scale_info.input_full_size, info->vertical.is_gather, info->vertical.contributors ); + + // now we know precisely how many entries we need + info->ring_buffer_num_entries = info->vertical.extent_info.widest; + + // we never need more ring buffer entries than the scanlines we're outputting + if ( ( !info->vertical.is_gather ) && ( info->ring_buffer_num_entries > conservative_split_output_size ) ) + info->ring_buffer_num_entries = conservative_split_output_size; + STBIR_ASSERT( info->ring_buffer_num_entries <= info->alloc_ring_buffer_num_entries ); + } + #undef STBIR__NEXT_PTR + + + // is this the first time through loop? + if ( info == 0 ) + { + alloced_total = ( 15 + (size_t)advance_mem ); + alloced = STBIR_MALLOC( alloced_total, user_data ); + if ( alloced == 0 ) + return 0; + } + else + return info; // success + } +} + +static int stbir__perform_resize( stbir__info const * info, int split_start, int split_count ) +{ + stbir__per_split_info * split_info = info->split_info + split_start; + + STBIR_PROFILE_CLEAR_EXTRAS(); + + STBIR_PROFILE_FIRST_START( looping ); + if (info->vertical.is_gather) + stbir__vertical_gather_loop( info, split_info, split_count ); + else + stbir__vertical_scatter_loop( info, split_info, split_count ); + STBIR_PROFILE_END( looping ); + + return 1; +} + +static void stbir__update_info_from_resize( stbir__info * info, STBIR_RESIZE * resize ) +{ + static stbir__decode_pixels_func * decode_simple[STBIR_TYPE_HALF_FLOAT-STBIR_TYPE_UINT8_SRGB+1]= + { + /* 1ch-4ch */ stbir__decode_uint8_srgb, stbir__decode_uint8_srgb, 0, stbir__decode_float_linear, stbir__decode_half_float_linear, + }; + + static stbir__decode_pixels_func * decode_alphas[STBIRI_AR-STBIRI_RGBA+1][STBIR_TYPE_HALF_FLOAT-STBIR_TYPE_UINT8_SRGB+1]= + { + { /* RGBA */ stbir__decode_uint8_srgb4_linearalpha, stbir__decode_uint8_srgb, 0, stbir__decode_float_linear, stbir__decode_half_float_linear }, + { /* BGRA */ stbir__decode_uint8_srgb4_linearalpha_BGRA, stbir__decode_uint8_srgb_BGRA, 0, stbir__decode_float_linear_BGRA, stbir__decode_half_float_linear_BGRA }, + { /* ARGB */ stbir__decode_uint8_srgb4_linearalpha_ARGB, stbir__decode_uint8_srgb_ARGB, 0, stbir__decode_float_linear_ARGB, stbir__decode_half_float_linear_ARGB }, + { /* ABGR */ stbir__decode_uint8_srgb4_linearalpha_ABGR, stbir__decode_uint8_srgb_ABGR, 0, stbir__decode_float_linear_ABGR, stbir__decode_half_float_linear_ABGR }, + { /* RA */ stbir__decode_uint8_srgb2_linearalpha, stbir__decode_uint8_srgb, 0, stbir__decode_float_linear, stbir__decode_half_float_linear }, + { /* AR */ stbir__decode_uint8_srgb2_linearalpha_AR, stbir__decode_uint8_srgb_AR, 0, stbir__decode_float_linear_AR, stbir__decode_half_float_linear_AR }, + }; + + static stbir__decode_pixels_func * decode_simple_scaled_or_not[2][2]= + { + { stbir__decode_uint8_linear_scaled, stbir__decode_uint8_linear }, { stbir__decode_uint16_linear_scaled, stbir__decode_uint16_linear }, + }; + + static stbir__decode_pixels_func * decode_alphas_scaled_or_not[STBIRI_AR-STBIRI_RGBA+1][2][2]= + { + { /* RGBA */ { stbir__decode_uint8_linear_scaled, stbir__decode_uint8_linear }, { stbir__decode_uint16_linear_scaled, stbir__decode_uint16_linear } }, + { /* BGRA */ { stbir__decode_uint8_linear_scaled_BGRA, stbir__decode_uint8_linear_BGRA }, { stbir__decode_uint16_linear_scaled_BGRA, stbir__decode_uint16_linear_BGRA } }, + { /* ARGB */ { stbir__decode_uint8_linear_scaled_ARGB, stbir__decode_uint8_linear_ARGB }, { stbir__decode_uint16_linear_scaled_ARGB, stbir__decode_uint16_linear_ARGB } }, + { /* ABGR */ { stbir__decode_uint8_linear_scaled_ABGR, stbir__decode_uint8_linear_ABGR }, { stbir__decode_uint16_linear_scaled_ABGR, stbir__decode_uint16_linear_ABGR } }, + { /* RA */ { stbir__decode_uint8_linear_scaled, stbir__decode_uint8_linear }, { stbir__decode_uint16_linear_scaled, stbir__decode_uint16_linear } }, + { /* AR */ { stbir__decode_uint8_linear_scaled_AR, stbir__decode_uint8_linear_AR }, { stbir__decode_uint16_linear_scaled_AR, stbir__decode_uint16_linear_AR } } + }; + + static stbir__encode_pixels_func * encode_simple[STBIR_TYPE_HALF_FLOAT-STBIR_TYPE_UINT8_SRGB+1]= + { + /* 1ch-4ch */ stbir__encode_uint8_srgb, stbir__encode_uint8_srgb, 0, stbir__encode_float_linear, stbir__encode_half_float_linear, + }; + + static stbir__encode_pixels_func * encode_alphas[STBIRI_AR-STBIRI_RGBA+1][STBIR_TYPE_HALF_FLOAT-STBIR_TYPE_UINT8_SRGB+1]= + { + { /* RGBA */ stbir__encode_uint8_srgb4_linearalpha, stbir__encode_uint8_srgb, 0, stbir__encode_float_linear, stbir__encode_half_float_linear }, + { /* BGRA */ stbir__encode_uint8_srgb4_linearalpha_BGRA, stbir__encode_uint8_srgb_BGRA, 0, stbir__encode_float_linear_BGRA, stbir__encode_half_float_linear_BGRA }, + { /* ARGB */ stbir__encode_uint8_srgb4_linearalpha_ARGB, stbir__encode_uint8_srgb_ARGB, 0, stbir__encode_float_linear_ARGB, stbir__encode_half_float_linear_ARGB }, + { /* ABGR */ stbir__encode_uint8_srgb4_linearalpha_ABGR, stbir__encode_uint8_srgb_ABGR, 0, stbir__encode_float_linear_ABGR, stbir__encode_half_float_linear_ABGR }, + { /* RA */ stbir__encode_uint8_srgb2_linearalpha, stbir__encode_uint8_srgb, 0, stbir__encode_float_linear, stbir__encode_half_float_linear }, + { /* AR */ stbir__encode_uint8_srgb2_linearalpha_AR, stbir__encode_uint8_srgb_AR, 0, stbir__encode_float_linear_AR, stbir__encode_half_float_linear_AR } + }; + + static stbir__encode_pixels_func * encode_simple_scaled_or_not[2][2]= + { + { stbir__encode_uint8_linear_scaled, stbir__encode_uint8_linear }, { stbir__encode_uint16_linear_scaled, stbir__encode_uint16_linear }, + }; + + static stbir__encode_pixels_func * encode_alphas_scaled_or_not[STBIRI_AR-STBIRI_RGBA+1][2][2]= + { + { /* RGBA */ { stbir__encode_uint8_linear_scaled, stbir__encode_uint8_linear }, { stbir__encode_uint16_linear_scaled, stbir__encode_uint16_linear } }, + { /* BGRA */ { stbir__encode_uint8_linear_scaled_BGRA, stbir__encode_uint8_linear_BGRA }, { stbir__encode_uint16_linear_scaled_BGRA, stbir__encode_uint16_linear_BGRA } }, + { /* ARGB */ { stbir__encode_uint8_linear_scaled_ARGB, stbir__encode_uint8_linear_ARGB }, { stbir__encode_uint16_linear_scaled_ARGB, stbir__encode_uint16_linear_ARGB } }, + { /* ABGR */ { stbir__encode_uint8_linear_scaled_ABGR, stbir__encode_uint8_linear_ABGR }, { stbir__encode_uint16_linear_scaled_ABGR, stbir__encode_uint16_linear_ABGR } }, + { /* RA */ { stbir__encode_uint8_linear_scaled, stbir__encode_uint8_linear }, { stbir__encode_uint16_linear_scaled, stbir__encode_uint16_linear } }, + { /* AR */ { stbir__encode_uint8_linear_scaled_AR, stbir__encode_uint8_linear_AR }, { stbir__encode_uint16_linear_scaled_AR, stbir__encode_uint16_linear_AR } } + }; + + stbir__decode_pixels_func * decode_pixels = 0; + stbir__encode_pixels_func * encode_pixels = 0; + stbir_datatype input_type, output_type; + + input_type = resize->input_data_type; + output_type = resize->output_data_type; + info->input_data = resize->input_pixels; + info->input_stride_bytes = resize->input_stride_in_bytes; + info->output_stride_bytes = resize->output_stride_in_bytes; + + // if we're completely point sampling, then we can turn off SRGB + if ( ( info->horizontal.filter_enum == STBIR_FILTER_POINT_SAMPLE ) && ( info->vertical.filter_enum == STBIR_FILTER_POINT_SAMPLE ) ) + { + if ( ( ( input_type == STBIR_TYPE_UINT8_SRGB ) || ( input_type == STBIR_TYPE_UINT8_SRGB_ALPHA ) ) && + ( ( output_type == STBIR_TYPE_UINT8_SRGB ) || ( output_type == STBIR_TYPE_UINT8_SRGB_ALPHA ) ) ) + { + input_type = STBIR_TYPE_UINT8; + output_type = STBIR_TYPE_UINT8; + } + } + + // recalc the output and input strides + if ( info->input_stride_bytes == 0 ) + info->input_stride_bytes = info->channels * info->horizontal.scale_info.input_full_size * stbir__type_size[input_type]; + + if ( info->output_stride_bytes == 0 ) + info->output_stride_bytes = info->channels * info->horizontal.scale_info.output_sub_size * stbir__type_size[output_type]; + + // calc offset + info->output_data = ( (char*) resize->output_pixels ) + ( (size_t) info->offset_y * (size_t) resize->output_stride_in_bytes ) + ( info->offset_x * info->channels * stbir__type_size[output_type] ); + + info->in_pixels_cb = resize->input_cb; + info->user_data = resize->user_data; + info->out_pixels_cb = resize->output_cb; + + // setup the input format converters + if ( ( input_type == STBIR_TYPE_UINT8 ) || ( input_type == STBIR_TYPE_UINT16 ) ) + { + int non_scaled = 0; + + // check if we can run unscaled - 0-255.0/0-65535.0 instead of 0-1.0 (which is a tiny bit faster when doing linear 8->8 or 16->16) + if ( ( !info->alpha_weight ) && ( !info->alpha_unweight ) ) // don't short circuit when alpha weighting (get everything to 0-1.0 as usual) + if ( ( ( input_type == STBIR_TYPE_UINT8 ) && ( output_type == STBIR_TYPE_UINT8 ) ) || ( ( input_type == STBIR_TYPE_UINT16 ) && ( output_type == STBIR_TYPE_UINT16 ) ) ) + non_scaled = 1; + + if ( info->input_pixel_layout_internal <= STBIRI_4CHANNEL ) + decode_pixels = decode_simple_scaled_or_not[ input_type == STBIR_TYPE_UINT16 ][ non_scaled ]; + else + decode_pixels = decode_alphas_scaled_or_not[ ( info->input_pixel_layout_internal - STBIRI_RGBA ) % ( STBIRI_AR-STBIRI_RGBA+1 ) ][ input_type == STBIR_TYPE_UINT16 ][ non_scaled ]; + } + else + { + if ( info->input_pixel_layout_internal <= STBIRI_4CHANNEL ) + decode_pixels = decode_simple[ input_type - STBIR_TYPE_UINT8_SRGB ]; + else + decode_pixels = decode_alphas[ ( info->input_pixel_layout_internal - STBIRI_RGBA ) % ( STBIRI_AR-STBIRI_RGBA+1 ) ][ input_type - STBIR_TYPE_UINT8_SRGB ]; + } + + // setup the output format converters + if ( ( output_type == STBIR_TYPE_UINT8 ) || ( output_type == STBIR_TYPE_UINT16 ) ) + { + int non_scaled = 0; + + // check if we can run unscaled - 0-255.0/0-65535.0 instead of 0-1.0 (which is a tiny bit faster when doing linear 8->8 or 16->16) + if ( ( !info->alpha_weight ) && ( !info->alpha_unweight ) ) // don't short circuit when alpha weighting (get everything to 0-1.0 as usual) + if ( ( ( input_type == STBIR_TYPE_UINT8 ) && ( output_type == STBIR_TYPE_UINT8 ) ) || ( ( input_type == STBIR_TYPE_UINT16 ) && ( output_type == STBIR_TYPE_UINT16 ) ) ) + non_scaled = 1; + + if ( info->output_pixel_layout_internal <= STBIRI_4CHANNEL ) + encode_pixels = encode_simple_scaled_or_not[ output_type == STBIR_TYPE_UINT16 ][ non_scaled ]; + else + encode_pixels = encode_alphas_scaled_or_not[ ( info->output_pixel_layout_internal - STBIRI_RGBA ) % ( STBIRI_AR-STBIRI_RGBA+1 ) ][ output_type == STBIR_TYPE_UINT16 ][ non_scaled ]; + } + else + { + if ( info->output_pixel_layout_internal <= STBIRI_4CHANNEL ) + encode_pixels = encode_simple[ output_type - STBIR_TYPE_UINT8_SRGB ]; + else + encode_pixels = encode_alphas[ ( info->output_pixel_layout_internal - STBIRI_RGBA ) % ( STBIRI_AR-STBIRI_RGBA+1 ) ][ output_type - STBIR_TYPE_UINT8_SRGB ]; + } + + info->input_type = input_type; + info->output_type = output_type; + info->decode_pixels = decode_pixels; + info->encode_pixels = encode_pixels; +} + +static void stbir__clip( int * outx, int * outsubw, int outw, double * u0, double * u1 ) +{ + double per, adj; + int over; + + // do left/top edge + if ( *outx < 0 ) + { + per = ( (double)*outx ) / ( (double)*outsubw ); // is negative + adj = per * ( *u1 - *u0 ); + *u0 -= adj; // increases u0 + *outx = 0; + } + + // do right/bot edge + over = outw - ( *outx + *outsubw ); + if ( over < 0 ) + { + per = ( (double)over ) / ( (double)*outsubw ); // is negative + adj = per * ( *u1 - *u0 ); + *u1 += adj; // decrease u1 + *outsubw = outw - *outx; + } +} + +// converts a double to a rational that has less than one float bit of error (returns 0 if unable to do so) +static int stbir__double_to_rational(double f, stbir_uint32 limit, stbir_uint32 *numer, stbir_uint32 *denom, int limit_denom ) // limit_denom (1) or limit numer (0) +{ + double err; + stbir_uint64 top, bot; + stbir_uint64 numer_last = 0; + stbir_uint64 denom_last = 1; + stbir_uint64 numer_estimate = 1; + stbir_uint64 denom_estimate = 0; + + // scale to past float error range + top = (stbir_uint64)( f * (double)(1 << 25) ); + bot = 1 << 25; + + // keep refining, but usually stops in a few loops - usually 5 for bad cases + for(;;) + { + stbir_uint64 est, temp; + + // hit limit, break out and do best full range estimate + if ( ( ( limit_denom ) ? denom_estimate : numer_estimate ) >= limit ) + break; + + // is the current error less than 1 bit of a float? if so, we're done + if ( denom_estimate ) + { + err = ( (double)numer_estimate / (double)denom_estimate ) - f; + if ( err < 0.0 ) err = -err; + if ( err < ( 1.0 / (double)(1<<24) ) ) + { + // yup, found it + *numer = (stbir_uint32) numer_estimate; + *denom = (stbir_uint32) denom_estimate; + return 1; + } + } + + // no more refinement bits left? break out and do full range estimate + if ( bot == 0 ) + break; + + // gcd the estimate bits + est = top / bot; + temp = top % bot; + top = bot; + bot = temp; + + // move remainders + temp = est * denom_estimate + denom_last; + denom_last = denom_estimate; + denom_estimate = temp; + + // move remainders + temp = est * numer_estimate + numer_last; + numer_last = numer_estimate; + numer_estimate = temp; + } + + // we didn't find anything good enough for float, use a full range estimate + if ( limit_denom ) + { + numer_estimate= (stbir_uint64)( f * (double)limit + 0.5 ); + denom_estimate = limit; + } + else + { + numer_estimate = limit; + denom_estimate = (stbir_uint64)( ( (double)limit / f ) + 0.5 ); + } + + *numer = (stbir_uint32) numer_estimate; + *denom = (stbir_uint32) denom_estimate; + + err = ( denom_estimate ) ? ( ( (double)(stbir_uint32)numer_estimate / (double)(stbir_uint32)denom_estimate ) - f ) : 1.0; + if ( err < 0.0 ) err = -err; + return ( err < ( 1.0 / (double)(1<<24) ) ) ? 1 : 0; +} + +static int stbir__calculate_region_transform( stbir__scale_info * scale_info, int output_full_range, int * output_offset, int output_sub_range, int input_full_range, double input_s0, double input_s1 ) +{ + double output_range, input_range, output_s, input_s, ratio, scale; + + input_s = input_s1 - input_s0; + + // null area + if ( ( output_full_range == 0 ) || ( input_full_range == 0 ) || + ( output_sub_range == 0 ) || ( input_s <= stbir__small_float ) ) + return 0; + + // are either of the ranges completely out of bounds? + if ( ( *output_offset >= output_full_range ) || ( ( *output_offset + output_sub_range ) <= 0 ) || ( input_s0 >= (1.0f-stbir__small_float) ) || ( input_s1 <= stbir__small_float ) ) + return 0; + + output_range = (double)output_full_range; + input_range = (double)input_full_range; + + output_s = ( (double)output_sub_range) / output_range; + + // figure out the scaling to use + ratio = output_s / input_s; + + // save scale before clipping + scale = ( output_range / input_range ) * ratio; + scale_info->scale = (float)scale; + scale_info->inv_scale = (float)( 1.0 / scale ); + + // clip output area to left/right output edges (and adjust input area) + stbir__clip( output_offset, &output_sub_range, output_full_range, &input_s0, &input_s1 ); + + // recalc input area + input_s = input_s1 - input_s0; + + // after clipping do we have zero input area? + if ( input_s <= stbir__small_float ) + return 0; + + // calculate and store the starting source offsets in output pixel space + scale_info->pixel_shift = (float) ( input_s0 * ratio * output_range ); + + scale_info->scale_is_rational = stbir__double_to_rational( scale, ( scale <= 1.0 ) ? output_full_range : input_full_range, &scale_info->scale_numerator, &scale_info->scale_denominator, ( scale >= 1.0 ) ); + + scale_info->input_full_size = input_full_range; + scale_info->output_sub_size = output_sub_range; + + return 1; +} + + +static void stbir__init_and_set_layout( STBIR_RESIZE * resize, stbir_pixel_layout pixel_layout, stbir_datatype data_type ) +{ + resize->input_cb = 0; + resize->output_cb = 0; + resize->user_data = resize; + resize->samplers = 0; + resize->called_alloc = 0; + resize->horizontal_filter = STBIR_FILTER_DEFAULT; + resize->horizontal_filter_kernel = 0; resize->horizontal_filter_support = 0; + resize->vertical_filter = STBIR_FILTER_DEFAULT; + resize->vertical_filter_kernel = 0; resize->vertical_filter_support = 0; + resize->horizontal_edge = STBIR_EDGE_CLAMP; + resize->vertical_edge = STBIR_EDGE_CLAMP; + resize->input_s0 = 0; resize->input_t0 = 0; resize->input_s1 = 1; resize->input_t1 = 1; + resize->output_subx = 0; resize->output_suby = 0; resize->output_subw = resize->output_w; resize->output_subh = resize->output_h; + resize->input_data_type = data_type; + resize->output_data_type = data_type; + resize->input_pixel_layout_public = pixel_layout; + resize->output_pixel_layout_public = pixel_layout; + resize->needs_rebuild = 1; +} + +STBIRDEF void stbir_resize_init( STBIR_RESIZE * resize, + const void *input_pixels, int input_w, int input_h, int input_stride_in_bytes, // stride can be zero + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, // stride can be zero + stbir_pixel_layout pixel_layout, stbir_datatype data_type ) +{ + resize->input_pixels = input_pixels; + resize->input_w = input_w; + resize->input_h = input_h; + resize->input_stride_in_bytes = input_stride_in_bytes; + resize->output_pixels = output_pixels; + resize->output_w = output_w; + resize->output_h = output_h; + resize->output_stride_in_bytes = output_stride_in_bytes; + resize->fast_alpha = 0; + + stbir__init_and_set_layout( resize, pixel_layout, data_type ); +} + +// You can update parameters any time after resize_init +STBIRDEF void stbir_set_datatypes( STBIR_RESIZE * resize, stbir_datatype input_type, stbir_datatype output_type ) // by default, datatype from resize_init +{ + resize->input_data_type = input_type; + resize->output_data_type = output_type; + if ( ( resize->samplers ) && ( !resize->needs_rebuild ) ) + stbir__update_info_from_resize( resize->samplers, resize ); +} + +STBIRDEF void stbir_set_pixel_callbacks( STBIR_RESIZE * resize, stbir_input_callback * input_cb, stbir_output_callback * output_cb ) // no callbacks by default +{ + resize->input_cb = input_cb; + resize->output_cb = output_cb; + + if ( ( resize->samplers ) && ( !resize->needs_rebuild ) ) + { + resize->samplers->in_pixels_cb = input_cb; + resize->samplers->out_pixels_cb = output_cb; + } +} + +STBIRDEF void stbir_set_user_data( STBIR_RESIZE * resize, void * user_data ) // pass back STBIR_RESIZE* by default +{ + resize->user_data = user_data; + if ( ( resize->samplers ) && ( !resize->needs_rebuild ) ) + resize->samplers->user_data = user_data; +} + +STBIRDEF void stbir_set_buffer_ptrs( STBIR_RESIZE * resize, const void * input_pixels, int input_stride_in_bytes, void * output_pixels, int output_stride_in_bytes ) +{ + resize->input_pixels = input_pixels; + resize->input_stride_in_bytes = input_stride_in_bytes; + resize->output_pixels = output_pixels; + resize->output_stride_in_bytes = output_stride_in_bytes; + if ( ( resize->samplers ) && ( !resize->needs_rebuild ) ) + stbir__update_info_from_resize( resize->samplers, resize ); +} + + +STBIRDEF int stbir_set_edgemodes( STBIR_RESIZE * resize, stbir_edge horizontal_edge, stbir_edge vertical_edge ) // CLAMP by default +{ + resize->horizontal_edge = horizontal_edge; + resize->vertical_edge = vertical_edge; + resize->needs_rebuild = 1; + return 1; +} + +STBIRDEF int stbir_set_filters( STBIR_RESIZE * resize, stbir_filter horizontal_filter, stbir_filter vertical_filter ) // STBIR_DEFAULT_FILTER_UPSAMPLE/DOWNSAMPLE by default +{ + resize->horizontal_filter = horizontal_filter; + resize->vertical_filter = vertical_filter; + resize->needs_rebuild = 1; + return 1; +} + +STBIRDEF int stbir_set_filter_callbacks( STBIR_RESIZE * resize, stbir__kernel_callback * horizontal_filter, stbir__support_callback * horizontal_support, stbir__kernel_callback * vertical_filter, stbir__support_callback * vertical_support ) +{ + resize->horizontal_filter_kernel = horizontal_filter; resize->horizontal_filter_support = horizontal_support; + resize->vertical_filter_kernel = vertical_filter; resize->vertical_filter_support = vertical_support; + resize->needs_rebuild = 1; + return 1; +} + +STBIRDEF int stbir_set_pixel_layouts( STBIR_RESIZE * resize, stbir_pixel_layout input_pixel_layout, stbir_pixel_layout output_pixel_layout ) // sets new pixel layouts +{ + resize->input_pixel_layout_public = input_pixel_layout; + resize->output_pixel_layout_public = output_pixel_layout; + resize->needs_rebuild = 1; + return 1; +} + + +STBIRDEF int stbir_set_non_pm_alpha_speed_over_quality( STBIR_RESIZE * resize, int non_pma_alpha_speed_over_quality ) // sets alpha speed +{ + resize->fast_alpha = non_pma_alpha_speed_over_quality; + resize->needs_rebuild = 1; + return 1; +} + +STBIRDEF int stbir_set_input_subrect( STBIR_RESIZE * resize, double s0, double t0, double s1, double t1 ) // sets input region (full region by default) +{ + resize->input_s0 = s0; + resize->input_t0 = t0; + resize->input_s1 = s1; + resize->input_t1 = t1; + resize->needs_rebuild = 1; + + // are we inbounds? + if ( ( s1 < stbir__small_float ) || ( (s1-s0) < stbir__small_float ) || + ( t1 < stbir__small_float ) || ( (t1-t0) < stbir__small_float ) || + ( s0 > (1.0f-stbir__small_float) ) || + ( t0 > (1.0f-stbir__small_float) ) ) + return 0; + + return 1; +} + +STBIRDEF int stbir_set_output_pixel_subrect( STBIR_RESIZE * resize, int subx, int suby, int subw, int subh ) // sets input region (full region by default) +{ + resize->output_subx = subx; + resize->output_suby = suby; + resize->output_subw = subw; + resize->output_subh = subh; + resize->needs_rebuild = 1; + + // are we inbounds? + if ( ( subx >= resize->output_w ) || ( ( subx + subw ) <= 0 ) || ( suby >= resize->output_h ) || ( ( suby + subh ) <= 0 ) || ( subw == 0 ) || ( subh == 0 ) ) + return 0; + + return 1; +} + +STBIRDEF int stbir_set_pixel_subrect( STBIR_RESIZE * resize, int subx, int suby, int subw, int subh ) // sets both regions (full regions by default) +{ + double s0, t0, s1, t1; + + s0 = ( (double)subx ) / ( (double)resize->output_w ); + t0 = ( (double)suby ) / ( (double)resize->output_h ); + s1 = ( (double)(subx+subw) ) / ( (double)resize->output_w ); + t1 = ( (double)(suby+subh) ) / ( (double)resize->output_h ); + + resize->input_s0 = s0; + resize->input_t0 = t0; + resize->input_s1 = s1; + resize->input_t1 = t1; + resize->output_subx = subx; + resize->output_suby = suby; + resize->output_subw = subw; + resize->output_subh = subh; + resize->needs_rebuild = 1; + + // are we inbounds? + if ( ( subx >= resize->output_w ) || ( ( subx + subw ) <= 0 ) || ( suby >= resize->output_h ) || ( ( suby + subh ) <= 0 ) || ( subw == 0 ) || ( subh == 0 ) ) + return 0; + + return 1; +} + +static int stbir__perform_build( STBIR_RESIZE * resize, int splits ) +{ + stbir__contributors conservative = { 0, 0 }; + stbir__sampler horizontal, vertical; + int new_output_subx, new_output_suby; + stbir__info * out_info; + #ifdef STBIR_PROFILE + stbir__info profile_infod; // used to contain building profile info before everything is allocated + stbir__info * profile_info = &profile_infod; + #endif + + // have we already built the samplers? + if ( resize->samplers ) + return 0; + + #define STBIR_RETURN_ERROR_AND_ASSERT( exp ) STBIR_ASSERT( !(exp) ); if (exp) return 0; + STBIR_RETURN_ERROR_AND_ASSERT( (unsigned)resize->horizontal_filter >= STBIR_FILTER_OTHER) + STBIR_RETURN_ERROR_AND_ASSERT( (unsigned)resize->vertical_filter >= STBIR_FILTER_OTHER) + #undef STBIR_RETURN_ERROR_AND_ASSERT + + if ( splits <= 0 ) + return 0; + + STBIR_PROFILE_BUILD_FIRST_START( build ); + + new_output_subx = resize->output_subx; + new_output_suby = resize->output_suby; + + // do horizontal clip and scale calcs + if ( !stbir__calculate_region_transform( &horizontal.scale_info, resize->output_w, &new_output_subx, resize->output_subw, resize->input_w, resize->input_s0, resize->input_s1 ) ) + return 0; + + // do vertical clip and scale calcs + if ( !stbir__calculate_region_transform( &vertical.scale_info, resize->output_h, &new_output_suby, resize->output_subh, resize->input_h, resize->input_t0, resize->input_t1 ) ) + return 0; + + // if nothing to do, just return + if ( ( horizontal.scale_info.output_sub_size == 0 ) || ( vertical.scale_info.output_sub_size == 0 ) ) + return 0; + + stbir__set_sampler(&horizontal, resize->horizontal_filter, resize->horizontal_filter_kernel, resize->horizontal_filter_support, resize->horizontal_edge, &horizontal.scale_info, 1, resize->user_data ); + stbir__get_conservative_extents( &horizontal, &conservative, resize->user_data ); + stbir__set_sampler(&vertical, resize->vertical_filter, resize->vertical_filter_kernel, resize->vertical_filter_support, resize->vertical_edge, &vertical.scale_info, 0, resize->user_data ); + + if ( ( vertical.scale_info.output_sub_size / splits ) < STBIR_FORCE_MINIMUM_SCANLINES_FOR_SPLITS ) // each split should be a minimum of 4 scanlines (handwavey choice) + { + splits = vertical.scale_info.output_sub_size / STBIR_FORCE_MINIMUM_SCANLINES_FOR_SPLITS; + if ( splits == 0 ) splits = 1; + } + + STBIR_PROFILE_BUILD_START( alloc ); + out_info = stbir__alloc_internal_mem_and_build_samplers( &horizontal, &vertical, &conservative, resize->input_pixel_layout_public, resize->output_pixel_layout_public, splits, new_output_subx, new_output_suby, resize->fast_alpha, resize->user_data STBIR_ONLY_PROFILE_BUILD_SET_INFO ); + STBIR_PROFILE_BUILD_END( alloc ); + STBIR_PROFILE_BUILD_END( build ); + + if ( out_info ) + { + resize->splits = splits; + resize->samplers = out_info; + resize->needs_rebuild = 0; + #ifdef STBIR_PROFILE + STBIR_MEMCPY( &out_info->profile, &profile_infod.profile, sizeof( out_info->profile ) ); + #endif + + // update anything that can be changed without recalcing samplers + stbir__update_info_from_resize( out_info, resize ); + + return splits; + } + + return 0; +} + +STBIRDEF void stbir_free_samplers( STBIR_RESIZE * resize ) +{ + if ( resize->samplers ) + { + stbir__free_internal_mem( resize->samplers ); + resize->samplers = 0; + resize->called_alloc = 0; + } +} + +STBIRDEF int stbir_build_samplers_with_splits( STBIR_RESIZE * resize, int splits ) +{ + if ( ( resize->samplers == 0 ) || ( resize->needs_rebuild ) ) + { + if ( resize->samplers ) + stbir_free_samplers( resize ); + + resize->called_alloc = 1; + return stbir__perform_build( resize, splits ); + } + + STBIR_PROFILE_BUILD_CLEAR( resize->samplers ); + + return 1; +} + +STBIRDEF int stbir_build_samplers( STBIR_RESIZE * resize ) +{ + return stbir_build_samplers_with_splits( resize, 1 ); +} + +STBIRDEF int stbir_resize_extended( STBIR_RESIZE * resize ) +{ + int result; + + if ( ( resize->samplers == 0 ) || ( resize->needs_rebuild ) ) + { + int alloc_state = resize->called_alloc; // remember allocated state + + if ( resize->samplers ) + { + stbir__free_internal_mem( resize->samplers ); + resize->samplers = 0; + } + + if ( !stbir_build_samplers( resize ) ) + return 0; + + resize->called_alloc = alloc_state; + + // if build_samplers succeeded (above), but there are no samplers set, then + // the area to stretch into was zero pixels, so don't do anything and return + // success + if ( resize->samplers == 0 ) + return 1; + } + else + { + // didn't build anything - clear it + STBIR_PROFILE_BUILD_CLEAR( resize->samplers ); + } + + // do resize + result = stbir__perform_resize( resize->samplers, 0, resize->splits ); + + // if we alloced, then free + if ( !resize->called_alloc ) + { + stbir_free_samplers( resize ); + resize->samplers = 0; + } + + return result; +} + +STBIRDEF int stbir_resize_extended_split( STBIR_RESIZE * resize, int split_start, int split_count ) +{ + STBIR_ASSERT( resize->samplers ); + + // if we're just doing the whole thing, call full + if ( ( split_start == -1 ) || ( ( split_start == 0 ) && ( split_count == resize->splits ) ) ) + return stbir_resize_extended( resize ); + + // you **must** build samplers first when using split resize + if ( ( resize->samplers == 0 ) || ( resize->needs_rebuild ) ) + return 0; + + if ( ( split_start >= resize->splits ) || ( split_start < 0 ) || ( ( split_start + split_count ) > resize->splits ) || ( split_count <= 0 ) ) + return 0; + + // do resize + return stbir__perform_resize( resize->samplers, split_start, split_count ); +} + + +static void * stbir_quick_resize_helper( const void *input_pixels , int input_w , int input_h, int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_pixel_layout pixel_layout, stbir_datatype data_type, stbir_edge edge, stbir_filter filter ) +{ + STBIR_RESIZE resize; + int scanline_output_in_bytes; + int positive_output_stride_in_bytes; + void * start_ptr; + void * free_ptr; + + scanline_output_in_bytes = output_w * stbir__type_size[ data_type ] * stbir__pixel_channels[ stbir__pixel_layout_convert_public_to_internal[ pixel_layout ] ]; + if ( scanline_output_in_bytes == 0 ) + return 0; + + // if zero stride, use scanline output + if ( output_stride_in_bytes == 0 ) + output_stride_in_bytes = scanline_output_in_bytes; + + // abs value for inverted images (negative pitches) + positive_output_stride_in_bytes = output_stride_in_bytes; + if ( positive_output_stride_in_bytes < 0 ) + positive_output_stride_in_bytes = -positive_output_stride_in_bytes; + + // is the requested stride smaller than the scanline output? if so, just fail + if ( positive_output_stride_in_bytes < scanline_output_in_bytes ) + return 0; + + start_ptr = output_pixels; + free_ptr = 0; // no free pointer, since they passed buffer to use + + // did they pass a zero for the dest? if so, allocate the buffer + if ( output_pixels == 0 ) + { + size_t size; + char * ptr; + + size = (size_t)positive_output_stride_in_bytes * (size_t)output_h; + if ( size == 0 ) + return 0; + + ptr = (char*) STBIR_MALLOC( size, 0 ); + if ( ptr == 0 ) + return 0; + + free_ptr = ptr; + + // point at the last scanline, if they requested a flipped image + if ( output_stride_in_bytes < 0 ) + start_ptr = ptr + ( (size_t)positive_output_stride_in_bytes * (size_t)( output_h - 1 ) ); + else + start_ptr = ptr; + } + + // ok, now do the resize + stbir_resize_init( &resize, + input_pixels, input_w, input_h, input_stride_in_bytes, + start_ptr, output_w, output_h, output_stride_in_bytes, + pixel_layout, data_type ); + + resize.horizontal_edge = edge; + resize.vertical_edge = edge; + resize.horizontal_filter = filter; + resize.vertical_filter = filter; + + if ( !stbir_resize_extended( &resize ) ) + { + if ( free_ptr ) + STBIR_FREE( free_ptr, 0 ); + return 0; + } + + return (free_ptr) ? free_ptr : start_ptr; +} + + + +STBIRDEF unsigned char * stbir_resize_uint8_linear( const unsigned char *input_pixels , int input_w , int input_h, int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_pixel_layout pixel_layout ) +{ + return (unsigned char *) stbir_quick_resize_helper( input_pixels , input_w , input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + pixel_layout, STBIR_TYPE_UINT8, STBIR_EDGE_CLAMP, STBIR_FILTER_DEFAULT ); +} + +STBIRDEF unsigned char * stbir_resize_uint8_srgb( const unsigned char *input_pixels , int input_w , int input_h, int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_pixel_layout pixel_layout ) +{ + return (unsigned char *) stbir_quick_resize_helper( input_pixels , input_w , input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + pixel_layout, STBIR_TYPE_UINT8_SRGB, STBIR_EDGE_CLAMP, STBIR_FILTER_DEFAULT ); +} + + +STBIRDEF float * stbir_resize_float_linear( const float *input_pixels , int input_w , int input_h, int input_stride_in_bytes, + float *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_pixel_layout pixel_layout ) +{ + return (float *) stbir_quick_resize_helper( input_pixels , input_w , input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + pixel_layout, STBIR_TYPE_FLOAT, STBIR_EDGE_CLAMP, STBIR_FILTER_DEFAULT ); +} + + +STBIRDEF void * stbir_resize( const void *input_pixels , int input_w , int input_h, int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_pixel_layout pixel_layout, stbir_datatype data_type, + stbir_edge edge, stbir_filter filter ) +{ + return (void *) stbir_quick_resize_helper( input_pixels , input_w , input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + pixel_layout, data_type, edge, filter ); +} + +#ifdef STBIR_PROFILE + +STBIRDEF void stbir_resize_build_profile_info( STBIR_PROFILE_INFO * info, STBIR_RESIZE const * resize ) +{ + static char const * bdescriptions[6] = { "Building", "Allocating", "Horizontal sampler", "Vertical sampler", "Coefficient cleanup", "Coefficient pivot" } ; + stbir__info* samp = resize->samplers; + int i; + + typedef int testa[ (STBIR__ARRAY_SIZE( bdescriptions ) == (STBIR__ARRAY_SIZE( samp->profile.array )-1) )?1:-1]; + typedef int testb[ (sizeof( samp->profile.array ) == (sizeof(samp->profile.named)) )?1:-1]; + typedef int testc[ (sizeof( info->clocks ) >= (sizeof(samp->profile.named)) )?1:-1]; + + for( i = 0 ; i < STBIR__ARRAY_SIZE( bdescriptions ) ; i++) + info->clocks[i] = samp->profile.array[i+1]; + + info->total_clocks = samp->profile.named.total; + info->descriptions = bdescriptions; + info->count = STBIR__ARRAY_SIZE( bdescriptions ); +} + +STBIRDEF void stbir_resize_split_profile_info( STBIR_PROFILE_INFO * info, STBIR_RESIZE const * resize, int split_start, int split_count ) +{ + static char const * descriptions[7] = { "Looping", "Vertical sampling", "Horizontal sampling", "Scanline input", "Scanline output", "Alpha weighting", "Alpha unweighting" }; + stbir__per_split_info * split_info; + int s, i; + + typedef int testa[ (STBIR__ARRAY_SIZE( descriptions ) == (STBIR__ARRAY_SIZE( split_info->profile.array )-1) )?1:-1]; + typedef int testb[ (sizeof( split_info->profile.array ) == (sizeof(split_info->profile.named)) )?1:-1]; + typedef int testc[ (sizeof( info->clocks ) >= (sizeof(split_info->profile.named)) )?1:-1]; + + if ( split_start == -1 ) + { + split_start = 0; + split_count = resize->samplers->splits; + } + + if ( ( split_start >= resize->splits ) || ( split_start < 0 ) || ( ( split_start + split_count ) > resize->splits ) || ( split_count <= 0 ) ) + { + info->total_clocks = 0; + info->descriptions = 0; + info->count = 0; + return; + } + + split_info = resize->samplers->split_info + split_start; + + // sum up the profile from all the splits + for( i = 0 ; i < STBIR__ARRAY_SIZE( descriptions ) ; i++ ) + { + stbir_uint64 sum = 0; + for( s = 0 ; s < split_count ; s++ ) + sum += split_info[s].profile.array[i+1]; + info->clocks[i] = sum; + } + + info->total_clocks = split_info->profile.named.total; + info->descriptions = descriptions; + info->count = STBIR__ARRAY_SIZE( descriptions ); +} + +STBIRDEF void stbir_resize_extended_profile_info( STBIR_PROFILE_INFO * info, STBIR_RESIZE const * resize ) +{ + stbir_resize_split_profile_info( info, resize, -1, 0 ); +} + +#endif // STBIR_PROFILE + +#undef STBIR_BGR +#undef STBIR_1CHANNEL +#undef STBIR_2CHANNEL +#undef STBIR_RGB +#undef STBIR_RGBA +#undef STBIR_4CHANNEL +#undef STBIR_BGRA +#undef STBIR_ARGB +#undef STBIR_ABGR +#undef STBIR_RA +#undef STBIR_AR +#undef STBIR_RGBA_PM +#undef STBIR_BGRA_PM +#undef STBIR_ARGB_PM +#undef STBIR_ABGR_PM +#undef STBIR_RA_PM +#undef STBIR_AR_PM + +#endif // STB_IMAGE_RESIZE_IMPLEMENTATION + +#else // STB_IMAGE_RESIZE_HORIZONTALS&STB_IMAGE_RESIZE_DO_VERTICALS + +// we reinclude the header file to define all the horizontal functions +// specializing each function for the number of coeffs is 20-40% faster *OVERALL* + +// by including the header file again this way, we can still debug the functions + +#define STBIR_strs_join2( start, mid, end ) start##mid##end +#define STBIR_strs_join1( start, mid, end ) STBIR_strs_join2( start, mid, end ) + +#define STBIR_strs_join24( start, mid1, mid2, end ) start##mid1##mid2##end +#define STBIR_strs_join14( start, mid1, mid2, end ) STBIR_strs_join24( start, mid1, mid2, end ) + +#ifdef STB_IMAGE_RESIZE_DO_CODERS + +#ifdef stbir__decode_suffix +#define STBIR__CODER_NAME( name ) STBIR_strs_join1( name, _, stbir__decode_suffix ) +#else +#define STBIR__CODER_NAME( name ) name +#endif + +#ifdef stbir__decode_swizzle +#define stbir__decode_simdf8_flip(reg) STBIR_strs_join1( STBIR_strs_join1( STBIR_strs_join1( STBIR_strs_join1( stbir__simdf8_0123to,stbir__decode_order0,stbir__decode_order1),stbir__decode_order2,stbir__decode_order3),stbir__decode_order0,stbir__decode_order1),stbir__decode_order2,stbir__decode_order3)(reg, reg) +#define stbir__decode_simdf4_flip(reg) STBIR_strs_join1( STBIR_strs_join1( stbir__simdf_0123to,stbir__decode_order0,stbir__decode_order1),stbir__decode_order2,stbir__decode_order3)(reg, reg) +#define stbir__encode_simdf8_unflip(reg) STBIR_strs_join1( STBIR_strs_join1( STBIR_strs_join1( STBIR_strs_join1( stbir__simdf8_0123to,stbir__encode_order0,stbir__encode_order1),stbir__encode_order2,stbir__encode_order3),stbir__encode_order0,stbir__encode_order1),stbir__encode_order2,stbir__encode_order3)(reg, reg) +#define stbir__encode_simdf4_unflip(reg) STBIR_strs_join1( STBIR_strs_join1( stbir__simdf_0123to,stbir__encode_order0,stbir__encode_order1),stbir__encode_order2,stbir__encode_order3)(reg, reg) +#else +#define stbir__decode_order0 0 +#define stbir__decode_order1 1 +#define stbir__decode_order2 2 +#define stbir__decode_order3 3 +#define stbir__encode_order0 0 +#define stbir__encode_order1 1 +#define stbir__encode_order2 2 +#define stbir__encode_order3 3 +#define stbir__decode_simdf8_flip(reg) +#define stbir__decode_simdf4_flip(reg) +#define stbir__encode_simdf8_unflip(reg) +#define stbir__encode_simdf4_unflip(reg) +#endif + +#ifdef STBIR_SIMD8 +#define stbir__encode_simdfX_unflip stbir__encode_simdf8_unflip +#else +#define stbir__encode_simdfX_unflip stbir__encode_simdf4_unflip +#endif + +static float * STBIR__CODER_NAME( stbir__decode_uint8_linear_scaled )( float * decodep, int width_times_channels, void const * inputp ) +{ + float STBIR_STREAMOUT_PTR( * ) decode = decodep; + float * decode_end = (float*) decode + width_times_channels; + unsigned char const * input = (unsigned char const*)inputp; + + #ifdef STBIR_SIMD + unsigned char const * end_input_m16 = input + width_times_channels - 16; + if ( width_times_channels >= 16 ) + { + decode_end -= 16; + STBIR_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + #ifdef STBIR_SIMD8 + stbir__simdi i; stbir__simdi8 o0,o1; + stbir__simdf8 of0, of1; + STBIR_NO_UNROLL(decode); + stbir__simdi_load( i, input ); + stbir__simdi8_expand_u8_to_u32( o0, o1, i ); + stbir__simdi8_convert_i32_to_float( of0, o0 ); + stbir__simdi8_convert_i32_to_float( of1, o1 ); + stbir__simdf8_mult( of0, of0, STBIR_max_uint8_as_float_inverted8); + stbir__simdf8_mult( of1, of1, STBIR_max_uint8_as_float_inverted8); + stbir__decode_simdf8_flip( of0 ); + stbir__decode_simdf8_flip( of1 ); + stbir__simdf8_store( decode + 0, of0 ); + stbir__simdf8_store( decode + 8, of1 ); + #else + stbir__simdi i, o0, o1, o2, o3; + stbir__simdf of0, of1, of2, of3; + STBIR_NO_UNROLL(decode); + stbir__simdi_load( i, input ); + stbir__simdi_expand_u8_to_u32( o0,o1,o2,o3,i); + stbir__simdi_convert_i32_to_float( of0, o0 ); + stbir__simdi_convert_i32_to_float( of1, o1 ); + stbir__simdi_convert_i32_to_float( of2, o2 ); + stbir__simdi_convert_i32_to_float( of3, o3 ); + stbir__simdf_mult( of0, of0, STBIR__CONSTF(STBIR_max_uint8_as_float_inverted) ); + stbir__simdf_mult( of1, of1, STBIR__CONSTF(STBIR_max_uint8_as_float_inverted) ); + stbir__simdf_mult( of2, of2, STBIR__CONSTF(STBIR_max_uint8_as_float_inverted) ); + stbir__simdf_mult( of3, of3, STBIR__CONSTF(STBIR_max_uint8_as_float_inverted) ); + stbir__decode_simdf4_flip( of0 ); + stbir__decode_simdf4_flip( of1 ); + stbir__decode_simdf4_flip( of2 ); + stbir__decode_simdf4_flip( of3 ); + stbir__simdf_store( decode + 0, of0 ); + stbir__simdf_store( decode + 4, of1 ); + stbir__simdf_store( decode + 8, of2 ); + stbir__simdf_store( decode + 12, of3 ); + #endif + decode += 16; + input += 16; + if ( decode <= decode_end ) + continue; + if ( decode == ( decode_end + 16 ) ) + break; + decode = decode_end; // backup and do last couple + input = end_input_m16; + } + return decode_end + 16; + } + #endif + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + decode += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( decode <= decode_end ) + { + STBIR_SIMD_NO_UNROLL(decode); + decode[0-4] = ((float)(input[stbir__decode_order0])) * stbir__max_uint8_as_float_inverted; + decode[1-4] = ((float)(input[stbir__decode_order1])) * stbir__max_uint8_as_float_inverted; + decode[2-4] = ((float)(input[stbir__decode_order2])) * stbir__max_uint8_as_float_inverted; + decode[3-4] = ((float)(input[stbir__decode_order3])) * stbir__max_uint8_as_float_inverted; + decode += 4; + input += 4; + } + decode -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( decode < decode_end ) + { + STBIR_NO_UNROLL(decode); + decode[0] = ((float)(input[stbir__decode_order0])) * stbir__max_uint8_as_float_inverted; + #if stbir__coder_min_num >= 2 + decode[1] = ((float)(input[stbir__decode_order1])) * stbir__max_uint8_as_float_inverted; + #endif + #if stbir__coder_min_num >= 3 + decode[2] = ((float)(input[stbir__decode_order2])) * stbir__max_uint8_as_float_inverted; + #endif + decode += stbir__coder_min_num; + input += stbir__coder_min_num; + } + #endif + + return decode_end; +} + +static void STBIR__CODER_NAME( stbir__encode_uint8_linear_scaled )( void * outputp, int width_times_channels, float const * encode ) +{ + unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char *) outputp; + unsigned char * end_output = ( (unsigned char *) output ) + width_times_channels; + + #ifdef STBIR_SIMD + if ( width_times_channels >= stbir__simdfX_float_count*2 ) + { + float const * end_encode_m8 = encode + width_times_channels - stbir__simdfX_float_count*2; + end_output -= stbir__simdfX_float_count*2; + STBIR_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + stbir__simdfX e0, e1; + stbir__simdi i; + STBIR_SIMD_NO_UNROLL(encode); + stbir__simdfX_madd_mem( e0, STBIR_simd_point5X, STBIR_max_uint8_as_floatX, encode ); + stbir__simdfX_madd_mem( e1, STBIR_simd_point5X, STBIR_max_uint8_as_floatX, encode+stbir__simdfX_float_count ); + stbir__encode_simdfX_unflip( e0 ); + stbir__encode_simdfX_unflip( e1 ); + #ifdef STBIR_SIMD8 + stbir__simdf8_pack_to_16bytes( i, e0, e1 ); + stbir__simdi_store( output, i ); + #else + stbir__simdf_pack_to_8bytes( i, e0, e1 ); + stbir__simdi_store2( output, i ); + #endif + encode += stbir__simdfX_float_count*2; + output += stbir__simdfX_float_count*2; + if ( output <= end_output ) + continue; + if ( output == ( end_output + stbir__simdfX_float_count*2 ) ) + break; + output = end_output; // backup and do last couple + encode = end_encode_m8; + } + return; + } + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_NO_UNROLL_LOOP_START + while( output <= end_output ) + { + stbir__simdf e0; + stbir__simdi i0; + STBIR_NO_UNROLL(encode); + stbir__simdf_load( e0, encode ); + stbir__simdf_madd( e0, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint8_as_float), e0 ); + stbir__encode_simdf4_unflip( e0 ); + stbir__simdf_pack_to_8bytes( i0, e0, e0 ); // only use first 4 + *(int*)(output-4) = stbir__simdi_to_int( i0 ); + output += 4; + encode += 4; + } + output -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( output < end_output ) + { + stbir__simdf e0; + STBIR_NO_UNROLL(encode); + stbir__simdf_madd1_mem( e0, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint8_as_float), encode+stbir__encode_order0 ); output[0] = stbir__simdf_convert_float_to_uint8( e0 ); + #if stbir__coder_min_num >= 2 + stbir__simdf_madd1_mem( e0, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint8_as_float), encode+stbir__encode_order1 ); output[1] = stbir__simdf_convert_float_to_uint8( e0 ); + #endif + #if stbir__coder_min_num >= 3 + stbir__simdf_madd1_mem( e0, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint8_as_float), encode+stbir__encode_order2 ); output[2] = stbir__simdf_convert_float_to_uint8( e0 ); + #endif + output += stbir__coder_min_num; + encode += stbir__coder_min_num; + } + #endif + + #else + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + while( output <= end_output ) + { + float f; + f = encode[stbir__encode_order0] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[0-4] = (unsigned char)f; + f = encode[stbir__encode_order1] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[1-4] = (unsigned char)f; + f = encode[stbir__encode_order2] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[2-4] = (unsigned char)f; + f = encode[stbir__encode_order3] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[3-4] = (unsigned char)f; + output += 4; + encode += 4; + } + output -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( output < end_output ) + { + float f; + STBIR_NO_UNROLL(encode); + f = encode[stbir__encode_order0] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[0] = (unsigned char)f; + #if stbir__coder_min_num >= 2 + f = encode[stbir__encode_order1] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[1] = (unsigned char)f; + #endif + #if stbir__coder_min_num >= 3 + f = encode[stbir__encode_order2] * stbir__max_uint8_as_float + 0.5f; STBIR_CLAMP(f, 0, 255); output[2] = (unsigned char)f; + #endif + output += stbir__coder_min_num; + encode += stbir__coder_min_num; + } + #endif + #endif +} + +static float * STBIR__CODER_NAME(stbir__decode_uint8_linear)( float * decodep, int width_times_channels, void const * inputp ) +{ + float STBIR_STREAMOUT_PTR( * ) decode = decodep; + float * decode_end = (float*) decode + width_times_channels; + unsigned char const * input = (unsigned char const*)inputp; + + #ifdef STBIR_SIMD + unsigned char const * end_input_m16 = input + width_times_channels - 16; + if ( width_times_channels >= 16 ) + { + decode_end -= 16; + STBIR_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + #ifdef STBIR_SIMD8 + stbir__simdi i; stbir__simdi8 o0,o1; + stbir__simdf8 of0, of1; + STBIR_NO_UNROLL(decode); + stbir__simdi_load( i, input ); + stbir__simdi8_expand_u8_to_u32( o0, o1, i ); + stbir__simdi8_convert_i32_to_float( of0, o0 ); + stbir__simdi8_convert_i32_to_float( of1, o1 ); + stbir__decode_simdf8_flip( of0 ); + stbir__decode_simdf8_flip( of1 ); + stbir__simdf8_store( decode + 0, of0 ); + stbir__simdf8_store( decode + 8, of1 ); + #else + stbir__simdi i, o0, o1, o2, o3; + stbir__simdf of0, of1, of2, of3; + STBIR_NO_UNROLL(decode); + stbir__simdi_load( i, input ); + stbir__simdi_expand_u8_to_u32( o0,o1,o2,o3,i); + stbir__simdi_convert_i32_to_float( of0, o0 ); + stbir__simdi_convert_i32_to_float( of1, o1 ); + stbir__simdi_convert_i32_to_float( of2, o2 ); + stbir__simdi_convert_i32_to_float( of3, o3 ); + stbir__decode_simdf4_flip( of0 ); + stbir__decode_simdf4_flip( of1 ); + stbir__decode_simdf4_flip( of2 ); + stbir__decode_simdf4_flip( of3 ); + stbir__simdf_store( decode + 0, of0 ); + stbir__simdf_store( decode + 4, of1 ); + stbir__simdf_store( decode + 8, of2 ); + stbir__simdf_store( decode + 12, of3 ); +#endif + decode += 16; + input += 16; + if ( decode <= decode_end ) + continue; + if ( decode == ( decode_end + 16 ) ) + break; + decode = decode_end; // backup and do last couple + input = end_input_m16; + } + return decode_end + 16; + } + #endif + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + decode += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( decode <= decode_end ) + { + STBIR_SIMD_NO_UNROLL(decode); + decode[0-4] = ((float)(input[stbir__decode_order0])); + decode[1-4] = ((float)(input[stbir__decode_order1])); + decode[2-4] = ((float)(input[stbir__decode_order2])); + decode[3-4] = ((float)(input[stbir__decode_order3])); + decode += 4; + input += 4; + } + decode -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( decode < decode_end ) + { + STBIR_NO_UNROLL(decode); + decode[0] = ((float)(input[stbir__decode_order0])); + #if stbir__coder_min_num >= 2 + decode[1] = ((float)(input[stbir__decode_order1])); + #endif + #if stbir__coder_min_num >= 3 + decode[2] = ((float)(input[stbir__decode_order2])); + #endif + decode += stbir__coder_min_num; + input += stbir__coder_min_num; + } + #endif + return decode_end; +} + +static void STBIR__CODER_NAME( stbir__encode_uint8_linear )( void * outputp, int width_times_channels, float const * encode ) +{ + unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char *) outputp; + unsigned char * end_output = ( (unsigned char *) output ) + width_times_channels; + + #ifdef STBIR_SIMD + if ( width_times_channels >= stbir__simdfX_float_count*2 ) + { + float const * end_encode_m8 = encode + width_times_channels - stbir__simdfX_float_count*2; + end_output -= stbir__simdfX_float_count*2; + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + stbir__simdfX e0, e1; + stbir__simdi i; + STBIR_SIMD_NO_UNROLL(encode); + stbir__simdfX_add_mem( e0, STBIR_simd_point5X, encode ); + stbir__simdfX_add_mem( e1, STBIR_simd_point5X, encode+stbir__simdfX_float_count ); + stbir__encode_simdfX_unflip( e0 ); + stbir__encode_simdfX_unflip( e1 ); + #ifdef STBIR_SIMD8 + stbir__simdf8_pack_to_16bytes( i, e0, e1 ); + stbir__simdi_store( output, i ); + #else + stbir__simdf_pack_to_8bytes( i, e0, e1 ); + stbir__simdi_store2( output, i ); + #endif + encode += stbir__simdfX_float_count*2; + output += stbir__simdfX_float_count*2; + if ( output <= end_output ) + continue; + if ( output == ( end_output + stbir__simdfX_float_count*2 ) ) + break; + output = end_output; // backup and do last couple + encode = end_encode_m8; + } + return; + } + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_NO_UNROLL_LOOP_START + while( output <= end_output ) + { + stbir__simdf e0; + stbir__simdi i0; + STBIR_NO_UNROLL(encode); + stbir__simdf_load( e0, encode ); + stbir__simdf_add( e0, STBIR__CONSTF(STBIR_simd_point5), e0 ); + stbir__encode_simdf4_unflip( e0 ); + stbir__simdf_pack_to_8bytes( i0, e0, e0 ); // only use first 4 + *(int*)(output-4) = stbir__simdi_to_int( i0 ); + output += 4; + encode += 4; + } + output -= 4; + #endif + + #else + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + while( output <= end_output ) + { + float f; + f = encode[stbir__encode_order0] + 0.5f; STBIR_CLAMP(f, 0, 255); output[0-4] = (unsigned char)f; + f = encode[stbir__encode_order1] + 0.5f; STBIR_CLAMP(f, 0, 255); output[1-4] = (unsigned char)f; + f = encode[stbir__encode_order2] + 0.5f; STBIR_CLAMP(f, 0, 255); output[2-4] = (unsigned char)f; + f = encode[stbir__encode_order3] + 0.5f; STBIR_CLAMP(f, 0, 255); output[3-4] = (unsigned char)f; + output += 4; + encode += 4; + } + output -= 4; + #endif + + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( output < end_output ) + { + float f; + STBIR_NO_UNROLL(encode); + f = encode[stbir__encode_order0] + 0.5f; STBIR_CLAMP(f, 0, 255); output[0] = (unsigned char)f; + #if stbir__coder_min_num >= 2 + f = encode[stbir__encode_order1] + 0.5f; STBIR_CLAMP(f, 0, 255); output[1] = (unsigned char)f; + #endif + #if stbir__coder_min_num >= 3 + f = encode[stbir__encode_order2] + 0.5f; STBIR_CLAMP(f, 0, 255); output[2] = (unsigned char)f; + #endif + output += stbir__coder_min_num; + encode += stbir__coder_min_num; + } + #endif +} + +static float * STBIR__CODER_NAME(stbir__decode_uint8_srgb)( float * decodep, int width_times_channels, void const * inputp ) +{ + float STBIR_STREAMOUT_PTR( * ) decode = decodep; + float * decode_end = (float*) decode + width_times_channels; + unsigned char const * input = (unsigned char const *)inputp; + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + decode += 4; + while( decode <= decode_end ) + { + decode[0-4] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order0 ] ]; + decode[1-4] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order1 ] ]; + decode[2-4] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order2 ] ]; + decode[3-4] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order3 ] ]; + decode += 4; + input += 4; + } + decode -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( decode < decode_end ) + { + STBIR_NO_UNROLL(decode); + decode[0] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order0 ] ]; + #if stbir__coder_min_num >= 2 + decode[1] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order1 ] ]; + #endif + #if stbir__coder_min_num >= 3 + decode[2] = stbir__srgb_uchar_to_linear_float[ input[ stbir__decode_order2 ] ]; + #endif + decode += stbir__coder_min_num; + input += stbir__coder_min_num; + } + #endif + return decode_end; +} + +#define stbir__min_max_shift20( i, f ) \ + stbir__simdf_max( f, f, stbir_simdf_casti(STBIR__CONSTI( STBIR_almost_zero )) ); \ + stbir__simdf_min( f, f, stbir_simdf_casti(STBIR__CONSTI( STBIR_almost_one )) ); \ + stbir__simdi_32shr( i, stbir_simdi_castf( f ), 20 ); + +#define stbir__scale_and_convert( i, f ) \ + stbir__simdf_madd( f, STBIR__CONSTF( STBIR_simd_point5 ), STBIR__CONSTF( STBIR_max_uint8_as_float ), f ); \ + stbir__simdf_max( f, f, stbir__simdf_zeroP() ); \ + stbir__simdf_min( f, f, STBIR__CONSTF( STBIR_max_uint8_as_float ) ); \ + stbir__simdf_convert_float_to_i32( i, f ); + +#define stbir__linear_to_srgb_finish( i, f ) \ +{ \ + stbir__simdi temp; \ + stbir__simdi_32shr( temp, stbir_simdi_castf( f ), 12 ) ; \ + stbir__simdi_and( temp, temp, STBIR__CONSTI(STBIR_mantissa_mask) ); \ + stbir__simdi_or( temp, temp, STBIR__CONSTI(STBIR_topscale) ); \ + stbir__simdi_16madd( i, i, temp ); \ + stbir__simdi_32shr( i, i, 16 ); \ +} + +#define stbir__simdi_table_lookup2( v0,v1, table ) \ +{ \ + stbir__simdi_u32 temp0,temp1; \ + temp0.m128i_i128 = v0; \ + temp1.m128i_i128 = v1; \ + temp0.m128i_u32[0] = table[temp0.m128i_i32[0]]; temp0.m128i_u32[1] = table[temp0.m128i_i32[1]]; temp0.m128i_u32[2] = table[temp0.m128i_i32[2]]; temp0.m128i_u32[3] = table[temp0.m128i_i32[3]]; \ + temp1.m128i_u32[0] = table[temp1.m128i_i32[0]]; temp1.m128i_u32[1] = table[temp1.m128i_i32[1]]; temp1.m128i_u32[2] = table[temp1.m128i_i32[2]]; temp1.m128i_u32[3] = table[temp1.m128i_i32[3]]; \ + v0 = temp0.m128i_i128; \ + v1 = temp1.m128i_i128; \ +} + +#define stbir__simdi_table_lookup3( v0,v1,v2, table ) \ +{ \ + stbir__simdi_u32 temp0,temp1,temp2; \ + temp0.m128i_i128 = v0; \ + temp1.m128i_i128 = v1; \ + temp2.m128i_i128 = v2; \ + temp0.m128i_u32[0] = table[temp0.m128i_i32[0]]; temp0.m128i_u32[1] = table[temp0.m128i_i32[1]]; temp0.m128i_u32[2] = table[temp0.m128i_i32[2]]; temp0.m128i_u32[3] = table[temp0.m128i_i32[3]]; \ + temp1.m128i_u32[0] = table[temp1.m128i_i32[0]]; temp1.m128i_u32[1] = table[temp1.m128i_i32[1]]; temp1.m128i_u32[2] = table[temp1.m128i_i32[2]]; temp1.m128i_u32[3] = table[temp1.m128i_i32[3]]; \ + temp2.m128i_u32[0] = table[temp2.m128i_i32[0]]; temp2.m128i_u32[1] = table[temp2.m128i_i32[1]]; temp2.m128i_u32[2] = table[temp2.m128i_i32[2]]; temp2.m128i_u32[3] = table[temp2.m128i_i32[3]]; \ + v0 = temp0.m128i_i128; \ + v1 = temp1.m128i_i128; \ + v2 = temp2.m128i_i128; \ +} + +#define stbir__simdi_table_lookup4( v0,v1,v2,v3, table ) \ +{ \ + stbir__simdi_u32 temp0,temp1,temp2,temp3; \ + temp0.m128i_i128 = v0; \ + temp1.m128i_i128 = v1; \ + temp2.m128i_i128 = v2; \ + temp3.m128i_i128 = v3; \ + temp0.m128i_u32[0] = table[temp0.m128i_i32[0]]; temp0.m128i_u32[1] = table[temp0.m128i_i32[1]]; temp0.m128i_u32[2] = table[temp0.m128i_i32[2]]; temp0.m128i_u32[3] = table[temp0.m128i_i32[3]]; \ + temp1.m128i_u32[0] = table[temp1.m128i_i32[0]]; temp1.m128i_u32[1] = table[temp1.m128i_i32[1]]; temp1.m128i_u32[2] = table[temp1.m128i_i32[2]]; temp1.m128i_u32[3] = table[temp1.m128i_i32[3]]; \ + temp2.m128i_u32[0] = table[temp2.m128i_i32[0]]; temp2.m128i_u32[1] = table[temp2.m128i_i32[1]]; temp2.m128i_u32[2] = table[temp2.m128i_i32[2]]; temp2.m128i_u32[3] = table[temp2.m128i_i32[3]]; \ + temp3.m128i_u32[0] = table[temp3.m128i_i32[0]]; temp3.m128i_u32[1] = table[temp3.m128i_i32[1]]; temp3.m128i_u32[2] = table[temp3.m128i_i32[2]]; temp3.m128i_u32[3] = table[temp3.m128i_i32[3]]; \ + v0 = temp0.m128i_i128; \ + v1 = temp1.m128i_i128; \ + v2 = temp2.m128i_i128; \ + v3 = temp3.m128i_i128; \ +} + +static void STBIR__CODER_NAME( stbir__encode_uint8_srgb )( void * outputp, int width_times_channels, float const * encode ) +{ + unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char*) outputp; + unsigned char * end_output = ( (unsigned char*) output ) + width_times_channels; + + #ifdef STBIR_SIMD + + if ( width_times_channels >= 16 ) + { + float const * end_encode_m16 = encode + width_times_channels - 16; + end_output -= 16; + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + stbir__simdf f0, f1, f2, f3; + stbir__simdi i0, i1, i2, i3; + STBIR_SIMD_NO_UNROLL(encode); + + stbir__simdf_load4_transposed( f0, f1, f2, f3, encode ); + + stbir__min_max_shift20( i0, f0 ); + stbir__min_max_shift20( i1, f1 ); + stbir__min_max_shift20( i2, f2 ); + stbir__min_max_shift20( i3, f3 ); + + stbir__simdi_table_lookup4( i0, i1, i2, i3, ( fp32_to_srgb8_tab4 - (127-13)*8 ) ); + + stbir__linear_to_srgb_finish( i0, f0 ); + stbir__linear_to_srgb_finish( i1, f1 ); + stbir__linear_to_srgb_finish( i2, f2 ); + stbir__linear_to_srgb_finish( i3, f3 ); + + stbir__interleave_pack_and_store_16_u8( output, STBIR_strs_join1(i, ,stbir__encode_order0), STBIR_strs_join1(i, ,stbir__encode_order1), STBIR_strs_join1(i, ,stbir__encode_order2), STBIR_strs_join1(i, ,stbir__encode_order3) ); + + encode += 16; + output += 16; + if ( output <= end_output ) + continue; + if ( output == ( end_output + 16 ) ) + break; + output = end_output; // backup and do last couple + encode = end_encode_m16; + } + return; + } + #endif + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while ( output <= end_output ) + { + STBIR_SIMD_NO_UNROLL(encode); + + output[0-4] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order0] ); + output[1-4] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order1] ); + output[2-4] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order2] ); + output[3-4] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order3] ); + + output += 4; + encode += 4; + } + output -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( output < end_output ) + { + STBIR_NO_UNROLL(encode); + output[0] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order0] ); + #if stbir__coder_min_num >= 2 + output[1] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order1] ); + #endif + #if stbir__coder_min_num >= 3 + output[2] = stbir__linear_to_srgb_uchar( encode[stbir__encode_order2] ); + #endif + output += stbir__coder_min_num; + encode += stbir__coder_min_num; + } + #endif +} + +#if ( stbir__coder_min_num == 4 ) || ( ( stbir__coder_min_num == 1 ) && ( !defined(stbir__decode_swizzle) ) ) + +static float * STBIR__CODER_NAME(stbir__decode_uint8_srgb4_linearalpha)( float * decodep, int width_times_channels, void const * inputp ) +{ + float STBIR_STREAMOUT_PTR( * ) decode = decodep; + float * decode_end = (float*) decode + width_times_channels; + unsigned char const * input = (unsigned char const *)inputp; + + do { + decode[0] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order0] ]; + decode[1] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order1] ]; + decode[2] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order2] ]; + decode[3] = ( (float) input[stbir__decode_order3] ) * stbir__max_uint8_as_float_inverted; + input += 4; + decode += 4; + } while( decode < decode_end ); + return decode_end; +} + + +static void STBIR__CODER_NAME( stbir__encode_uint8_srgb4_linearalpha )( void * outputp, int width_times_channels, float const * encode ) +{ + unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char*) outputp; + unsigned char * end_output = ( (unsigned char*) output ) + width_times_channels; + + #ifdef STBIR_SIMD + + if ( width_times_channels >= 16 ) + { + float const * end_encode_m16 = encode + width_times_channels - 16; + end_output -= 16; + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + stbir__simdf f0, f1, f2, f3; + stbir__simdi i0, i1, i2, i3; + + STBIR_SIMD_NO_UNROLL(encode); + stbir__simdf_load4_transposed( f0, f1, f2, f3, encode ); + + stbir__min_max_shift20( i0, f0 ); + stbir__min_max_shift20( i1, f1 ); + stbir__min_max_shift20( i2, f2 ); + stbir__scale_and_convert( i3, f3 ); + + stbir__simdi_table_lookup3( i0, i1, i2, ( fp32_to_srgb8_tab4 - (127-13)*8 ) ); + + stbir__linear_to_srgb_finish( i0, f0 ); + stbir__linear_to_srgb_finish( i1, f1 ); + stbir__linear_to_srgb_finish( i2, f2 ); + + stbir__interleave_pack_and_store_16_u8( output, STBIR_strs_join1(i, ,stbir__encode_order0), STBIR_strs_join1(i, ,stbir__encode_order1), STBIR_strs_join1(i, ,stbir__encode_order2), STBIR_strs_join1(i, ,stbir__encode_order3) ); + + output += 16; + encode += 16; + + if ( output <= end_output ) + continue; + if ( output == ( end_output + 16 ) ) + break; + output = end_output; // backup and do last couple + encode = end_encode_m16; + } + return; + } + #endif + + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float f; + STBIR_SIMD_NO_UNROLL(encode); + + output[stbir__decode_order0] = stbir__linear_to_srgb_uchar( encode[0] ); + output[stbir__decode_order1] = stbir__linear_to_srgb_uchar( encode[1] ); + output[stbir__decode_order2] = stbir__linear_to_srgb_uchar( encode[2] ); + + f = encode[3] * stbir__max_uint8_as_float + 0.5f; + STBIR_CLAMP(f, 0, 255); + output[stbir__decode_order3] = (unsigned char) f; + + output += 4; + encode += 4; + } while( output < end_output ); +} + +#endif + +#if ( stbir__coder_min_num == 2 ) || ( ( stbir__coder_min_num == 1 ) && ( !defined(stbir__decode_swizzle) ) ) + +static float * STBIR__CODER_NAME(stbir__decode_uint8_srgb2_linearalpha)( float * decodep, int width_times_channels, void const * inputp ) +{ + float STBIR_STREAMOUT_PTR( * ) decode = decodep; + float * decode_end = (float*) decode + width_times_channels; + unsigned char const * input = (unsigned char const *)inputp; + + decode += 4; + while( decode <= decode_end ) + { + decode[0-4] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order0] ]; + decode[1-4] = ( (float) input[stbir__decode_order1] ) * stbir__max_uint8_as_float_inverted; + decode[2-4] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order0+2] ]; + decode[3-4] = ( (float) input[stbir__decode_order1+2] ) * stbir__max_uint8_as_float_inverted; + input += 4; + decode += 4; + } + decode -= 4; + if( decode < decode_end ) + { + decode[0] = stbir__srgb_uchar_to_linear_float[ input[stbir__decode_order0] ]; + decode[1] = ( (float) input[stbir__decode_order1] ) * stbir__max_uint8_as_float_inverted; + } + return decode_end; +} + +static void STBIR__CODER_NAME( stbir__encode_uint8_srgb2_linearalpha )( void * outputp, int width_times_channels, float const * encode ) +{ + unsigned char STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned char*) outputp; + unsigned char * end_output = ( (unsigned char*) output ) + width_times_channels; + + #ifdef STBIR_SIMD + + if ( width_times_channels >= 16 ) + { + float const * end_encode_m16 = encode + width_times_channels - 16; + end_output -= 16; + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + stbir__simdf f0, f1, f2, f3; + stbir__simdi i0, i1, i2, i3; + + STBIR_SIMD_NO_UNROLL(encode); + stbir__simdf_load4_transposed( f0, f1, f2, f3, encode ); + + stbir__min_max_shift20( i0, f0 ); + stbir__scale_and_convert( i1, f1 ); + stbir__min_max_shift20( i2, f2 ); + stbir__scale_and_convert( i3, f3 ); + + stbir__simdi_table_lookup2( i0, i2, ( fp32_to_srgb8_tab4 - (127-13)*8 ) ); + + stbir__linear_to_srgb_finish( i0, f0 ); + stbir__linear_to_srgb_finish( i2, f2 ); + + stbir__interleave_pack_and_store_16_u8( output, STBIR_strs_join1(i, ,stbir__encode_order0), STBIR_strs_join1(i, ,stbir__encode_order1), STBIR_strs_join1(i, ,stbir__encode_order2), STBIR_strs_join1(i, ,stbir__encode_order3) ); + + output += 16; + encode += 16; + if ( output <= end_output ) + continue; + if ( output == ( end_output + 16 ) ) + break; + output = end_output; // backup and do last couple + encode = end_encode_m16; + } + return; + } + #endif + + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float f; + STBIR_SIMD_NO_UNROLL(encode); + + output[stbir__decode_order0] = stbir__linear_to_srgb_uchar( encode[0] ); + + f = encode[1] * stbir__max_uint8_as_float + 0.5f; + STBIR_CLAMP(f, 0, 255); + output[stbir__decode_order1] = (unsigned char) f; + + output += 2; + encode += 2; + } while( output < end_output ); +} + +#endif + +static float * STBIR__CODER_NAME(stbir__decode_uint16_linear_scaled)( float * decodep, int width_times_channels, void const * inputp ) +{ + float STBIR_STREAMOUT_PTR( * ) decode = decodep; + float * decode_end = (float*) decode + width_times_channels; + unsigned short const * input = (unsigned short const *)inputp; + + #ifdef STBIR_SIMD + unsigned short const * end_input_m8 = input + width_times_channels - 8; + if ( width_times_channels >= 8 ) + { + decode_end -= 8; + STBIR_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + #ifdef STBIR_SIMD8 + stbir__simdi i; stbir__simdi8 o; + stbir__simdf8 of; + STBIR_NO_UNROLL(decode); + stbir__simdi_load( i, input ); + stbir__simdi8_expand_u16_to_u32( o, i ); + stbir__simdi8_convert_i32_to_float( of, o ); + stbir__simdf8_mult( of, of, STBIR_max_uint16_as_float_inverted8); + stbir__decode_simdf8_flip( of ); + stbir__simdf8_store( decode + 0, of ); + #else + stbir__simdi i, o0, o1; + stbir__simdf of0, of1; + STBIR_NO_UNROLL(decode); + stbir__simdi_load( i, input ); + stbir__simdi_expand_u16_to_u32( o0,o1,i ); + stbir__simdi_convert_i32_to_float( of0, o0 ); + stbir__simdi_convert_i32_to_float( of1, o1 ); + stbir__simdf_mult( of0, of0, STBIR__CONSTF(STBIR_max_uint16_as_float_inverted) ); + stbir__simdf_mult( of1, of1, STBIR__CONSTF(STBIR_max_uint16_as_float_inverted)); + stbir__decode_simdf4_flip( of0 ); + stbir__decode_simdf4_flip( of1 ); + stbir__simdf_store( decode + 0, of0 ); + stbir__simdf_store( decode + 4, of1 ); + #endif + decode += 8; + input += 8; + if ( decode <= decode_end ) + continue; + if ( decode == ( decode_end + 8 ) ) + break; + decode = decode_end; // backup and do last couple + input = end_input_m8; + } + return decode_end + 8; + } + #endif + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + decode += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( decode <= decode_end ) + { + STBIR_SIMD_NO_UNROLL(decode); + decode[0-4] = ((float)(input[stbir__decode_order0])) * stbir__max_uint16_as_float_inverted; + decode[1-4] = ((float)(input[stbir__decode_order1])) * stbir__max_uint16_as_float_inverted; + decode[2-4] = ((float)(input[stbir__decode_order2])) * stbir__max_uint16_as_float_inverted; + decode[3-4] = ((float)(input[stbir__decode_order3])) * stbir__max_uint16_as_float_inverted; + decode += 4; + input += 4; + } + decode -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( decode < decode_end ) + { + STBIR_NO_UNROLL(decode); + decode[0] = ((float)(input[stbir__decode_order0])) * stbir__max_uint16_as_float_inverted; + #if stbir__coder_min_num >= 2 + decode[1] = ((float)(input[stbir__decode_order1])) * stbir__max_uint16_as_float_inverted; + #endif + #if stbir__coder_min_num >= 3 + decode[2] = ((float)(input[stbir__decode_order2])) * stbir__max_uint16_as_float_inverted; + #endif + decode += stbir__coder_min_num; + input += stbir__coder_min_num; + } + #endif + return decode_end; +} + + +static void STBIR__CODER_NAME(stbir__encode_uint16_linear_scaled)( void * outputp, int width_times_channels, float const * encode ) +{ + unsigned short STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned short*) outputp; + unsigned short * end_output = ( (unsigned short*) output ) + width_times_channels; + + #ifdef STBIR_SIMD + { + if ( width_times_channels >= stbir__simdfX_float_count*2 ) + { + float const * end_encode_m8 = encode + width_times_channels - stbir__simdfX_float_count*2; + end_output -= stbir__simdfX_float_count*2; + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + stbir__simdfX e0, e1; + stbir__simdiX i; + STBIR_SIMD_NO_UNROLL(encode); + stbir__simdfX_madd_mem( e0, STBIR_simd_point5X, STBIR_max_uint16_as_floatX, encode ); + stbir__simdfX_madd_mem( e1, STBIR_simd_point5X, STBIR_max_uint16_as_floatX, encode+stbir__simdfX_float_count ); + stbir__encode_simdfX_unflip( e0 ); + stbir__encode_simdfX_unflip( e1 ); + stbir__simdfX_pack_to_words( i, e0, e1 ); + stbir__simdiX_store( output, i ); + encode += stbir__simdfX_float_count*2; + output += stbir__simdfX_float_count*2; + if ( output <= end_output ) + continue; + if ( output == ( end_output + stbir__simdfX_float_count*2 ) ) + break; + output = end_output; // backup and do last couple + encode = end_encode_m8; + } + return; + } + } + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_NO_UNROLL_LOOP_START + while( output <= end_output ) + { + stbir__simdf e; + stbir__simdi i; + STBIR_NO_UNROLL(encode); + stbir__simdf_load( e, encode ); + stbir__simdf_madd( e, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint16_as_float), e ); + stbir__encode_simdf4_unflip( e ); + stbir__simdf_pack_to_8words( i, e, e ); // only use first 4 + stbir__simdi_store2( output-4, i ); + output += 4; + encode += 4; + } + output -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( output < end_output ) + { + stbir__simdf e; + STBIR_NO_UNROLL(encode); + stbir__simdf_madd1_mem( e, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint16_as_float), encode+stbir__encode_order0 ); output[0] = stbir__simdf_convert_float_to_short( e ); + #if stbir__coder_min_num >= 2 + stbir__simdf_madd1_mem( e, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint16_as_float), encode+stbir__encode_order1 ); output[1] = stbir__simdf_convert_float_to_short( e ); + #endif + #if stbir__coder_min_num >= 3 + stbir__simdf_madd1_mem( e, STBIR__CONSTF(STBIR_simd_point5), STBIR__CONSTF(STBIR_max_uint16_as_float), encode+stbir__encode_order2 ); output[2] = stbir__simdf_convert_float_to_short( e ); + #endif + output += stbir__coder_min_num; + encode += stbir__coder_min_num; + } + #endif + + #else + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( output <= end_output ) + { + float f; + STBIR_SIMD_NO_UNROLL(encode); + f = encode[stbir__encode_order0] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[0-4] = (unsigned short)f; + f = encode[stbir__encode_order1] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[1-4] = (unsigned short)f; + f = encode[stbir__encode_order2] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[2-4] = (unsigned short)f; + f = encode[stbir__encode_order3] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[3-4] = (unsigned short)f; + output += 4; + encode += 4; + } + output -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( output < end_output ) + { + float f; + STBIR_NO_UNROLL(encode); + f = encode[stbir__encode_order0] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[0] = (unsigned short)f; + #if stbir__coder_min_num >= 2 + f = encode[stbir__encode_order1] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[1] = (unsigned short)f; + #endif + #if stbir__coder_min_num >= 3 + f = encode[stbir__encode_order2] * stbir__max_uint16_as_float + 0.5f; STBIR_CLAMP(f, 0, 65535); output[2] = (unsigned short)f; + #endif + output += stbir__coder_min_num; + encode += stbir__coder_min_num; + } + #endif + #endif +} + +static float * STBIR__CODER_NAME(stbir__decode_uint16_linear)( float * decodep, int width_times_channels, void const * inputp ) +{ + float STBIR_STREAMOUT_PTR( * ) decode = decodep; + float * decode_end = (float*) decode + width_times_channels; + unsigned short const * input = (unsigned short const *)inputp; + + #ifdef STBIR_SIMD + unsigned short const * end_input_m8 = input + width_times_channels - 8; + if ( width_times_channels >= 8 ) + { + decode_end -= 8; + STBIR_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + #ifdef STBIR_SIMD8 + stbir__simdi i; stbir__simdi8 o; + stbir__simdf8 of; + STBIR_NO_UNROLL(decode); + stbir__simdi_load( i, input ); + stbir__simdi8_expand_u16_to_u32( o, i ); + stbir__simdi8_convert_i32_to_float( of, o ); + stbir__decode_simdf8_flip( of ); + stbir__simdf8_store( decode + 0, of ); + #else + stbir__simdi i, o0, o1; + stbir__simdf of0, of1; + STBIR_NO_UNROLL(decode); + stbir__simdi_load( i, input ); + stbir__simdi_expand_u16_to_u32( o0, o1, i ); + stbir__simdi_convert_i32_to_float( of0, o0 ); + stbir__simdi_convert_i32_to_float( of1, o1 ); + stbir__decode_simdf4_flip( of0 ); + stbir__decode_simdf4_flip( of1 ); + stbir__simdf_store( decode + 0, of0 ); + stbir__simdf_store( decode + 4, of1 ); + #endif + decode += 8; + input += 8; + if ( decode <= decode_end ) + continue; + if ( decode == ( decode_end + 8 ) ) + break; + decode = decode_end; // backup and do last couple + input = end_input_m8; + } + return decode_end + 8; + } + #endif + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + decode += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( decode <= decode_end ) + { + STBIR_SIMD_NO_UNROLL(decode); + decode[0-4] = ((float)(input[stbir__decode_order0])); + decode[1-4] = ((float)(input[stbir__decode_order1])); + decode[2-4] = ((float)(input[stbir__decode_order2])); + decode[3-4] = ((float)(input[stbir__decode_order3])); + decode += 4; + input += 4; + } + decode -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( decode < decode_end ) + { + STBIR_NO_UNROLL(decode); + decode[0] = ((float)(input[stbir__decode_order0])); + #if stbir__coder_min_num >= 2 + decode[1] = ((float)(input[stbir__decode_order1])); + #endif + #if stbir__coder_min_num >= 3 + decode[2] = ((float)(input[stbir__decode_order2])); + #endif + decode += stbir__coder_min_num; + input += stbir__coder_min_num; + } + #endif + return decode_end; +} + +static void STBIR__CODER_NAME(stbir__encode_uint16_linear)( void * outputp, int width_times_channels, float const * encode ) +{ + unsigned short STBIR_SIMD_STREAMOUT_PTR( * ) output = (unsigned short*) outputp; + unsigned short * end_output = ( (unsigned short*) output ) + width_times_channels; + + #ifdef STBIR_SIMD + { + if ( width_times_channels >= stbir__simdfX_float_count*2 ) + { + float const * end_encode_m8 = encode + width_times_channels - stbir__simdfX_float_count*2; + end_output -= stbir__simdfX_float_count*2; + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + stbir__simdfX e0, e1; + stbir__simdiX i; + STBIR_SIMD_NO_UNROLL(encode); + stbir__simdfX_add_mem( e0, STBIR_simd_point5X, encode ); + stbir__simdfX_add_mem( e1, STBIR_simd_point5X, encode+stbir__simdfX_float_count ); + stbir__encode_simdfX_unflip( e0 ); + stbir__encode_simdfX_unflip( e1 ); + stbir__simdfX_pack_to_words( i, e0, e1 ); + stbir__simdiX_store( output, i ); + encode += stbir__simdfX_float_count*2; + output += stbir__simdfX_float_count*2; + if ( output <= end_output ) + continue; + if ( output == ( end_output + stbir__simdfX_float_count*2 ) ) + break; + output = end_output; // backup and do last couple + encode = end_encode_m8; + } + return; + } + } + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_NO_UNROLL_LOOP_START + while( output <= end_output ) + { + stbir__simdf e; + stbir__simdi i; + STBIR_NO_UNROLL(encode); + stbir__simdf_load( e, encode ); + stbir__simdf_add( e, STBIR__CONSTF(STBIR_simd_point5), e ); + stbir__encode_simdf4_unflip( e ); + stbir__simdf_pack_to_8words( i, e, e ); // only use first 4 + stbir__simdi_store2( output-4, i ); + output += 4; + encode += 4; + } + output -= 4; + #endif + + #else + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( output <= end_output ) + { + float f; + STBIR_SIMD_NO_UNROLL(encode); + f = encode[stbir__encode_order0] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[0-4] = (unsigned short)f; + f = encode[stbir__encode_order1] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[1-4] = (unsigned short)f; + f = encode[stbir__encode_order2] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[2-4] = (unsigned short)f; + f = encode[stbir__encode_order3] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[3-4] = (unsigned short)f; + output += 4; + encode += 4; + } + output -= 4; + #endif + + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( output < end_output ) + { + float f; + STBIR_NO_UNROLL(encode); + f = encode[stbir__encode_order0] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[0] = (unsigned short)f; + #if stbir__coder_min_num >= 2 + f = encode[stbir__encode_order1] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[1] = (unsigned short)f; + #endif + #if stbir__coder_min_num >= 3 + f = encode[stbir__encode_order2] + 0.5f; STBIR_CLAMP(f, 0, 65535); output[2] = (unsigned short)f; + #endif + output += stbir__coder_min_num; + encode += stbir__coder_min_num; + } + #endif +} + +static float * STBIR__CODER_NAME(stbir__decode_half_float_linear)( float * decodep, int width_times_channels, void const * inputp ) +{ + float STBIR_STREAMOUT_PTR( * ) decode = decodep; + float * decode_end = (float*) decode + width_times_channels; + stbir__FP16 const * input = (stbir__FP16 const *)inputp; + + #ifdef STBIR_SIMD + if ( width_times_channels >= 8 ) + { + stbir__FP16 const * end_input_m8 = input + width_times_channels - 8; + decode_end -= 8; + STBIR_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + STBIR_NO_UNROLL(decode); + + stbir__half_to_float_SIMD( decode, input ); + #ifdef stbir__decode_swizzle + #ifdef STBIR_SIMD8 + { + stbir__simdf8 of; + stbir__simdf8_load( of, decode ); + stbir__decode_simdf8_flip( of ); + stbir__simdf8_store( decode, of ); + } + #else + { + stbir__simdf of0,of1; + stbir__simdf_load( of0, decode ); + stbir__simdf_load( of1, decode+4 ); + stbir__decode_simdf4_flip( of0 ); + stbir__decode_simdf4_flip( of1 ); + stbir__simdf_store( decode, of0 ); + stbir__simdf_store( decode+4, of1 ); + } + #endif + #endif + decode += 8; + input += 8; + if ( decode <= decode_end ) + continue; + if ( decode == ( decode_end + 8 ) ) + break; + decode = decode_end; // backup and do last couple + input = end_input_m8; + } + return decode_end + 8; + } + #endif + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + decode += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( decode <= decode_end ) + { + STBIR_SIMD_NO_UNROLL(decode); + decode[0-4] = stbir__half_to_float(input[stbir__decode_order0]); + decode[1-4] = stbir__half_to_float(input[stbir__decode_order1]); + decode[2-4] = stbir__half_to_float(input[stbir__decode_order2]); + decode[3-4] = stbir__half_to_float(input[stbir__decode_order3]); + decode += 4; + input += 4; + } + decode -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( decode < decode_end ) + { + STBIR_NO_UNROLL(decode); + decode[0] = stbir__half_to_float(input[stbir__decode_order0]); + #if stbir__coder_min_num >= 2 + decode[1] = stbir__half_to_float(input[stbir__decode_order1]); + #endif + #if stbir__coder_min_num >= 3 + decode[2] = stbir__half_to_float(input[stbir__decode_order2]); + #endif + decode += stbir__coder_min_num; + input += stbir__coder_min_num; + } + #endif + return decode_end; +} + +static void STBIR__CODER_NAME( stbir__encode_half_float_linear )( void * outputp, int width_times_channels, float const * encode ) +{ + stbir__FP16 STBIR_SIMD_STREAMOUT_PTR( * ) output = (stbir__FP16*) outputp; + stbir__FP16 * end_output = ( (stbir__FP16*) output ) + width_times_channels; + + #ifdef STBIR_SIMD + if ( width_times_channels >= 8 ) + { + float const * end_encode_m8 = encode + width_times_channels - 8; + end_output -= 8; + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + STBIR_SIMD_NO_UNROLL(encode); + #ifdef stbir__decode_swizzle + #ifdef STBIR_SIMD8 + { + stbir__simdf8 of; + stbir__simdf8_load( of, encode ); + stbir__encode_simdf8_unflip( of ); + stbir__float_to_half_SIMD( output, (float*)&of ); + } + #else + { + stbir__simdf of[2]; + stbir__simdf_load( of[0], encode ); + stbir__simdf_load( of[1], encode+4 ); + stbir__encode_simdf4_unflip( of[0] ); + stbir__encode_simdf4_unflip( of[1] ); + stbir__float_to_half_SIMD( output, (float*)of ); + } + #endif + #else + stbir__float_to_half_SIMD( output, encode ); + #endif + encode += 8; + output += 8; + if ( output <= end_output ) + continue; + if ( output == ( end_output + 8 ) ) + break; + output = end_output; // backup and do last couple + encode = end_encode_m8; + } + return; + } + #endif + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( output <= end_output ) + { + STBIR_SIMD_NO_UNROLL(output); + output[0-4] = stbir__float_to_half(encode[stbir__encode_order0]); + output[1-4] = stbir__float_to_half(encode[stbir__encode_order1]); + output[2-4] = stbir__float_to_half(encode[stbir__encode_order2]); + output[3-4] = stbir__float_to_half(encode[stbir__encode_order3]); + output += 4; + encode += 4; + } + output -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( output < end_output ) + { + STBIR_NO_UNROLL(output); + output[0] = stbir__float_to_half(encode[stbir__encode_order0]); + #if stbir__coder_min_num >= 2 + output[1] = stbir__float_to_half(encode[stbir__encode_order1]); + #endif + #if stbir__coder_min_num >= 3 + output[2] = stbir__float_to_half(encode[stbir__encode_order2]); + #endif + output += stbir__coder_min_num; + encode += stbir__coder_min_num; + } + #endif +} + +static float * STBIR__CODER_NAME(stbir__decode_float_linear)( float * decodep, int width_times_channels, void const * inputp ) +{ + #ifdef stbir__decode_swizzle + float STBIR_STREAMOUT_PTR( * ) decode = decodep; + float * decode_end = (float*) decode + width_times_channels; + float const * input = (float const *)inputp; + + #ifdef STBIR_SIMD + if ( width_times_channels >= 16 ) + { + float const * end_input_m16 = input + width_times_channels - 16; + decode_end -= 16; + STBIR_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + STBIR_NO_UNROLL(decode); + #ifdef stbir__decode_swizzle + #ifdef STBIR_SIMD8 + { + stbir__simdf8 of0,of1; + stbir__simdf8_load( of0, input ); + stbir__simdf8_load( of1, input+8 ); + stbir__decode_simdf8_flip( of0 ); + stbir__decode_simdf8_flip( of1 ); + stbir__simdf8_store( decode, of0 ); + stbir__simdf8_store( decode+8, of1 ); + } + #else + { + stbir__simdf of0,of1,of2,of3; + stbir__simdf_load( of0, input ); + stbir__simdf_load( of1, input+4 ); + stbir__simdf_load( of2, input+8 ); + stbir__simdf_load( of3, input+12 ); + stbir__decode_simdf4_flip( of0 ); + stbir__decode_simdf4_flip( of1 ); + stbir__decode_simdf4_flip( of2 ); + stbir__decode_simdf4_flip( of3 ); + stbir__simdf_store( decode, of0 ); + stbir__simdf_store( decode+4, of1 ); + stbir__simdf_store( decode+8, of2 ); + stbir__simdf_store( decode+12, of3 ); + } + #endif + #endif + decode += 16; + input += 16; + if ( decode <= decode_end ) + continue; + if ( decode == ( decode_end + 16 ) ) + break; + decode = decode_end; // backup and do last couple + input = end_input_m16; + } + return decode_end + 16; + } + #endif + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + decode += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( decode <= decode_end ) + { + STBIR_SIMD_NO_UNROLL(decode); + decode[0-4] = input[stbir__decode_order0]; + decode[1-4] = input[stbir__decode_order1]; + decode[2-4] = input[stbir__decode_order2]; + decode[3-4] = input[stbir__decode_order3]; + decode += 4; + input += 4; + } + decode -= 4; + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( decode < decode_end ) + { + STBIR_NO_UNROLL(decode); + decode[0] = input[stbir__decode_order0]; + #if stbir__coder_min_num >= 2 + decode[1] = input[stbir__decode_order1]; + #endif + #if stbir__coder_min_num >= 3 + decode[2] = input[stbir__decode_order2]; + #endif + decode += stbir__coder_min_num; + input += stbir__coder_min_num; + } + #endif + return decode_end; + + #else + + if ( (void*)decodep != inputp ) + STBIR_MEMCPY( decodep, inputp, width_times_channels * sizeof( float ) ); + + return decodep + width_times_channels; + + #endif +} + +static void STBIR__CODER_NAME( stbir__encode_float_linear )( void * outputp, int width_times_channels, float const * encode ) +{ + #if !defined( STBIR_FLOAT_HIGH_CLAMP ) && !defined(STBIR_FLOAT_LOW_CLAMP) && !defined(stbir__decode_swizzle) + + if ( (void*)outputp != (void*) encode ) + STBIR_MEMCPY( outputp, encode, width_times_channels * sizeof( float ) ); + + #else + + float STBIR_SIMD_STREAMOUT_PTR( * ) output = (float*) outputp; + float * end_output = ( (float*) output ) + width_times_channels; + + #ifdef STBIR_FLOAT_HIGH_CLAMP + #define stbir_scalar_hi_clamp( v ) if ( v > STBIR_FLOAT_HIGH_CLAMP ) v = STBIR_FLOAT_HIGH_CLAMP; + #else + #define stbir_scalar_hi_clamp( v ) + #endif + #ifdef STBIR_FLOAT_LOW_CLAMP + #define stbir_scalar_lo_clamp( v ) if ( v < STBIR_FLOAT_LOW_CLAMP ) v = STBIR_FLOAT_LOW_CLAMP; + #else + #define stbir_scalar_lo_clamp( v ) + #endif + + #ifdef STBIR_SIMD + + #ifdef STBIR_FLOAT_HIGH_CLAMP + const stbir__simdfX high_clamp = stbir__simdf_frepX(STBIR_FLOAT_HIGH_CLAMP); + #endif + #ifdef STBIR_FLOAT_LOW_CLAMP + const stbir__simdfX low_clamp = stbir__simdf_frepX(STBIR_FLOAT_LOW_CLAMP); + #endif + + if ( width_times_channels >= ( stbir__simdfX_float_count * 2 ) ) + { + float const * end_encode_m8 = encode + width_times_channels - ( stbir__simdfX_float_count * 2 ); + end_output -= ( stbir__simdfX_float_count * 2 ); + STBIR_SIMD_NO_UNROLL_LOOP_START_INF_FOR + for(;;) + { + stbir__simdfX e0, e1; + STBIR_SIMD_NO_UNROLL(encode); + stbir__simdfX_load( e0, encode ); + stbir__simdfX_load( e1, encode+stbir__simdfX_float_count ); +#ifdef STBIR_FLOAT_HIGH_CLAMP + stbir__simdfX_min( e0, e0, high_clamp ); + stbir__simdfX_min( e1, e1, high_clamp ); +#endif +#ifdef STBIR_FLOAT_LOW_CLAMP + stbir__simdfX_max( e0, e0, low_clamp ); + stbir__simdfX_max( e1, e1, low_clamp ); +#endif + stbir__encode_simdfX_unflip( e0 ); + stbir__encode_simdfX_unflip( e1 ); + stbir__simdfX_store( output, e0 ); + stbir__simdfX_store( output+stbir__simdfX_float_count, e1 ); + encode += stbir__simdfX_float_count * 2; + output += stbir__simdfX_float_count * 2; + if ( output <= end_output ) + continue; + if ( output == ( end_output + ( stbir__simdfX_float_count * 2 ) ) ) + break; + output = end_output; // backup and do last couple + encode = end_encode_m8; + } + return; + } + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_NO_UNROLL_LOOP_START + while( output <= end_output ) + { + stbir__simdf e0; + STBIR_NO_UNROLL(encode); + stbir__simdf_load( e0, encode ); +#ifdef STBIR_FLOAT_HIGH_CLAMP + stbir__simdf_min( e0, e0, high_clamp ); +#endif +#ifdef STBIR_FLOAT_LOW_CLAMP + stbir__simdf_max( e0, e0, low_clamp ); +#endif + stbir__encode_simdf4_unflip( e0 ); + stbir__simdf_store( output-4, e0 ); + output += 4; + encode += 4; + } + output -= 4; + #endif + + #else + + // try to do blocks of 4 when you can + #if stbir__coder_min_num != 3 // doesn't divide cleanly by four + output += 4; + STBIR_SIMD_NO_UNROLL_LOOP_START + while( output <= end_output ) + { + float e; + STBIR_SIMD_NO_UNROLL(encode); + e = encode[ stbir__encode_order0 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[0-4] = e; + e = encode[ stbir__encode_order1 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[1-4] = e; + e = encode[ stbir__encode_order2 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[2-4] = e; + e = encode[ stbir__encode_order3 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[3-4] = e; + output += 4; + encode += 4; + } + output -= 4; + + #endif + + #endif + + // do the remnants + #if stbir__coder_min_num < 4 + STBIR_NO_UNROLL_LOOP_START + while( output < end_output ) + { + float e; + STBIR_NO_UNROLL(encode); + e = encode[ stbir__encode_order0 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[0] = e; + #if stbir__coder_min_num >= 2 + e = encode[ stbir__encode_order1 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[1] = e; + #endif + #if stbir__coder_min_num >= 3 + e = encode[ stbir__encode_order2 ]; stbir_scalar_hi_clamp( e ); stbir_scalar_lo_clamp( e ); output[2] = e; + #endif + output += stbir__coder_min_num; + encode += stbir__coder_min_num; + } + #endif + + #endif +} + +#undef stbir__decode_suffix +#undef stbir__decode_simdf8_flip +#undef stbir__decode_simdf4_flip +#undef stbir__decode_order0 +#undef stbir__decode_order1 +#undef stbir__decode_order2 +#undef stbir__decode_order3 +#undef stbir__encode_order0 +#undef stbir__encode_order1 +#undef stbir__encode_order2 +#undef stbir__encode_order3 +#undef stbir__encode_simdf8_unflip +#undef stbir__encode_simdf4_unflip +#undef stbir__encode_simdfX_unflip +#undef STBIR__CODER_NAME +#undef stbir__coder_min_num +#undef stbir__decode_swizzle +#undef stbir_scalar_hi_clamp +#undef stbir_scalar_lo_clamp +#undef STB_IMAGE_RESIZE_DO_CODERS + +#elif defined( STB_IMAGE_RESIZE_DO_VERTICALS) + +#ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#define STBIR_chans( start, end ) STBIR_strs_join14(start,STBIR__vertical_channels,end,_cont) +#else +#define STBIR_chans( start, end ) STBIR_strs_join1(start,STBIR__vertical_channels,end) +#endif + +#if STBIR__vertical_channels >= 1 +#define stbIF0( code ) code +#else +#define stbIF0( code ) +#endif +#if STBIR__vertical_channels >= 2 +#define stbIF1( code ) code +#else +#define stbIF1( code ) +#endif +#if STBIR__vertical_channels >= 3 +#define stbIF2( code ) code +#else +#define stbIF2( code ) +#endif +#if STBIR__vertical_channels >= 4 +#define stbIF3( code ) code +#else +#define stbIF3( code ) +#endif +#if STBIR__vertical_channels >= 5 +#define stbIF4( code ) code +#else +#define stbIF4( code ) +#endif +#if STBIR__vertical_channels >= 6 +#define stbIF5( code ) code +#else +#define stbIF5( code ) +#endif +#if STBIR__vertical_channels >= 7 +#define stbIF6( code ) code +#else +#define stbIF6( code ) +#endif +#if STBIR__vertical_channels >= 8 +#define stbIF7( code ) code +#else +#define stbIF7( code ) +#endif + +static void STBIR_chans( stbir__vertical_scatter_with_,_coeffs)( float ** outputs, float const * vertical_coefficients, float const * input, float const * input_end ) +{ + stbIF0( float STBIR_SIMD_STREAMOUT_PTR( * ) output0 = outputs[0]; float c0s = vertical_coefficients[0]; ) + stbIF1( float STBIR_SIMD_STREAMOUT_PTR( * ) output1 = outputs[1]; float c1s = vertical_coefficients[1]; ) + stbIF2( float STBIR_SIMD_STREAMOUT_PTR( * ) output2 = outputs[2]; float c2s = vertical_coefficients[2]; ) + stbIF3( float STBIR_SIMD_STREAMOUT_PTR( * ) output3 = outputs[3]; float c3s = vertical_coefficients[3]; ) + stbIF4( float STBIR_SIMD_STREAMOUT_PTR( * ) output4 = outputs[4]; float c4s = vertical_coefficients[4]; ) + stbIF5( float STBIR_SIMD_STREAMOUT_PTR( * ) output5 = outputs[5]; float c5s = vertical_coefficients[5]; ) + stbIF6( float STBIR_SIMD_STREAMOUT_PTR( * ) output6 = outputs[6]; float c6s = vertical_coefficients[6]; ) + stbIF7( float STBIR_SIMD_STREAMOUT_PTR( * ) output7 = outputs[7]; float c7s = vertical_coefficients[7]; ) + + #ifdef STBIR_SIMD + { + stbIF0(stbir__simdfX c0 = stbir__simdf_frepX( c0s ); ) + stbIF1(stbir__simdfX c1 = stbir__simdf_frepX( c1s ); ) + stbIF2(stbir__simdfX c2 = stbir__simdf_frepX( c2s ); ) + stbIF3(stbir__simdfX c3 = stbir__simdf_frepX( c3s ); ) + stbIF4(stbir__simdfX c4 = stbir__simdf_frepX( c4s ); ) + stbIF5(stbir__simdfX c5 = stbir__simdf_frepX( c5s ); ) + stbIF6(stbir__simdfX c6 = stbir__simdf_frepX( c6s ); ) + stbIF7(stbir__simdfX c7 = stbir__simdf_frepX( c7s ); ) + STBIR_SIMD_NO_UNROLL_LOOP_START + while ( ( (char*)input_end - (char*) input ) >= (16*stbir__simdfX_float_count) ) + { + stbir__simdfX o0, o1, o2, o3, r0, r1, r2, r3; + STBIR_SIMD_NO_UNROLL(output0); + + stbir__simdfX_load( r0, input ); stbir__simdfX_load( r1, input+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input+(3*stbir__simdfX_float_count) ); + + #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE + stbIF0( stbir__simdfX_load( o0, output0 ); stbir__simdfX_load( o1, output0+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output0+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output0+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c0 ); stbir__simdfX_madd( o1, o1, r1, c0 ); stbir__simdfX_madd( o2, o2, r2, c0 ); stbir__simdfX_madd( o3, o3, r3, c0 ); + stbir__simdfX_store( output0, o0 ); stbir__simdfX_store( output0+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output0+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output0+(3*stbir__simdfX_float_count), o3 ); ) + stbIF1( stbir__simdfX_load( o0, output1 ); stbir__simdfX_load( o1, output1+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output1+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output1+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c1 ); stbir__simdfX_madd( o1, o1, r1, c1 ); stbir__simdfX_madd( o2, o2, r2, c1 ); stbir__simdfX_madd( o3, o3, r3, c1 ); + stbir__simdfX_store( output1, o0 ); stbir__simdfX_store( output1+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output1+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output1+(3*stbir__simdfX_float_count), o3 ); ) + stbIF2( stbir__simdfX_load( o0, output2 ); stbir__simdfX_load( o1, output2+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output2+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output2+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c2 ); stbir__simdfX_madd( o1, o1, r1, c2 ); stbir__simdfX_madd( o2, o2, r2, c2 ); stbir__simdfX_madd( o3, o3, r3, c2 ); + stbir__simdfX_store( output2, o0 ); stbir__simdfX_store( output2+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output2+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output2+(3*stbir__simdfX_float_count), o3 ); ) + stbIF3( stbir__simdfX_load( o0, output3 ); stbir__simdfX_load( o1, output3+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output3+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output3+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c3 ); stbir__simdfX_madd( o1, o1, r1, c3 ); stbir__simdfX_madd( o2, o2, r2, c3 ); stbir__simdfX_madd( o3, o3, r3, c3 ); + stbir__simdfX_store( output3, o0 ); stbir__simdfX_store( output3+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output3+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output3+(3*stbir__simdfX_float_count), o3 ); ) + stbIF4( stbir__simdfX_load( o0, output4 ); stbir__simdfX_load( o1, output4+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output4+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output4+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c4 ); stbir__simdfX_madd( o1, o1, r1, c4 ); stbir__simdfX_madd( o2, o2, r2, c4 ); stbir__simdfX_madd( o3, o3, r3, c4 ); + stbir__simdfX_store( output4, o0 ); stbir__simdfX_store( output4+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output4+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output4+(3*stbir__simdfX_float_count), o3 ); ) + stbIF5( stbir__simdfX_load( o0, output5 ); stbir__simdfX_load( o1, output5+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output5+(2*stbir__simdfX_float_count)); stbir__simdfX_load( o3, output5+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c5 ); stbir__simdfX_madd( o1, o1, r1, c5 ); stbir__simdfX_madd( o2, o2, r2, c5 ); stbir__simdfX_madd( o3, o3, r3, c5 ); + stbir__simdfX_store( output5, o0 ); stbir__simdfX_store( output5+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output5+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output5+(3*stbir__simdfX_float_count), o3 ); ) + stbIF6( stbir__simdfX_load( o0, output6 ); stbir__simdfX_load( o1, output6+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output6+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output6+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c6 ); stbir__simdfX_madd( o1, o1, r1, c6 ); stbir__simdfX_madd( o2, o2, r2, c6 ); stbir__simdfX_madd( o3, o3, r3, c6 ); + stbir__simdfX_store( output6, o0 ); stbir__simdfX_store( output6+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output6+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output6+(3*stbir__simdfX_float_count), o3 ); ) + stbIF7( stbir__simdfX_load( o0, output7 ); stbir__simdfX_load( o1, output7+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output7+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output7+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c7 ); stbir__simdfX_madd( o1, o1, r1, c7 ); stbir__simdfX_madd( o2, o2, r2, c7 ); stbir__simdfX_madd( o3, o3, r3, c7 ); + stbir__simdfX_store( output7, o0 ); stbir__simdfX_store( output7+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output7+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output7+(3*stbir__simdfX_float_count), o3 ); ) + #else + stbIF0( stbir__simdfX_mult( o0, r0, c0 ); stbir__simdfX_mult( o1, r1, c0 ); stbir__simdfX_mult( o2, r2, c0 ); stbir__simdfX_mult( o3, r3, c0 ); + stbir__simdfX_store( output0, o0 ); stbir__simdfX_store( output0+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output0+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output0+(3*stbir__simdfX_float_count), o3 ); ) + stbIF1( stbir__simdfX_mult( o0, r0, c1 ); stbir__simdfX_mult( o1, r1, c1 ); stbir__simdfX_mult( o2, r2, c1 ); stbir__simdfX_mult( o3, r3, c1 ); + stbir__simdfX_store( output1, o0 ); stbir__simdfX_store( output1+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output1+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output1+(3*stbir__simdfX_float_count), o3 ); ) + stbIF2( stbir__simdfX_mult( o0, r0, c2 ); stbir__simdfX_mult( o1, r1, c2 ); stbir__simdfX_mult( o2, r2, c2 ); stbir__simdfX_mult( o3, r3, c2 ); + stbir__simdfX_store( output2, o0 ); stbir__simdfX_store( output2+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output2+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output2+(3*stbir__simdfX_float_count), o3 ); ) + stbIF3( stbir__simdfX_mult( o0, r0, c3 ); stbir__simdfX_mult( o1, r1, c3 ); stbir__simdfX_mult( o2, r2, c3 ); stbir__simdfX_mult( o3, r3, c3 ); + stbir__simdfX_store( output3, o0 ); stbir__simdfX_store( output3+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output3+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output3+(3*stbir__simdfX_float_count), o3 ); ) + stbIF4( stbir__simdfX_mult( o0, r0, c4 ); stbir__simdfX_mult( o1, r1, c4 ); stbir__simdfX_mult( o2, r2, c4 ); stbir__simdfX_mult( o3, r3, c4 ); + stbir__simdfX_store( output4, o0 ); stbir__simdfX_store( output4+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output4+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output4+(3*stbir__simdfX_float_count), o3 ); ) + stbIF5( stbir__simdfX_mult( o0, r0, c5 ); stbir__simdfX_mult( o1, r1, c5 ); stbir__simdfX_mult( o2, r2, c5 ); stbir__simdfX_mult( o3, r3, c5 ); + stbir__simdfX_store( output5, o0 ); stbir__simdfX_store( output5+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output5+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output5+(3*stbir__simdfX_float_count), o3 ); ) + stbIF6( stbir__simdfX_mult( o0, r0, c6 ); stbir__simdfX_mult( o1, r1, c6 ); stbir__simdfX_mult( o2, r2, c6 ); stbir__simdfX_mult( o3, r3, c6 ); + stbir__simdfX_store( output6, o0 ); stbir__simdfX_store( output6+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output6+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output6+(3*stbir__simdfX_float_count), o3 ); ) + stbIF7( stbir__simdfX_mult( o0, r0, c7 ); stbir__simdfX_mult( o1, r1, c7 ); stbir__simdfX_mult( o2, r2, c7 ); stbir__simdfX_mult( o3, r3, c7 ); + stbir__simdfX_store( output7, o0 ); stbir__simdfX_store( output7+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output7+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output7+(3*stbir__simdfX_float_count), o3 ); ) + #endif + + input += (4*stbir__simdfX_float_count); + stbIF0( output0 += (4*stbir__simdfX_float_count); ) stbIF1( output1 += (4*stbir__simdfX_float_count); ) stbIF2( output2 += (4*stbir__simdfX_float_count); ) stbIF3( output3 += (4*stbir__simdfX_float_count); ) stbIF4( output4 += (4*stbir__simdfX_float_count); ) stbIF5( output5 += (4*stbir__simdfX_float_count); ) stbIF6( output6 += (4*stbir__simdfX_float_count); ) stbIF7( output7 += (4*stbir__simdfX_float_count); ) + } + STBIR_SIMD_NO_UNROLL_LOOP_START + while ( ( (char*)input_end - (char*) input ) >= 16 ) + { + stbir__simdf o0, r0; + STBIR_SIMD_NO_UNROLL(output0); + + stbir__simdf_load( r0, input ); + + #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE + stbIF0( stbir__simdf_load( o0, output0 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c0 ) ); stbir__simdf_store( output0, o0 ); ) + stbIF1( stbir__simdf_load( o0, output1 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c1 ) ); stbir__simdf_store( output1, o0 ); ) + stbIF2( stbir__simdf_load( o0, output2 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c2 ) ); stbir__simdf_store( output2, o0 ); ) + stbIF3( stbir__simdf_load( o0, output3 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c3 ) ); stbir__simdf_store( output3, o0 ); ) + stbIF4( stbir__simdf_load( o0, output4 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c4 ) ); stbir__simdf_store( output4, o0 ); ) + stbIF5( stbir__simdf_load( o0, output5 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c5 ) ); stbir__simdf_store( output5, o0 ); ) + stbIF6( stbir__simdf_load( o0, output6 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c6 ) ); stbir__simdf_store( output6, o0 ); ) + stbIF7( stbir__simdf_load( o0, output7 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c7 ) ); stbir__simdf_store( output7, o0 ); ) + #else + stbIF0( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c0 ) ); stbir__simdf_store( output0, o0 ); ) + stbIF1( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c1 ) ); stbir__simdf_store( output1, o0 ); ) + stbIF2( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c2 ) ); stbir__simdf_store( output2, o0 ); ) + stbIF3( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c3 ) ); stbir__simdf_store( output3, o0 ); ) + stbIF4( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c4 ) ); stbir__simdf_store( output4, o0 ); ) + stbIF5( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c5 ) ); stbir__simdf_store( output5, o0 ); ) + stbIF6( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c6 ) ); stbir__simdf_store( output6, o0 ); ) + stbIF7( stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c7 ) ); stbir__simdf_store( output7, o0 ); ) + #endif + + input += 4; + stbIF0( output0 += 4; ) stbIF1( output1 += 4; ) stbIF2( output2 += 4; ) stbIF3( output3 += 4; ) stbIF4( output4 += 4; ) stbIF5( output5 += 4; ) stbIF6( output6 += 4; ) stbIF7( output7 += 4; ) + } + } + #else + STBIR_NO_UNROLL_LOOP_START + while ( ( (char*)input_end - (char*) input ) >= 16 ) + { + float r0, r1, r2, r3; + STBIR_NO_UNROLL(input); + + r0 = input[0], r1 = input[1], r2 = input[2], r3 = input[3]; + + #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE + stbIF0( output0[0] += ( r0 * c0s ); output0[1] += ( r1 * c0s ); output0[2] += ( r2 * c0s ); output0[3] += ( r3 * c0s ); ) + stbIF1( output1[0] += ( r0 * c1s ); output1[1] += ( r1 * c1s ); output1[2] += ( r2 * c1s ); output1[3] += ( r3 * c1s ); ) + stbIF2( output2[0] += ( r0 * c2s ); output2[1] += ( r1 * c2s ); output2[2] += ( r2 * c2s ); output2[3] += ( r3 * c2s ); ) + stbIF3( output3[0] += ( r0 * c3s ); output3[1] += ( r1 * c3s ); output3[2] += ( r2 * c3s ); output3[3] += ( r3 * c3s ); ) + stbIF4( output4[0] += ( r0 * c4s ); output4[1] += ( r1 * c4s ); output4[2] += ( r2 * c4s ); output4[3] += ( r3 * c4s ); ) + stbIF5( output5[0] += ( r0 * c5s ); output5[1] += ( r1 * c5s ); output5[2] += ( r2 * c5s ); output5[3] += ( r3 * c5s ); ) + stbIF6( output6[0] += ( r0 * c6s ); output6[1] += ( r1 * c6s ); output6[2] += ( r2 * c6s ); output6[3] += ( r3 * c6s ); ) + stbIF7( output7[0] += ( r0 * c7s ); output7[1] += ( r1 * c7s ); output7[2] += ( r2 * c7s ); output7[3] += ( r3 * c7s ); ) + #else + stbIF0( output0[0] = ( r0 * c0s ); output0[1] = ( r1 * c0s ); output0[2] = ( r2 * c0s ); output0[3] = ( r3 * c0s ); ) + stbIF1( output1[0] = ( r0 * c1s ); output1[1] = ( r1 * c1s ); output1[2] = ( r2 * c1s ); output1[3] = ( r3 * c1s ); ) + stbIF2( output2[0] = ( r0 * c2s ); output2[1] = ( r1 * c2s ); output2[2] = ( r2 * c2s ); output2[3] = ( r3 * c2s ); ) + stbIF3( output3[0] = ( r0 * c3s ); output3[1] = ( r1 * c3s ); output3[2] = ( r2 * c3s ); output3[3] = ( r3 * c3s ); ) + stbIF4( output4[0] = ( r0 * c4s ); output4[1] = ( r1 * c4s ); output4[2] = ( r2 * c4s ); output4[3] = ( r3 * c4s ); ) + stbIF5( output5[0] = ( r0 * c5s ); output5[1] = ( r1 * c5s ); output5[2] = ( r2 * c5s ); output5[3] = ( r3 * c5s ); ) + stbIF6( output6[0] = ( r0 * c6s ); output6[1] = ( r1 * c6s ); output6[2] = ( r2 * c6s ); output6[3] = ( r3 * c6s ); ) + stbIF7( output7[0] = ( r0 * c7s ); output7[1] = ( r1 * c7s ); output7[2] = ( r2 * c7s ); output7[3] = ( r3 * c7s ); ) + #endif + + input += 4; + stbIF0( output0 += 4; ) stbIF1( output1 += 4; ) stbIF2( output2 += 4; ) stbIF3( output3 += 4; ) stbIF4( output4 += 4; ) stbIF5( output5 += 4; ) stbIF6( output6 += 4; ) stbIF7( output7 += 4; ) + } + #endif + STBIR_NO_UNROLL_LOOP_START + while ( input < input_end ) + { + float r = input[0]; + STBIR_NO_UNROLL(output0); + + #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE + stbIF0( output0[0] += ( r * c0s ); ) + stbIF1( output1[0] += ( r * c1s ); ) + stbIF2( output2[0] += ( r * c2s ); ) + stbIF3( output3[0] += ( r * c3s ); ) + stbIF4( output4[0] += ( r * c4s ); ) + stbIF5( output5[0] += ( r * c5s ); ) + stbIF6( output6[0] += ( r * c6s ); ) + stbIF7( output7[0] += ( r * c7s ); ) + #else + stbIF0( output0[0] = ( r * c0s ); ) + stbIF1( output1[0] = ( r * c1s ); ) + stbIF2( output2[0] = ( r * c2s ); ) + stbIF3( output3[0] = ( r * c3s ); ) + stbIF4( output4[0] = ( r * c4s ); ) + stbIF5( output5[0] = ( r * c5s ); ) + stbIF6( output6[0] = ( r * c6s ); ) + stbIF7( output7[0] = ( r * c7s ); ) + #endif + + ++input; + stbIF0( ++output0; ) stbIF1( ++output1; ) stbIF2( ++output2; ) stbIF3( ++output3; ) stbIF4( ++output4; ) stbIF5( ++output5; ) stbIF6( ++output6; ) stbIF7( ++output7; ) + } +} + +static void STBIR_chans( stbir__vertical_gather_with_,_coeffs)( float * outputp, float const * vertical_coefficients, float const ** inputs, float const * input0_end ) +{ + float STBIR_SIMD_STREAMOUT_PTR( * ) output = outputp; + + stbIF0( float const * input0 = inputs[0]; float c0s = vertical_coefficients[0]; ) + stbIF1( float const * input1 = inputs[1]; float c1s = vertical_coefficients[1]; ) + stbIF2( float const * input2 = inputs[2]; float c2s = vertical_coefficients[2]; ) + stbIF3( float const * input3 = inputs[3]; float c3s = vertical_coefficients[3]; ) + stbIF4( float const * input4 = inputs[4]; float c4s = vertical_coefficients[4]; ) + stbIF5( float const * input5 = inputs[5]; float c5s = vertical_coefficients[5]; ) + stbIF6( float const * input6 = inputs[6]; float c6s = vertical_coefficients[6]; ) + stbIF7( float const * input7 = inputs[7]; float c7s = vertical_coefficients[7]; ) + +#if ( STBIR__vertical_channels == 1 ) && !defined(STB_IMAGE_RESIZE_VERTICAL_CONTINUE) + // check single channel one weight + if ( ( c0s >= (1.0f-0.000001f) ) && ( c0s <= (1.0f+0.000001f) ) ) + { + STBIR_MEMCPY( output, input0, (char*)input0_end - (char*)input0 ); + return; + } +#endif + + #ifdef STBIR_SIMD + { + stbIF0(stbir__simdfX c0 = stbir__simdf_frepX( c0s ); ) + stbIF1(stbir__simdfX c1 = stbir__simdf_frepX( c1s ); ) + stbIF2(stbir__simdfX c2 = stbir__simdf_frepX( c2s ); ) + stbIF3(stbir__simdfX c3 = stbir__simdf_frepX( c3s ); ) + stbIF4(stbir__simdfX c4 = stbir__simdf_frepX( c4s ); ) + stbIF5(stbir__simdfX c5 = stbir__simdf_frepX( c5s ); ) + stbIF6(stbir__simdfX c6 = stbir__simdf_frepX( c6s ); ) + stbIF7(stbir__simdfX c7 = stbir__simdf_frepX( c7s ); ) + + STBIR_SIMD_NO_UNROLL_LOOP_START + while ( ( (char*)input0_end - (char*) input0 ) >= (16*stbir__simdfX_float_count) ) + { + stbir__simdfX o0, o1, o2, o3, r0, r1, r2, r3; + STBIR_SIMD_NO_UNROLL(output); + + // prefetch four loop iterations ahead (doesn't affect much for small resizes, but helps with big ones) + stbIF0( stbir__prefetch( input0 + (16*stbir__simdfX_float_count) ); ) + stbIF1( stbir__prefetch( input1 + (16*stbir__simdfX_float_count) ); ) + stbIF2( stbir__prefetch( input2 + (16*stbir__simdfX_float_count) ); ) + stbIF3( stbir__prefetch( input3 + (16*stbir__simdfX_float_count) ); ) + stbIF4( stbir__prefetch( input4 + (16*stbir__simdfX_float_count) ); ) + stbIF5( stbir__prefetch( input5 + (16*stbir__simdfX_float_count) ); ) + stbIF6( stbir__prefetch( input6 + (16*stbir__simdfX_float_count) ); ) + stbIF7( stbir__prefetch( input7 + (16*stbir__simdfX_float_count) ); ) + + #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE + stbIF0( stbir__simdfX_load( o0, output ); stbir__simdfX_load( o1, output+stbir__simdfX_float_count ); stbir__simdfX_load( o2, output+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( o3, output+(3*stbir__simdfX_float_count) ); + stbir__simdfX_load( r0, input0 ); stbir__simdfX_load( r1, input0+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input0+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input0+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c0 ); stbir__simdfX_madd( o1, o1, r1, c0 ); stbir__simdfX_madd( o2, o2, r2, c0 ); stbir__simdfX_madd( o3, o3, r3, c0 ); ) + #else + stbIF0( stbir__simdfX_load( r0, input0 ); stbir__simdfX_load( r1, input0+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input0+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input0+(3*stbir__simdfX_float_count) ); + stbir__simdfX_mult( o0, r0, c0 ); stbir__simdfX_mult( o1, r1, c0 ); stbir__simdfX_mult( o2, r2, c0 ); stbir__simdfX_mult( o3, r3, c0 ); ) + #endif + + stbIF1( stbir__simdfX_load( r0, input1 ); stbir__simdfX_load( r1, input1+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input1+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input1+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c1 ); stbir__simdfX_madd( o1, o1, r1, c1 ); stbir__simdfX_madd( o2, o2, r2, c1 ); stbir__simdfX_madd( o3, o3, r3, c1 ); ) + stbIF2( stbir__simdfX_load( r0, input2 ); stbir__simdfX_load( r1, input2+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input2+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input2+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c2 ); stbir__simdfX_madd( o1, o1, r1, c2 ); stbir__simdfX_madd( o2, o2, r2, c2 ); stbir__simdfX_madd( o3, o3, r3, c2 ); ) + stbIF3( stbir__simdfX_load( r0, input3 ); stbir__simdfX_load( r1, input3+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input3+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input3+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c3 ); stbir__simdfX_madd( o1, o1, r1, c3 ); stbir__simdfX_madd( o2, o2, r2, c3 ); stbir__simdfX_madd( o3, o3, r3, c3 ); ) + stbIF4( stbir__simdfX_load( r0, input4 ); stbir__simdfX_load( r1, input4+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input4+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input4+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c4 ); stbir__simdfX_madd( o1, o1, r1, c4 ); stbir__simdfX_madd( o2, o2, r2, c4 ); stbir__simdfX_madd( o3, o3, r3, c4 ); ) + stbIF5( stbir__simdfX_load( r0, input5 ); stbir__simdfX_load( r1, input5+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input5+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input5+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c5 ); stbir__simdfX_madd( o1, o1, r1, c5 ); stbir__simdfX_madd( o2, o2, r2, c5 ); stbir__simdfX_madd( o3, o3, r3, c5 ); ) + stbIF6( stbir__simdfX_load( r0, input6 ); stbir__simdfX_load( r1, input6+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input6+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input6+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c6 ); stbir__simdfX_madd( o1, o1, r1, c6 ); stbir__simdfX_madd( o2, o2, r2, c6 ); stbir__simdfX_madd( o3, o3, r3, c6 ); ) + stbIF7( stbir__simdfX_load( r0, input7 ); stbir__simdfX_load( r1, input7+stbir__simdfX_float_count ); stbir__simdfX_load( r2, input7+(2*stbir__simdfX_float_count) ); stbir__simdfX_load( r3, input7+(3*stbir__simdfX_float_count) ); + stbir__simdfX_madd( o0, o0, r0, c7 ); stbir__simdfX_madd( o1, o1, r1, c7 ); stbir__simdfX_madd( o2, o2, r2, c7 ); stbir__simdfX_madd( o3, o3, r3, c7 ); ) + + stbir__simdfX_store( output, o0 ); stbir__simdfX_store( output+stbir__simdfX_float_count, o1 ); stbir__simdfX_store( output+(2*stbir__simdfX_float_count), o2 ); stbir__simdfX_store( output+(3*stbir__simdfX_float_count), o3 ); + output += (4*stbir__simdfX_float_count); + stbIF0( input0 += (4*stbir__simdfX_float_count); ) stbIF1( input1 += (4*stbir__simdfX_float_count); ) stbIF2( input2 += (4*stbir__simdfX_float_count); ) stbIF3( input3 += (4*stbir__simdfX_float_count); ) stbIF4( input4 += (4*stbir__simdfX_float_count); ) stbIF5( input5 += (4*stbir__simdfX_float_count); ) stbIF6( input6 += (4*stbir__simdfX_float_count); ) stbIF7( input7 += (4*stbir__simdfX_float_count); ) + } + + STBIR_SIMD_NO_UNROLL_LOOP_START + while ( ( (char*)input0_end - (char*) input0 ) >= 16 ) + { + stbir__simdf o0, r0; + STBIR_SIMD_NO_UNROLL(output); + + #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE + stbIF0( stbir__simdf_load( o0, output ); stbir__simdf_load( r0, input0 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c0 ) ); ) + #else + stbIF0( stbir__simdf_load( r0, input0 ); stbir__simdf_mult( o0, r0, stbir__if_simdf8_cast_to_simdf4( c0 ) ); ) + #endif + stbIF1( stbir__simdf_load( r0, input1 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c1 ) ); ) + stbIF2( stbir__simdf_load( r0, input2 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c2 ) ); ) + stbIF3( stbir__simdf_load( r0, input3 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c3 ) ); ) + stbIF4( stbir__simdf_load( r0, input4 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c4 ) ); ) + stbIF5( stbir__simdf_load( r0, input5 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c5 ) ); ) + stbIF6( stbir__simdf_load( r0, input6 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c6 ) ); ) + stbIF7( stbir__simdf_load( r0, input7 ); stbir__simdf_madd( o0, o0, r0, stbir__if_simdf8_cast_to_simdf4( c7 ) ); ) + + stbir__simdf_store( output, o0 ); + output += 4; + stbIF0( input0 += 4; ) stbIF1( input1 += 4; ) stbIF2( input2 += 4; ) stbIF3( input3 += 4; ) stbIF4( input4 += 4; ) stbIF5( input5 += 4; ) stbIF6( input6 += 4; ) stbIF7( input7 += 4; ) + } + } + #else + STBIR_NO_UNROLL_LOOP_START + while ( ( (char*)input0_end - (char*) input0 ) >= 16 ) + { + float o0, o1, o2, o3; + STBIR_NO_UNROLL(output); + #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE + stbIF0( o0 = output[0] + input0[0] * c0s; o1 = output[1] + input0[1] * c0s; o2 = output[2] + input0[2] * c0s; o3 = output[3] + input0[3] * c0s; ) + #else + stbIF0( o0 = input0[0] * c0s; o1 = input0[1] * c0s; o2 = input0[2] * c0s; o3 = input0[3] * c0s; ) + #endif + stbIF1( o0 += input1[0] * c1s; o1 += input1[1] * c1s; o2 += input1[2] * c1s; o3 += input1[3] * c1s; ) + stbIF2( o0 += input2[0] * c2s; o1 += input2[1] * c2s; o2 += input2[2] * c2s; o3 += input2[3] * c2s; ) + stbIF3( o0 += input3[0] * c3s; o1 += input3[1] * c3s; o2 += input3[2] * c3s; o3 += input3[3] * c3s; ) + stbIF4( o0 += input4[0] * c4s; o1 += input4[1] * c4s; o2 += input4[2] * c4s; o3 += input4[3] * c4s; ) + stbIF5( o0 += input5[0] * c5s; o1 += input5[1] * c5s; o2 += input5[2] * c5s; o3 += input5[3] * c5s; ) + stbIF6( o0 += input6[0] * c6s; o1 += input6[1] * c6s; o2 += input6[2] * c6s; o3 += input6[3] * c6s; ) + stbIF7( o0 += input7[0] * c7s; o1 += input7[1] * c7s; o2 += input7[2] * c7s; o3 += input7[3] * c7s; ) + output[0] = o0; output[1] = o1; output[2] = o2; output[3] = o3; + output += 4; + stbIF0( input0 += 4; ) stbIF1( input1 += 4; ) stbIF2( input2 += 4; ) stbIF3( input3 += 4; ) stbIF4( input4 += 4; ) stbIF5( input5 += 4; ) stbIF6( input6 += 4; ) stbIF7( input7 += 4; ) + } + #endif + STBIR_NO_UNROLL_LOOP_START + while ( input0 < input0_end ) + { + float o0; + STBIR_NO_UNROLL(output); + #ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE + stbIF0( o0 = output[0] + input0[0] * c0s; ) + #else + stbIF0( o0 = input0[0] * c0s; ) + #endif + stbIF1( o0 += input1[0] * c1s; ) + stbIF2( o0 += input2[0] * c2s; ) + stbIF3( o0 += input3[0] * c3s; ) + stbIF4( o0 += input4[0] * c4s; ) + stbIF5( o0 += input5[0] * c5s; ) + stbIF6( o0 += input6[0] * c6s; ) + stbIF7( o0 += input7[0] * c7s; ) + output[0] = o0; + ++output; + stbIF0( ++input0; ) stbIF1( ++input1; ) stbIF2( ++input2; ) stbIF3( ++input3; ) stbIF4( ++input4; ) stbIF5( ++input5; ) stbIF6( ++input6; ) stbIF7( ++input7; ) + } +} + +#undef stbIF0 +#undef stbIF1 +#undef stbIF2 +#undef stbIF3 +#undef stbIF4 +#undef stbIF5 +#undef stbIF6 +#undef stbIF7 +#undef STB_IMAGE_RESIZE_DO_VERTICALS +#undef STBIR__vertical_channels +#undef STB_IMAGE_RESIZE_DO_HORIZONTALS +#undef STBIR_strs_join24 +#undef STBIR_strs_join14 +#undef STBIR_chans +#ifdef STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#undef STB_IMAGE_RESIZE_VERTICAL_CONTINUE +#endif + +#else // !STB_IMAGE_RESIZE_DO_VERTICALS + +#define STBIR_chans( start, end ) STBIR_strs_join1(start,STBIR__horizontal_channels,end) + +#ifndef stbir__2_coeff_only +#define stbir__2_coeff_only() \ + stbir__1_coeff_only(); \ + stbir__1_coeff_remnant(1); +#endif + +#ifndef stbir__2_coeff_remnant +#define stbir__2_coeff_remnant( ofs ) \ + stbir__1_coeff_remnant(ofs); \ + stbir__1_coeff_remnant((ofs)+1); +#endif + +#ifndef stbir__3_coeff_only +#define stbir__3_coeff_only() \ + stbir__2_coeff_only(); \ + stbir__1_coeff_remnant(2); +#endif + +#ifndef stbir__3_coeff_remnant +#define stbir__3_coeff_remnant( ofs ) \ + stbir__2_coeff_remnant(ofs); \ + stbir__1_coeff_remnant((ofs)+2); +#endif + +#ifndef stbir__3_coeff_setup +#define stbir__3_coeff_setup() +#endif + +#ifndef stbir__4_coeff_start +#define stbir__4_coeff_start() \ + stbir__2_coeff_only(); \ + stbir__2_coeff_remnant(2); +#endif + +#ifndef stbir__4_coeff_continue_from_4 +#define stbir__4_coeff_continue_from_4( ofs ) \ + stbir__2_coeff_remnant(ofs); \ + stbir__2_coeff_remnant((ofs)+2); +#endif + +#ifndef stbir__store_output_tiny +#define stbir__store_output_tiny stbir__store_output +#endif + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_1_coeff)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__1_coeff_only(); + stbir__store_output_tiny(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_2_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__2_coeff_only(); + stbir__store_output_tiny(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_3_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__3_coeff_only(); + stbir__store_output_tiny(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_4_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__4_coeff_start(); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_5_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__4_coeff_start(); + stbir__1_coeff_remnant(4); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_6_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__4_coeff_start(); + stbir__2_coeff_remnant(4); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_7_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + stbir__3_coeff_setup(); + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + + stbir__4_coeff_start(); + stbir__3_coeff_remnant(4); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_8_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__4_coeff_start(); + stbir__4_coeff_continue_from_4(4); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_9_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__4_coeff_start(); + stbir__4_coeff_continue_from_4(4); + stbir__1_coeff_remnant(8); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_10_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__4_coeff_start(); + stbir__4_coeff_continue_from_4(4); + stbir__2_coeff_remnant(8); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_11_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + stbir__3_coeff_setup(); + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__4_coeff_start(); + stbir__4_coeff_continue_from_4(4); + stbir__3_coeff_remnant(8); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_12_coeffs)( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + float const * hc = horizontal_coefficients; + stbir__4_coeff_start(); + stbir__4_coeff_continue_from_4(4); + stbir__4_coeff_continue_from_4(8); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_n_coeffs_mod0 )( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + int n = ( ( horizontal_contributors->n1 - horizontal_contributors->n0 + 1 ) - 4 + 3 ) >> 2; + float const * hc = horizontal_coefficients; + + stbir__4_coeff_start(); + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + hc += 4; + decode += STBIR__horizontal_channels * 4; + stbir__4_coeff_continue_from_4( 0 ); + --n; + } while ( n > 0 ); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_n_coeffs_mod1 )( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + int n = ( ( horizontal_contributors->n1 - horizontal_contributors->n0 + 1 ) - 5 + 3 ) >> 2; + float const * hc = horizontal_coefficients; + + stbir__4_coeff_start(); + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + hc += 4; + decode += STBIR__horizontal_channels * 4; + stbir__4_coeff_continue_from_4( 0 ); + --n; + } while ( n > 0 ); + stbir__1_coeff_remnant( 4 ); + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_n_coeffs_mod2 )( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + int n = ( ( horizontal_contributors->n1 - horizontal_contributors->n0 + 1 ) - 6 + 3 ) >> 2; + float const * hc = horizontal_coefficients; + + stbir__4_coeff_start(); + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + hc += 4; + decode += STBIR__horizontal_channels * 4; + stbir__4_coeff_continue_from_4( 0 ); + --n; + } while ( n > 0 ); + stbir__2_coeff_remnant( 4 ); + + stbir__store_output(); + } while ( output < output_end ); +} + +static void STBIR_chans( stbir__horizontal_gather_,_channels_with_n_coeffs_mod3 )( float * output_buffer, unsigned int output_sub_size, float const * decode_buffer, stbir__contributors const * horizontal_contributors, float const * horizontal_coefficients, int coefficient_width ) +{ + float const * output_end = output_buffer + output_sub_size * STBIR__horizontal_channels; + float STBIR_SIMD_STREAMOUT_PTR( * ) output = output_buffer; + stbir__3_coeff_setup(); + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + float const * decode = decode_buffer + horizontal_contributors->n0 * STBIR__horizontal_channels; + int n = ( ( horizontal_contributors->n1 - horizontal_contributors->n0 + 1 ) - 7 + 3 ) >> 2; + float const * hc = horizontal_coefficients; + + stbir__4_coeff_start(); + STBIR_SIMD_NO_UNROLL_LOOP_START + do { + hc += 4; + decode += STBIR__horizontal_channels * 4; + stbir__4_coeff_continue_from_4( 0 ); + --n; + } while ( n > 0 ); + stbir__3_coeff_remnant( 4 ); + + stbir__store_output(); + } while ( output < output_end ); +} + +static stbir__horizontal_gather_channels_func * STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_funcs)[4]= +{ + STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_mod0), + STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_mod1), + STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_mod2), + STBIR_chans(stbir__horizontal_gather_,_channels_with_n_coeffs_mod3), +}; + +static stbir__horizontal_gather_channels_func * STBIR_chans(stbir__horizontal_gather_,_channels_funcs)[12]= +{ + STBIR_chans(stbir__horizontal_gather_,_channels_with_1_coeff), + STBIR_chans(stbir__horizontal_gather_,_channels_with_2_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_3_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_4_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_5_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_6_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_7_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_8_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_9_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_10_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_11_coeffs), + STBIR_chans(stbir__horizontal_gather_,_channels_with_12_coeffs), +}; + +#undef STBIR__horizontal_channels +#undef STB_IMAGE_RESIZE_DO_HORIZONTALS +#undef stbir__1_coeff_only +#undef stbir__1_coeff_remnant +#undef stbir__2_coeff_only +#undef stbir__2_coeff_remnant +#undef stbir__3_coeff_only +#undef stbir__3_coeff_remnant +#undef stbir__3_coeff_setup +#undef stbir__4_coeff_start +#undef stbir__4_coeff_continue_from_4 +#undef stbir__store_output +#undef stbir__store_output_tiny +#undef STBIR_chans + +#endif // HORIZONALS + +#undef STBIR_strs_join2 +#undef STBIR_strs_join1 + +#endif // STB_IMAGE_RESIZE_DO_HORIZONTALS/VERTICALS/CODERS + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/src/cdeps/stb_image_write.h b/src/cdeps/stb_image_write.h new file mode 100644 index 0000000..e4b32ed --- /dev/null +++ b/src/cdeps/stb_image_write.h @@ -0,0 +1,1724 @@ +/* stb_image_write - v1.16 - public domain - http://nothings.org/stb + writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015 + no warranty implied; use at your own risk + + Before #including, + + #define STB_IMAGE_WRITE_IMPLEMENTATION + + in the file that you want to have the implementation. + + Will probably not work correctly with strict-aliasing optimizations. + +ABOUT: + + This header file is a library for writing images to C stdio or a callback. + + The PNG output is not optimal; it is 20-50% larger than the file + written by a decent optimizing implementation; though providing a custom + zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that. + This library is designed for source code compactness and simplicity, + not optimal image file size or run-time performance. + +BUILDING: + + You can #define STBIW_ASSERT(x) before the #include to avoid using assert.h. + You can #define STBIW_MALLOC(), STBIW_REALLOC(), and STBIW_FREE() to replace + malloc,realloc,free. + You can #define STBIW_MEMMOVE() to replace memmove() + You can #define STBIW_ZLIB_COMPRESS to use a custom zlib-style compress function + for PNG compression (instead of the builtin one), it must have the following signature: + unsigned char * my_compress(unsigned char *data, int data_len, int *out_len, int quality); + The returned data will be freed with STBIW_FREE() (free() by default), + so it must be heap allocated with STBIW_MALLOC() (malloc() by default), + +UNICODE: + + If compiling for Windows and you wish to use Unicode filenames, compile + with + #define STBIW_WINDOWS_UTF8 + and pass utf8-encoded filenames. Call stbiw_convert_wchar_to_utf8 to convert + Windows wchar_t filenames to utf8. + +USAGE: + + There are five functions, one for each image file format: + + int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality); + int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); + + void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically + + There are also five equivalent functions that use an arbitrary write function. You are + expected to open/close your file-equivalent before and after calling these: + + int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); + int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + + where the callback is: + void stbi_write_func(void *context, void *data, int size); + + You can configure it with these global variables: + int stbi_write_tga_with_rle; // defaults to true; set to 0 to disable RLE + int stbi_write_png_compression_level; // defaults to 8; set to higher for more compression + int stbi_write_force_png_filter; // defaults to -1; set to 0..5 to force a filter mode + + + You can define STBI_WRITE_NO_STDIO to disable the file variant of these + functions, so the library will not use stdio.h at all. However, this will + also disable HDR writing, because it requires stdio for formatted output. + + Each function returns 0 on failure and non-0 on success. + + The functions create an image file defined by the parameters. The image + is a rectangle of pixels stored from left-to-right, top-to-bottom. + Each pixel contains 'comp' channels of data stored interleaved with 8-bits + per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is + monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall. + The *data pointer points to the first byte of the top-left-most pixel. + For PNG, "stride_in_bytes" is the distance in bytes from the first byte of + a row of pixels to the first byte of the next row of pixels. + + PNG creates output files with the same number of components as the input. + The BMP format expands Y to RGB in the file format and does not + output alpha. + + PNG supports writing rectangles of data even when the bytes storing rows of + data are not consecutive in memory (e.g. sub-rectangles of a larger image), + by supplying the stride between the beginning of adjacent rows. The other + formats do not. (Thus you cannot write a native-format BMP through the BMP + writer, both because it is in BGR order and because it may have padding + at the end of the line.) + + PNG allows you to set the deflate compression level by setting the global + variable 'stbi_write_png_compression_level' (it defaults to 8). + + HDR expects linear float data. Since the format is always 32-bit rgb(e) + data, alpha (if provided) is discarded, and for monochrome data it is + replicated across all three channels. + + TGA supports RLE or non-RLE compressed data. To use non-RLE-compressed + data, set the global variable 'stbi_write_tga_with_rle' to 0. + + JPEG does ignore alpha channels in input data; quality is between 1 and 100. + Higher quality looks better but results in a bigger image. + JPEG baseline (no JPEG progressive). + +CREDITS: + + + Sean Barrett - PNG/BMP/TGA + Baldur Karlsson - HDR + Jean-Sebastien Guay - TGA monochrome + Tim Kelsey - misc enhancements + Alan Hickman - TGA RLE + Emmanuel Julien - initial file IO callback implementation + Jon Olick - original jo_jpeg.cpp code + Daniel Gibson - integrate JPEG, allow external zlib + Aarni Koskela - allow choosing PNG filter + + bugfixes: + github:Chribba + Guillaume Chereau + github:jry2 + github:romigrou + Sergio Gonzalez + Jonas Karlsson + Filip Wasil + Thatcher Ulrich + github:poppolopoppo + Patrick Boettcher + github:xeekworx + Cap Petschulat + Simon Rodriguez + Ivan Tikhonov + github:ignotion + Adam Schackart + Andrew Kensler + +LICENSE + + See end of file for license information. + +*/ + +#ifndef INCLUDE_STB_IMAGE_WRITE_H +#define INCLUDE_STB_IMAGE_WRITE_H + +#include <stdlib.h> + +// if STB_IMAGE_WRITE_STATIC causes problems, try defining STBIWDEF to 'inline' or 'static inline' +#ifndef STBIWDEF +#ifdef STB_IMAGE_WRITE_STATIC +#define STBIWDEF static +#else +#ifdef __cplusplus +#define STBIWDEF extern "C" +#else +#define STBIWDEF extern +#endif +#endif +#endif + +#ifndef STB_IMAGE_WRITE_STATIC // C++ forbids static forward declarations +STBIWDEF int stbi_write_tga_with_rle; +STBIWDEF int stbi_write_png_compression_level; +STBIWDEF int stbi_write_force_png_filter; +#endif + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality); + +#ifdef STBIW_WINDOWS_UTF8 +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif +#endif + +typedef void stbi_write_func(void *context, void *data, int size); + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + +STBIWDEF void stbi_flip_vertically_on_write(int flip_boolean); + +#endif//INCLUDE_STB_IMAGE_WRITE_H + +#ifdef STB_IMAGE_WRITE_IMPLEMENTATION + +#ifdef _WIN32 + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif + #ifndef _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_NONSTDC_NO_DEPRECATE + #endif +#endif + +#ifndef STBI_WRITE_NO_STDIO +#include <stdio.h> +#endif // STBI_WRITE_NO_STDIO + +#include <stdarg.h> +#include <stdlib.h> +#include <string.h> +#include <math.h> + +#if defined(STBIW_MALLOC) && defined(STBIW_FREE) && (defined(STBIW_REALLOC) || defined(STBIW_REALLOC_SIZED)) +// ok +#elif !defined(STBIW_MALLOC) && !defined(STBIW_FREE) && !defined(STBIW_REALLOC) && !defined(STBIW_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBIW_MALLOC, STBIW_FREE, and STBIW_REALLOC (or STBIW_REALLOC_SIZED)." +#endif + +#ifndef STBIW_MALLOC +#define STBIW_MALLOC(sz) malloc(sz) +#define STBIW_REALLOC(p,newsz) realloc(p,newsz) +#define STBIW_FREE(p) free(p) +#endif + +#ifndef STBIW_REALLOC_SIZED +#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz) +#endif + + +#ifndef STBIW_MEMMOVE +#define STBIW_MEMMOVE(a,b,sz) memmove(a,b,sz) +#endif + + +#ifndef STBIW_ASSERT +#include <assert.h> +#define STBIW_ASSERT(x) assert(x) +#endif + +#define STBIW_UCHAR(x) (unsigned char) ((x) & 0xff) + +#ifdef STB_IMAGE_WRITE_STATIC +static int stbi_write_png_compression_level = 8; +static int stbi_write_tga_with_rle = 1; +static int stbi_write_force_png_filter = -1; +#else +int stbi_write_png_compression_level = 8; +int stbi_write_tga_with_rle = 1; +int stbi_write_force_png_filter = -1; +#endif + +static int stbi__flip_vertically_on_write = 0; + +STBIWDEF void stbi_flip_vertically_on_write(int flag) +{ + stbi__flip_vertically_on_write = flag; +} + +typedef struct +{ + stbi_write_func *func; + void *context; + unsigned char buffer[64]; + int buf_used; +} stbi__write_context; + +// initialize a callback-based context +static void stbi__start_write_callbacks(stbi__write_context *s, stbi_write_func *c, void *context) +{ + s->func = c; + s->context = context; +} + +#ifndef STBI_WRITE_NO_STDIO + +static void stbi__stdio_write(void *context, void *data, int size) +{ + fwrite(data,1,size,(FILE*) context); +} + +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) +#ifdef __cplusplus +#define STBIW_EXTERN extern "C" +#else +#define STBIW_EXTERN extern +#endif +STBIW_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBIW_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); + +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbiw__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) + return 0; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + +static int stbi__start_write_file(stbi__write_context *s, const char *filename) +{ + FILE *f = stbiw__fopen(filename, "wb"); + stbi__start_write_callbacks(s, stbi__stdio_write, (void *) f); + return f != NULL; +} + +static void stbi__end_write_file(stbi__write_context *s) +{ + fclose((FILE *)s->context); +} + +#endif // !STBI_WRITE_NO_STDIO + +typedef unsigned int stbiw_uint32; +typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1]; + +static void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v) +{ + while (*fmt) { + switch (*fmt++) { + case ' ': break; + case '1': { unsigned char x = STBIW_UCHAR(va_arg(v, int)); + s->func(s->context,&x,1); + break; } + case '2': { int x = va_arg(v,int); + unsigned char b[2]; + b[0] = STBIW_UCHAR(x); + b[1] = STBIW_UCHAR(x>>8); + s->func(s->context,b,2); + break; } + case '4': { stbiw_uint32 x = va_arg(v,int); + unsigned char b[4]; + b[0]=STBIW_UCHAR(x); + b[1]=STBIW_UCHAR(x>>8); + b[2]=STBIW_UCHAR(x>>16); + b[3]=STBIW_UCHAR(x>>24); + s->func(s->context,b,4); + break; } + default: + STBIW_ASSERT(0); + return; + } + } +} + +static void stbiw__writef(stbi__write_context *s, const char *fmt, ...) +{ + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); +} + +static void stbiw__write_flush(stbi__write_context *s) +{ + if (s->buf_used) { + s->func(s->context, &s->buffer, s->buf_used); + s->buf_used = 0; + } +} + +static void stbiw__putc(stbi__write_context *s, unsigned char c) +{ + s->func(s->context, &c, 1); +} + +static void stbiw__write1(stbi__write_context *s, unsigned char a) +{ + if ((size_t)s->buf_used + 1 > sizeof(s->buffer)) + stbiw__write_flush(s); + s->buffer[s->buf_used++] = a; +} + +static void stbiw__write3(stbi__write_context *s, unsigned char a, unsigned char b, unsigned char c) +{ + int n; + if ((size_t)s->buf_used + 3 > sizeof(s->buffer)) + stbiw__write_flush(s); + n = s->buf_used; + s->buf_used = n+3; + s->buffer[n+0] = a; + s->buffer[n+1] = b; + s->buffer[n+2] = c; +} + +static void stbiw__write_pixel(stbi__write_context *s, int rgb_dir, int comp, int write_alpha, int expand_mono, unsigned char *d) +{ + unsigned char bg[3] = { 255, 0, 255}, px[3]; + int k; + + if (write_alpha < 0) + stbiw__write1(s, d[comp - 1]); + + switch (comp) { + case 2: // 2 pixels = mono + alpha, alpha is written separately, so same as 1-channel case + case 1: + if (expand_mono) + stbiw__write3(s, d[0], d[0], d[0]); // monochrome bmp + else + stbiw__write1(s, d[0]); // monochrome TGA + break; + case 4: + if (!write_alpha) { + // composite against pink background + for (k = 0; k < 3; ++k) + px[k] = bg[k] + ((d[k] - bg[k]) * d[3]) / 255; + stbiw__write3(s, px[1 - rgb_dir], px[1], px[1 + rgb_dir]); + break; + } + /* FALLTHROUGH */ + case 3: + stbiw__write3(s, d[1 - rgb_dir], d[1], d[1 + rgb_dir]); + break; + } + if (write_alpha > 0) + stbiw__write1(s, d[comp - 1]); +} + +static void stbiw__write_pixels(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad, int expand_mono) +{ + stbiw_uint32 zero = 0; + int i,j, j_end; + + if (y <= 0) + return; + + if (stbi__flip_vertically_on_write) + vdir *= -1; + + if (vdir < 0) { + j_end = -1; j = y-1; + } else { + j_end = y; j = 0; + } + + for (; j != j_end; j += vdir) { + for (i=0; i < x; ++i) { + unsigned char *d = (unsigned char *) data + (j*x+i)*comp; + stbiw__write_pixel(s, rgb_dir, comp, write_alpha, expand_mono, d); + } + stbiw__write_flush(s); + s->func(s->context, &zero, scanline_pad); + } +} + +static int stbiw__outfile(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, int expand_mono, void *data, int alpha, int pad, const char *fmt, ...) +{ + if (y < 0 || x < 0) { + return 0; + } else { + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); + stbiw__write_pixels(s,rgb_dir,vdir,x,y,comp,data,alpha,pad, expand_mono); + return 1; + } +} + +static int stbi_write_bmp_core(stbi__write_context *s, int x, int y, int comp, const void *data) +{ + if (comp != 4) { + // write RGB bitmap + int pad = (-x*3) & 3; + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *) data,0,pad, + "11 4 22 4" "4 44 22 444444", + 'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header + 40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header + } else { + // RGBA bitmaps need a v4 header + // use BI_BITFIELDS mode with 32bpp and alpha mask + // (straight BI_RGB with alpha mask doesn't work in most readers) + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *)data,1,0, + "11 4 22 4" "4 44 22 444444 4444 4 444 444 444 444", + 'B', 'M', 14+108+x*y*4, 0, 0, 14+108, // file header + 108, x,y, 1,32, 3,0,0,0,0,0, 0xff0000,0xff00,0xff,0xff000000u, 0, 0,0,0, 0,0,0, 0,0,0, 0,0,0); // bitmap V4 header + } +} + +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_bmp_core(&s, x, y, comp, data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_bmp_core(&s, x, y, comp, data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif //!STBI_WRITE_NO_STDIO + +static int stbi_write_tga_core(stbi__write_context *s, int x, int y, int comp, void *data) +{ + int has_alpha = (comp == 2 || comp == 4); + int colorbytes = has_alpha ? comp-1 : comp; + int format = colorbytes < 2 ? 3 : 2; // 3 color channels (RGB/RGBA) = 2, 1 color channel (Y/YA) = 3 + + if (y < 0 || x < 0) + return 0; + + if (!stbi_write_tga_with_rle) { + return stbiw__outfile(s, -1, -1, x, y, comp, 0, (void *) data, has_alpha, 0, + "111 221 2222 11", 0, 0, format, 0, 0, 0, 0, 0, x, y, (colorbytes + has_alpha) * 8, has_alpha * 8); + } else { + int i,j,k; + int jend, jdir; + + stbiw__writef(s, "111 221 2222 11", 0,0,format+8, 0,0,0, 0,0,x,y, (colorbytes + has_alpha) * 8, has_alpha * 8); + + if (stbi__flip_vertically_on_write) { + j = 0; + jend = y; + jdir = 1; + } else { + j = y-1; + jend = -1; + jdir = -1; + } + for (; j != jend; j += jdir) { + unsigned char *row = (unsigned char *) data + j * x * comp; + int len; + + for (i = 0; i < x; i += len) { + unsigned char *begin = row + i * comp; + int diff = 1; + len = 1; + + if (i < x - 1) { + ++len; + diff = memcmp(begin, row + (i + 1) * comp, comp); + if (diff) { + const unsigned char *prev = begin; + for (k = i + 2; k < x && len < 128; ++k) { + if (memcmp(prev, row + k * comp, comp)) { + prev += comp; + ++len; + } else { + --len; + break; + } + } + } else { + for (k = i + 2; k < x && len < 128; ++k) { + if (!memcmp(begin, row + k * comp, comp)) { + ++len; + } else { + break; + } + } + } + } + + if (diff) { + unsigned char header = STBIW_UCHAR(len - 1); + stbiw__write1(s, header); + for (k = 0; k < len; ++k) { + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin + k * comp); + } + } else { + unsigned char header = STBIW_UCHAR(len - 129); + stbiw__write1(s, header); + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin); + } + } + } + stbiw__write_flush(s); + } + return 1; +} + +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_tga_core(&s, x, y, comp, (void *) data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_tga_core(&s, x, y, comp, (void *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR writer +// by Baldur Karlsson + +#define stbiw__max(a, b) ((a) > (b) ? (a) : (b)) + +#ifndef STBI_WRITE_NO_STDIO + +static void stbiw__linear_to_rgbe(unsigned char *rgbe, float *linear) +{ + int exponent; + float maxcomp = stbiw__max(linear[0], stbiw__max(linear[1], linear[2])); + + if (maxcomp < 1e-32f) { + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + } else { + float normalize = (float) frexp(maxcomp, &exponent) * 256.0f/maxcomp; + + rgbe[0] = (unsigned char)(linear[0] * normalize); + rgbe[1] = (unsigned char)(linear[1] * normalize); + rgbe[2] = (unsigned char)(linear[2] * normalize); + rgbe[3] = (unsigned char)(exponent + 128); + } +} + +static void stbiw__write_run_data(stbi__write_context *s, int length, unsigned char databyte) +{ + unsigned char lengthbyte = STBIW_UCHAR(length+128); + STBIW_ASSERT(length+128 <= 255); + s->func(s->context, &lengthbyte, 1); + s->func(s->context, &databyte, 1); +} + +static void stbiw__write_dump_data(stbi__write_context *s, int length, unsigned char *data) +{ + unsigned char lengthbyte = STBIW_UCHAR(length); + STBIW_ASSERT(length <= 128); // inconsistent with spec but consistent with official code + s->func(s->context, &lengthbyte, 1); + s->func(s->context, data, length); +} + +static void stbiw__write_hdr_scanline(stbi__write_context *s, int width, int ncomp, unsigned char *scratch, float *scanline) +{ + unsigned char scanlineheader[4] = { 2, 2, 0, 0 }; + unsigned char rgbe[4]; + float linear[3]; + int x; + + scanlineheader[2] = (width&0xff00)>>8; + scanlineheader[3] = (width&0x00ff); + + /* skip RLE for images too small or large */ + if (width < 8 || width >= 32768) { + for (x=0; x < width; x++) { + switch (ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + s->func(s->context, rgbe, 4); + } + } else { + int c,r; + /* encode into scratch buffer */ + for (x=0; x < width; x++) { + switch(ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + scratch[x + width*0] = rgbe[0]; + scratch[x + width*1] = rgbe[1]; + scratch[x + width*2] = rgbe[2]; + scratch[x + width*3] = rgbe[3]; + } + + s->func(s->context, scanlineheader, 4); + + /* RLE each component separately */ + for (c=0; c < 4; c++) { + unsigned char *comp = &scratch[width*c]; + + x = 0; + while (x < width) { + // find first run + r = x; + while (r+2 < width) { + if (comp[r] == comp[r+1] && comp[r] == comp[r+2]) + break; + ++r; + } + if (r+2 >= width) + r = width; + // dump up to first run + while (x < r) { + int len = r-x; + if (len > 128) len = 128; + stbiw__write_dump_data(s, len, &comp[x]); + x += len; + } + // if there's a run, output it + if (r+2 < width) { // same test as what we break out of in search loop, so only true if we break'd + // find next byte after run + while (r < width && comp[r] == comp[x]) + ++r; + // output run up to r + while (x < r) { + int len = r-x; + if (len > 127) len = 127; + stbiw__write_run_data(s, len, comp[x]); + x += len; + } + } + } + } + } +} + +static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, float *data) +{ + if (y <= 0 || x <= 0 || data == NULL) + return 0; + else { + // Each component is stored separately. Allocate scratch space for full output scanline. + unsigned char *scratch = (unsigned char *) STBIW_MALLOC(x*4); + int i, len; + char buffer[128]; + char header[] = "#?RADIANCE\n# Written by stb_image_write.h\nFORMAT=32-bit_rle_rgbe\n"; + s->func(s->context, header, sizeof(header)-1); + +#ifdef __STDC_LIB_EXT1__ + len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#else + len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#endif + s->func(s->context, buffer, len); + + for(i=0; i < y; i++) + stbiw__write_hdr_scanline(s, x, comp, scratch, data + comp*x*(stbi__flip_vertically_on_write ? y-1-i : i)); + STBIW_FREE(scratch); + return 1; + } +} + +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_hdr_core(&s, x, y, comp, (float *) data); +} + +STBIWDEF int stbi_write_hdr(char const *filename, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_hdr_core(&s, x, y, comp, (float *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif // STBI_WRITE_NO_STDIO + + +////////////////////////////////////////////////////////////////////////////// +// +// PNG writer +// + +#ifndef STBIW_ZLIB_COMPRESS +// stretchy buffer; stbiw__sbpush() == vector<>::push_back() -- stbiw__sbcount() == vector<>::size() +#define stbiw__sbraw(a) ((int *) (void *) (a) - 2) +#define stbiw__sbm(a) stbiw__sbraw(a)[0] +#define stbiw__sbn(a) stbiw__sbraw(a)[1] + +#define stbiw__sbneedgrow(a,n) ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a)) +#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0) +#define stbiw__sbgrow(a,n) stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a))) + +#define stbiw__sbpush(a, v) (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v)) +#define stbiw__sbcount(a) ((a) ? stbiw__sbn(a) : 0) +#define stbiw__sbfree(a) ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0) + +static void *stbiw__sbgrowf(void **arr, int increment, int itemsize) +{ + int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1; + void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2); + STBIW_ASSERT(p); + if (p) { + if (!*arr) ((int *) p)[1] = 0; + *arr = (void *) ((int *) p + 2); + stbiw__sbm(*arr) = m; + } + return *arr; +} + +static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) +{ + while (*bitcount >= 8) { + stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer)); + *bitbuffer >>= 8; + *bitcount -= 8; + } + return data; +} + +static int stbiw__zlib_bitrev(int code, int codebits) +{ + int res=0; + while (codebits--) { + res = (res << 1) | (code & 1); + code >>= 1; + } + return res; +} + +static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit) +{ + int i; + for (i=0; i < limit && i < 258; ++i) + if (a[i] != b[i]) break; + return i; +} + +static unsigned int stbiw__zhash(unsigned char *data) +{ + stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); + hash ^= hash << 3; + hash += hash >> 5; + hash ^= hash << 4; + hash += hash >> 17; + hash ^= hash << 25; + hash += hash >> 6; + return hash; +} + +#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount)) +#define stbiw__zlib_add(code,codebits) \ + (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush()) +#define stbiw__zlib_huffa(b,c) stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c) +// default huffman tables +#define stbiw__zlib_huff1(n) stbiw__zlib_huffa(0x30 + (n), 8) +#define stbiw__zlib_huff2(n) stbiw__zlib_huffa(0x190 + (n)-144, 9) +#define stbiw__zlib_huff3(n) stbiw__zlib_huffa(0 + (n)-256,7) +#define stbiw__zlib_huff4(n) stbiw__zlib_huffa(0xc0 + (n)-280,8) +#define stbiw__zlib_huff(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n)) +#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n)) + +#define stbiw__ZHASH 16384 + +#endif // STBIW_ZLIB_COMPRESS + +STBIWDEF unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) +{ +#ifdef STBIW_ZLIB_COMPRESS + // user provided a zlib compress implementation, use that + return STBIW_ZLIB_COMPRESS(data, data_len, out_len, quality); +#else // use builtin + static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 }; + static unsigned char lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; + static unsigned short distc[] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 }; + static unsigned char disteb[] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; + unsigned int bitbuf=0; + int i,j, bitcount=0; + unsigned char *out = NULL; + unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(unsigned char**)); + if (hash_table == NULL) + return NULL; + if (quality < 5) quality = 5; + + stbiw__sbpush(out, 0x78); // DEFLATE 32K window + stbiw__sbpush(out, 0x5e); // FLEVEL = 1 + stbiw__zlib_add(1,1); // BFINAL = 1 + stbiw__zlib_add(1,2); // BTYPE = 1 -- fixed huffman + + for (i=0; i < stbiw__ZHASH; ++i) + hash_table[i] = NULL; + + i=0; + while (i < data_len-3) { + // hash next 3 bytes of data to be compressed + int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3; + unsigned char *bestloc = 0; + unsigned char **hlist = hash_table[h]; + int n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32768) { // if entry lies within window + int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i); + if (d >= best) { best=d; bestloc=hlist[j]; } + } + } + // when hash table entry is too long, delete half the entries + if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) { + STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality); + stbiw__sbn(hash_table[h]) = quality; + } + stbiw__sbpush(hash_table[h],data+i); + + if (bestloc) { + // "lazy matching" - check match at *next* byte, and if it's better, do cur byte as literal + h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1); + hlist = hash_table[h]; + n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32767) { + int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1); + if (e > best) { // if next match is better, bail on current match + bestloc = NULL; + break; + } + } + } + } + + if (bestloc) { + int d = (int) (data+i - bestloc); // distance back + STBIW_ASSERT(d <= 32767 && best <= 258); + for (j=0; best > lengthc[j+1]-1; ++j); + stbiw__zlib_huff(j+257); + if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]); + for (j=0; d > distc[j+1]-1; ++j); + stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5); + if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]); + i += best; + } else { + stbiw__zlib_huffb(data[i]); + ++i; + } + } + // write out final bytes + for (;i < data_len; ++i) + stbiw__zlib_huffb(data[i]); + stbiw__zlib_huff(256); // end of block + // pad with 0 bits to byte boundary + while (bitcount) + stbiw__zlib_add(0,1); + + for (i=0; i < stbiw__ZHASH; ++i) + (void) stbiw__sbfree(hash_table[i]); + STBIW_FREE(hash_table); + + // store uncompressed instead if compression was worse + if (stbiw__sbn(out) > data_len + 2 + ((data_len+32766)/32767)*5) { + stbiw__sbn(out) = 2; // truncate to DEFLATE 32K window and FLEVEL = 1 + for (j = 0; j < data_len;) { + int blocklen = data_len - j; + if (blocklen > 32767) blocklen = 32767; + stbiw__sbpush(out, data_len - j == blocklen); // BFINAL = ?, BTYPE = 0 -- no compression + stbiw__sbpush(out, STBIW_UCHAR(blocklen)); // LEN + stbiw__sbpush(out, STBIW_UCHAR(blocklen >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(~blocklen)); // NLEN + stbiw__sbpush(out, STBIW_UCHAR(~blocklen >> 8)); + memcpy(out+stbiw__sbn(out), data+j, blocklen); + stbiw__sbn(out) += blocklen; + j += blocklen; + } + } + + { + // compute adler32 on input + unsigned int s1=1, s2=0; + int blocklen = (int) (data_len % 5552); + j=0; + while (j < data_len) { + for (i=0; i < blocklen; ++i) { s1 += data[j+i]; s2 += s1; } + s1 %= 65521; s2 %= 65521; + j += blocklen; + blocklen = 5552; + } + stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s2)); + stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s1)); + } + *out_len = stbiw__sbn(out); + // make returned pointer freeable + STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len); + return (unsigned char *) stbiw__sbraw(out); +#endif // STBIW_ZLIB_COMPRESS +} + +static unsigned int stbiw__crc32(unsigned char *buffer, int len) +{ +#ifdef STBIW_CRC32 + return STBIW_CRC32(buffer, len); +#else + static unsigned int crc_table[256] = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0eDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + unsigned int crc = ~0u; + int i; + for (i=0; i < len; ++i) + crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)]; + return ~crc; +#endif +} + +#define stbiw__wpng4(o,a,b,c,d) ((o)[0]=STBIW_UCHAR(a),(o)[1]=STBIW_UCHAR(b),(o)[2]=STBIW_UCHAR(c),(o)[3]=STBIW_UCHAR(d),(o)+=4) +#define stbiw__wp32(data,v) stbiw__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v)); +#define stbiw__wptag(data,s) stbiw__wpng4(data, s[0],s[1],s[2],s[3]) + +static void stbiw__wpcrc(unsigned char **data, int len) +{ + unsigned int crc = stbiw__crc32(*data - len - 4, len+4); + stbiw__wp32(*data, crc); +} + +static unsigned char stbiw__paeth(int a, int b, int c) +{ + int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c); + if (pa <= pb && pa <= pc) return STBIW_UCHAR(a); + if (pb <= pc) return STBIW_UCHAR(b); + return STBIW_UCHAR(c); +} + +// @OPTIMIZE: provide an option that always forces left-predict or paeth predict +static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer) +{ + static int mapping[] = { 0,1,2,3,4 }; + static int firstmap[] = { 0,1,0,5,6 }; + int *mymap = (y != 0) ? mapping : firstmap; + int i; + int type = mymap[filter_type]; + unsigned char *z = pixels + stride_bytes * (stbi__flip_vertically_on_write ? height-1-y : y); + int signed_stride = stbi__flip_vertically_on_write ? -stride_bytes : stride_bytes; + + if (type==0) { + memcpy(line_buffer, z, width*n); + return; + } + + // first loop isn't optimized since it's just one pixel + for (i = 0; i < n; ++i) { + switch (type) { + case 1: line_buffer[i] = z[i]; break; + case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: line_buffer[i] = z[i] - (z[i-signed_stride]>>1); break; + case 4: line_buffer[i] = (signed char) (z[i] - stbiw__paeth(0,z[i-signed_stride],0)); break; + case 5: line_buffer[i] = z[i]; break; + case 6: line_buffer[i] = z[i]; break; + } + } + switch (type) { + case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break; + case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break; + case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break; + case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break; + case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break; + } +} + +STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len) +{ + int force_filter = stbi_write_force_png_filter; + int ctype[5] = { -1, 0, 4, 2, 6 }; + unsigned char sig[8] = { 137,80,78,71,13,10,26,10 }; + unsigned char *out,*o, *filt, *zlib; + signed char *line_buffer; + int j,zlen; + + if (stride_bytes == 0) + stride_bytes = x * n; + + if (force_filter >= 5) { + force_filter = -1; + } + + filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0; + line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; } + for (j=0; j < y; ++j) { + int filter_type; + if (force_filter > -1) { + filter_type = force_filter; + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer); + } else { // Estimate the best filter by running through all of them: + int best_filter = 0, best_filter_val = 0x7fffffff, est, i; + for (filter_type = 0; filter_type < 5; filter_type++) { + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer); + + // Estimate the entropy of the line using this filter; the less, the better. + est = 0; + for (i = 0; i < x*n; ++i) { + est += abs((signed char) line_buffer[i]); + } + if (est < best_filter_val) { + best_filter_val = est; + best_filter = filter_type; + } + } + if (filter_type != best_filter) { // If the last iteration already got us the best filter, don't redo it + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer); + filter_type = best_filter; + } + } + // when we get here, filter_type contains the filter type, and line_buffer contains the data + filt[j*(x*n+1)] = (unsigned char) filter_type; + STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n); + } + STBIW_FREE(line_buffer); + zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, stbi_write_png_compression_level); + STBIW_FREE(filt); + if (!zlib) return 0; + + // each tag requires 12 bytes of overhead + out = (unsigned char *) STBIW_MALLOC(8 + 12+13 + 12+zlen + 12); + if (!out) return 0; + *out_len = 8 + 12+13 + 12+zlen + 12; + + o=out; + STBIW_MEMMOVE(o,sig,8); o+= 8; + stbiw__wp32(o, 13); // header length + stbiw__wptag(o, "IHDR"); + stbiw__wp32(o, x); + stbiw__wp32(o, y); + *o++ = 8; + *o++ = STBIW_UCHAR(ctype[n]); + *o++ = 0; + *o++ = 0; + *o++ = 0; + stbiw__wpcrc(&o,13); + + stbiw__wp32(o, zlen); + stbiw__wptag(o, "IDAT"); + STBIW_MEMMOVE(o, zlib, zlen); + o += zlen; + STBIW_FREE(zlib); + stbiw__wpcrc(&o, zlen); + + stbiw__wp32(o,0); + stbiw__wptag(o, "IEND"); + stbiw__wpcrc(&o,0); + + STBIW_ASSERT(o == out + *out_len); + + return out; +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes) +{ + FILE *f; + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + + f = stbiw__fopen(filename, "wb"); + if (!f) { STBIW_FREE(png); return 0; } + fwrite(png, 1, len, f); + fclose(f); + STBIW_FREE(png); + return 1; +} +#endif + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes) +{ + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + func(context, png, len); + STBIW_FREE(png); + return 1; +} + + +/* *************************************************************************** + * + * JPEG writer + * + * This is based on Jon Olick's jo_jpeg.cpp: + * public domain Simple, Minimalistic JPEG writer - http://www.jonolick.com/code.html + */ + +static const unsigned char stbiw__jpg_ZigZag[] = { 0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18, + 24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63 }; + +static void stbiw__jpg_writeBits(stbi__write_context *s, int *bitBufP, int *bitCntP, const unsigned short *bs) { + int bitBuf = *bitBufP, bitCnt = *bitCntP; + bitCnt += bs[1]; + bitBuf |= bs[0] << (24 - bitCnt); + while(bitCnt >= 8) { + unsigned char c = (bitBuf >> 16) & 255; + stbiw__putc(s, c); + if(c == 255) { + stbiw__putc(s, 0); + } + bitBuf <<= 8; + bitCnt -= 8; + } + *bitBufP = bitBuf; + *bitCntP = bitCnt; +} + +static void stbiw__jpg_DCT(float *d0p, float *d1p, float *d2p, float *d3p, float *d4p, float *d5p, float *d6p, float *d7p) { + float d0 = *d0p, d1 = *d1p, d2 = *d2p, d3 = *d3p, d4 = *d4p, d5 = *d5p, d6 = *d6p, d7 = *d7p; + float z1, z2, z3, z4, z5, z11, z13; + + float tmp0 = d0 + d7; + float tmp7 = d0 - d7; + float tmp1 = d1 + d6; + float tmp6 = d1 - d6; + float tmp2 = d2 + d5; + float tmp5 = d2 - d5; + float tmp3 = d3 + d4; + float tmp4 = d3 - d4; + + // Even part + float tmp10 = tmp0 + tmp3; // phase 2 + float tmp13 = tmp0 - tmp3; + float tmp11 = tmp1 + tmp2; + float tmp12 = tmp1 - tmp2; + + d0 = tmp10 + tmp11; // phase 3 + d4 = tmp10 - tmp11; + + z1 = (tmp12 + tmp13) * 0.707106781f; // c4 + d2 = tmp13 + z1; // phase 5 + d6 = tmp13 - z1; + + // Odd part + tmp10 = tmp4 + tmp5; // phase 2 + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + // The rotator is modified from fig 4-8 to avoid extra negations. + z5 = (tmp10 - tmp12) * 0.382683433f; // c6 + z2 = tmp10 * 0.541196100f + z5; // c2-c6 + z4 = tmp12 * 1.306562965f + z5; // c2+c6 + z3 = tmp11 * 0.707106781f; // c4 + + z11 = tmp7 + z3; // phase 5 + z13 = tmp7 - z3; + + *d5p = z13 + z2; // phase 6 + *d3p = z13 - z2; + *d1p = z11 + z4; + *d7p = z11 - z4; + + *d0p = d0; *d2p = d2; *d4p = d4; *d6p = d6; +} + +static void stbiw__jpg_calcBits(int val, unsigned short bits[2]) { + int tmp1 = val < 0 ? -val : val; + val = val < 0 ? val-1 : val; + bits[1] = 1; + while(tmp1 >>= 1) { + ++bits[1]; + } + bits[0] = val & ((1<<bits[1])-1); +} + +static int stbiw__jpg_processDU(stbi__write_context *s, int *bitBuf, int *bitCnt, float *CDU, int du_stride, float *fdtbl, int DC, const unsigned short HTDC[256][2], const unsigned short HTAC[256][2]) { + const unsigned short EOB[2] = { HTAC[0x00][0], HTAC[0x00][1] }; + const unsigned short M16zeroes[2] = { HTAC[0xF0][0], HTAC[0xF0][1] }; + int dataOff, i, j, n, diff, end0pos, x, y; + int DU[64]; + + // DCT rows + for(dataOff=0, n=du_stride*8; dataOff<n; dataOff+=du_stride) { + stbiw__jpg_DCT(&CDU[dataOff], &CDU[dataOff+1], &CDU[dataOff+2], &CDU[dataOff+3], &CDU[dataOff+4], &CDU[dataOff+5], &CDU[dataOff+6], &CDU[dataOff+7]); + } + // DCT columns + for(dataOff=0; dataOff<8; ++dataOff) { + stbiw__jpg_DCT(&CDU[dataOff], &CDU[dataOff+du_stride], &CDU[dataOff+du_stride*2], &CDU[dataOff+du_stride*3], &CDU[dataOff+du_stride*4], + &CDU[dataOff+du_stride*5], &CDU[dataOff+du_stride*6], &CDU[dataOff+du_stride*7]); + } + // Quantize/descale/zigzag the coefficients + for(y = 0, j=0; y < 8; ++y) { + for(x = 0; x < 8; ++x,++j) { + float v; + i = y*du_stride+x; + v = CDU[i]*fdtbl[j]; + // DU[stbiw__jpg_ZigZag[j]] = (int)(v < 0 ? ceilf(v - 0.5f) : floorf(v + 0.5f)); + // ceilf() and floorf() are C99, not C89, but I /think/ they're not needed here anyway? + DU[stbiw__jpg_ZigZag[j]] = (int)(v < 0 ? v - 0.5f : v + 0.5f); + } + } + + // Encode DC + diff = DU[0] - DC; + if (diff == 0) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTDC[0]); + } else { + unsigned short bits[2]; + stbiw__jpg_calcBits(diff, bits); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTDC[bits[1]]); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits); + } + // Encode ACs + end0pos = 63; + for(; (end0pos>0)&&(DU[end0pos]==0); --end0pos) { + } + // end0pos = first element in reverse order !=0 + if(end0pos == 0) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + return DU[0]; + } + for(i = 1; i <= end0pos; ++i) { + int startpos = i; + int nrzeroes; + unsigned short bits[2]; + for (; DU[i]==0 && i<=end0pos; ++i) { + } + nrzeroes = i-startpos; + if ( nrzeroes >= 16 ) { + int lng = nrzeroes>>4; + int nrmarker; + for (nrmarker=1; nrmarker <= lng; ++nrmarker) + stbiw__jpg_writeBits(s, bitBuf, bitCnt, M16zeroes); + nrzeroes &= 15; + } + stbiw__jpg_calcBits(DU[i], bits); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTAC[(nrzeroes<<4)+bits[1]]); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits); + } + if(end0pos != 63) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + } + return DU[0]; +} + +static int stbi_write_jpg_core(stbi__write_context *s, int width, int height, int comp, const void* data, int quality) { + // Constants that don't pollute global namespace + static const unsigned char std_dc_luminance_nrcodes[] = {0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0}; + static const unsigned char std_dc_luminance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_luminance_nrcodes[] = {0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d}; + static const unsigned char std_ac_luminance_values[] = { + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + static const unsigned char std_dc_chrominance_nrcodes[] = {0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0}; + static const unsigned char std_dc_chrominance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_chrominance_nrcodes[] = {0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77}; + static const unsigned char std_ac_chrominance_values[] = { + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + // Huffman tables + static const unsigned short YDC_HT[256][2] = { {0,2},{2,3},{3,3},{4,3},{5,3},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9}}; + static const unsigned short UVDC_HT[256][2] = { {0,2},{1,2},{2,2},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9},{1022,10},{2046,11}}; + static const unsigned short YAC_HT[256][2] = { + {10,4},{0,2},{1,2},{4,3},{11,4},{26,5},{120,7},{248,8},{1014,10},{65410,16},{65411,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {12,4},{27,5},{121,7},{502,9},{2038,11},{65412,16},{65413,16},{65414,16},{65415,16},{65416,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {28,5},{249,8},{1015,10},{4084,12},{65417,16},{65418,16},{65419,16},{65420,16},{65421,16},{65422,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{503,9},{4085,12},{65423,16},{65424,16},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1016,10},{65430,16},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2039,11},{65438,16},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {123,7},{4086,12},{65446,16},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {250,8},{4087,12},{65454,16},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{32704,15},{65462,16},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65470,16},{65471,16},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65479,16},{65480,16},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1017,10},{65488,16},{65489,16},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{65497,16},{65498,16},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2040,11},{65506,16},{65507,16},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {65515,16},{65516,16},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65525,16},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const unsigned short UVAC_HT[256][2] = { + {0,2},{1,2},{4,3},{10,4},{24,5},{25,5},{56,6},{120,7},{500,9},{1014,10},{4084,12},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {11,4},{57,6},{246,8},{501,9},{2038,11},{4085,12},{65416,16},{65417,16},{65418,16},{65419,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {26,5},{247,8},{1015,10},{4086,12},{32706,15},{65420,16},{65421,16},{65422,16},{65423,16},{65424,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {27,5},{248,8},{1016,10},{4087,12},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{65430,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{502,9},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{65438,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1017,10},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{65446,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {121,7},{2039,11},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{65454,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2040,11},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{65462,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {249,8},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{65470,16},{65471,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {503,9},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{65479,16},{65480,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{65488,16},{65489,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{65497,16},{65498,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{65506,16},{65507,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{65515,16},{65516,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {16352,14},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{65525,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{32707,15},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const int YQT[] = {16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22, + 37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99}; + static const int UVQT[] = {17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99, + 99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99}; + static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, + 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; + + int row, col, i, k, subsample; + float fdtbl_Y[64], fdtbl_UV[64]; + unsigned char YTable[64], UVTable[64]; + + if(!data || !width || !height || comp > 4 || comp < 1) { + return 0; + } + + quality = quality ? quality : 90; + subsample = quality <= 90 ? 1 : 0; + quality = quality < 1 ? 1 : quality > 100 ? 100 : quality; + quality = quality < 50 ? 5000 / quality : 200 - quality * 2; + + for(i = 0; i < 64; ++i) { + int uvti, yti = (YQT[i]*quality+50)/100; + YTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (yti < 1 ? 1 : yti > 255 ? 255 : yti); + uvti = (UVQT[i]*quality+50)/100; + UVTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (uvti < 1 ? 1 : uvti > 255 ? 255 : uvti); + } + + for(row = 0, k = 0; row < 8; ++row) { + for(col = 0; col < 8; ++col, ++k) { + fdtbl_Y[k] = 1 / (YTable [stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + fdtbl_UV[k] = 1 / (UVTable[stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + } + } + + // Write Headers + { + static const unsigned char head0[] = { 0xFF,0xD8,0xFF,0xE0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xFF,0xDB,0,0x84,0 }; + static const unsigned char head2[] = { 0xFF,0xDA,0,0xC,3,1,0,2,0x11,3,0x11,0,0x3F,0 }; + const unsigned char head1[] = { 0xFF,0xC0,0,0x11,8,(unsigned char)(height>>8),STBIW_UCHAR(height),(unsigned char)(width>>8),STBIW_UCHAR(width), + 3,1,(unsigned char)(subsample?0x22:0x11),0,2,0x11,1,3,0x11,1,0xFF,0xC4,0x01,0xA2,0 }; + s->func(s->context, (void*)head0, sizeof(head0)); + s->func(s->context, (void*)YTable, sizeof(YTable)); + stbiw__putc(s, 1); + s->func(s->context, UVTable, sizeof(UVTable)); + s->func(s->context, (void*)head1, sizeof(head1)); + s->func(s->context, (void*)(std_dc_luminance_nrcodes+1), sizeof(std_dc_luminance_nrcodes)-1); + s->func(s->context, (void*)std_dc_luminance_values, sizeof(std_dc_luminance_values)); + stbiw__putc(s, 0x10); // HTYACinfo + s->func(s->context, (void*)(std_ac_luminance_nrcodes+1), sizeof(std_ac_luminance_nrcodes)-1); + s->func(s->context, (void*)std_ac_luminance_values, sizeof(std_ac_luminance_values)); + stbiw__putc(s, 1); // HTUDCinfo + s->func(s->context, (void*)(std_dc_chrominance_nrcodes+1), sizeof(std_dc_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_dc_chrominance_values, sizeof(std_dc_chrominance_values)); + stbiw__putc(s, 0x11); // HTUACinfo + s->func(s->context, (void*)(std_ac_chrominance_nrcodes+1), sizeof(std_ac_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_ac_chrominance_values, sizeof(std_ac_chrominance_values)); + s->func(s->context, (void*)head2, sizeof(head2)); + } + + // Encode 8x8 macroblocks + { + static const unsigned short fillBits[] = {0x7F, 7}; + int DCY=0, DCU=0, DCV=0; + int bitBuf=0, bitCnt=0; + // comp == 2 is grey+alpha (alpha is ignored) + int ofsG = comp > 2 ? 1 : 0, ofsB = comp > 2 ? 2 : 0; + const unsigned char *dataR = (const unsigned char *)data; + const unsigned char *dataG = dataR + ofsG; + const unsigned char *dataB = dataR + ofsB; + int x, y, pos; + if(subsample) { + for(y = 0; y < height; y += 16) { + for(x = 0; x < width; x += 16) { + float Y[256], U[256], V[256]; + for(row = y, pos = 0; row < y+16; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+16; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+0, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+8, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+128, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+136, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + + // subsample U,V + { + float subU[64], subV[64]; + int yy, xx; + for(yy = 0, pos = 0; yy < 8; ++yy) { + for(xx = 0; xx < 8; ++xx, ++pos) { + int j = yy*32+xx*2; + subU[pos] = (U[j+0] + U[j+1] + U[j+16] + U[j+17]) * 0.25f; + subV[pos] = (V[j+0] + V[j+1] + V[j+16] + V[j+17]) * 0.25f; + } + } + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subU, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subV, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + } else { + for(y = 0; y < height; y += 8) { + for(x = 0; x < width; x += 8) { + float Y[64], U[64], V[64]; + for(row = y, pos = 0; row < y+8; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+8; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y, 8, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, U, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, V, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + + // Do the bit alignment of the EOI marker + stbiw__jpg_writeBits(s, &bitBuf, &bitCnt, fillBits); + } + + // EOI + stbiw__putc(s, 0xFF); + stbiw__putc(s, 0xD9); + + return 1; +} + +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_jpg_core(&s, x, y, comp, (void *) data, quality); +} + + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_jpg_core(&s, x, y, comp, data, quality); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +#endif // STB_IMAGE_WRITE_IMPLEMENTATION + +/* Revision history + 1.16 (2021-07-11) + make Deflate code emit uncompressed blocks when it would otherwise expand + support writing BMPs with alpha channel + 1.15 (2020-07-13) unknown + 1.14 (2020-02-02) updated JPEG writer to downsample chroma channels + 1.13 + 1.12 + 1.11 (2019-08-11) + + 1.10 (2019-02-07) + support utf8 filenames in Windows; fix warnings and platform ifdefs + 1.09 (2018-02-11) + fix typo in zlib quality API, improve STB_I_W_STATIC in C++ + 1.08 (2018-01-29) + add stbi__flip_vertically_on_write, external zlib, zlib quality, choose PNG filter + 1.07 (2017-07-24) + doc fix + 1.06 (2017-07-23) + writing JPEG (using Jon Olick's code) + 1.05 ??? + 1.04 (2017-03-03) + monochrome BMP expansion + 1.03 ??? + 1.02 (2016-04-02) + avoid allocating large structures on the stack + 1.01 (2016-01-16) + STBIW_REALLOC_SIZED: support allocators with no realloc support + avoid race-condition in crc initialization + minor compile issues + 1.00 (2015-09-14) + installable file IO function + 0.99 (2015-09-13) + warning fixes; TGA rle support + 0.98 (2015-04-08) + added STBIW_MALLOC, STBIW_ASSERT etc + 0.97 (2015-01-18) + fixed HDR asserts, rewrote HDR rle logic + 0.96 (2015-01-17) + add HDR output + fix monochrome BMP + 0.95 (2014-08-17) + add monochrome TGA output + 0.94 (2014-05-31) + rename private functions to avoid conflicts with stb_image.h + 0.93 (2014-05-27) + warning fixes + 0.92 (2010-08-01) + casts to unsigned char to fix warnings + 0.91 (2010-07-17) + first public release + 0.90 first internal release +*/ + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/src/compaction.zig b/src/compaction.zig new file mode 100644 index 0000000..f620f30 --- /dev/null +++ b/src/compaction.zig @@ -0,0 +1,838 @@ +//! Compaction sizing and retention policy. +//! +//! Compaction reduces context size by summarizing an older prefix of the +//! conversation into a synthetic seed (a `.CompactionSummary` block) and +//! keeping a recent suffix of whole turns verbatim. This module owns the +//! *policy* half — deciding where to split prefix-to-summarize from +//! suffix-to-keep — and the transcript serialization of the prefix. The +//! *execution* half (running the compaction request, mutating the +//! conversation) lives in `agent.zig`. +//! +//! ## Retention model +//! +//! The retention unit is a whole **turn**: a user message followed by the +//! assistant message(s) it elicited (including any interleaved tool-result +//! user messages that belong to the same request/response cycle). v1 never +//! splits a turn. +//! +//! Splitting walks the active conversation backward, accumulating a token +//! estimate per turn, and stops as soon as the running total *exceeds* +//! `keep_verbatim`. The turn that crosses the threshold is the last turn +//! folded into the summary; turns after it are kept verbatim. So +//! `keep_verbatim` is an upper bound on the kept-suffix size, not a target. +//! +//! ## Sizing +//! +//! When provider-reported `Usage` is available for a message, its token +//! total drives sizing. Otherwise we fall back to a cheap monotonic +//! heuristic: word count scaled by `word_to_token_factor`. The fallback is +//! not exact but is sufficient to choose a conservative recent suffix. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const conversation = @import("conversation.zig"); +const session_mod = @import("session.zig"); + +pub const Message = conversation.Message; +pub const Usage = session_mod.Usage; + +/// Multiplier applied to a word count to approximate token count when no +/// provider usage data is available. Tokens-per-word runs ~1.3 for English +/// prose and code on common BPE tokenizers (rough, deliberately so). +pub const word_to_token_factor: f64 = 1.3; + +/// Estimate the token size of a single message. +/// +/// If `usage` is non-null, its prompt+output token total is used directly. +/// Otherwise the message's textual content is word-counted and scaled by +/// `word_to_token_factor`. +pub fn messageTokenEstimate(msg: Message, usage: ?Usage) u64 { + if (usage) |u| { + // Total tokens attributable to this message: all input categories + // plus output. (For a user message only inputs are typically set; + // for an assistant message output dominates.) + return u.input + u.cache_read + u.cache_write + u.output; + } + const words = countWords(msg); + const est = @as(f64, @floatFromInt(words)) * word_to_token_factor; + return @intFromFloat(@ceil(est)); +} + +/// The full prompt size (all input categories) reported by a turn's usage. +/// Provider `input` may exclude cached tokens, which appear under +/// `cache_read`/`cache_write`; summing them recovers the true context size +/// that was sent to the model. +fn promptTokens(u: Usage) u64 { + return u.input + u.cache_read + u.cache_write; +} + +/// The cumulative context footprint at a turn's final assistant message: +/// the full prompt it consumed plus the output it produced. This is the +/// running total of *all* prior conversation tokens as of that turn. +fn cumulativeAt(u: Usage) u64 { + return promptTokens(u) + u.output; +} + +/// Estimate the incremental token cost of the turn spanning +/// `messages[start..end)` using cumulative provider usage. +/// +/// Provider `usage.input` is *cumulative*: it is the entire prompt sent for +/// that turn (the whole prior conversation), not the size of one message. +/// So a turn's own contribution is the delta between its final assistant's +/// cumulative footprint and the previous turn's: +/// +/// cumulativeAt(this_turn_last_assistant) +/// - cumulativeAt(prev_turn_last_assistant) +/// +/// This single delta captures the turn-start user prompt, every +/// tool-result, every intermediate assistant output, and the final +/// assistant output at once — no per-message decomposition needed. At +/// session start there is no previous assistant, so `before` is zero and +/// the first turn's cost is its own full cumulative footprint (which fairly +/// includes the system prompt). +/// +/// `prev_usage` is the usage of the assistant message immediately before +/// `start` (the previous turn's final assistant), or null at session start. +/// +/// Returns null when usage data is insufficient (no usage on this turn's +/// assistant), signalling the caller to fall back to word counting. +fn turnTokenEstimate( + messages: []const Message, + usages: []const ?Usage, + start: usize, + end: usize, + prev_usage: ?Usage, +) ?u64 { + // Find this turn's final assistant usage (cumulative as of turn end). + var after: ?Usage = null; + var j = end; + while (j > start) { + j -= 1; + if (messages[j].role == .assistant) { + if (usages[j]) |u| { + after = u; + break; + } + } + } + const after_usage = after orelse return null; + const after_total = cumulativeAt(after_usage); + const before_total = if (prev_usage) |b| cumulativeAt(b) else 0; + // Saturating subtraction: usage is an estimate and can be non-monotone. + return if (after_total > before_total) after_total - before_total else 0; +} + +/// Find the usage of the assistant message immediately before index +/// `start`, scanning backward. Returns null if none precedes it. +fn priorAssistantUsage( + messages: []const Message, + usages: []const ?Usage, + start: usize, +) ?Usage { + var j = start; + while (j > 0) { + j -= 1; + if (messages[j].role == .assistant) { + if (usages[j]) |u| return u; + } + } + return null; +} + +/// Count whitespace-delimited words across all textual content in a +/// message. Tool-use input JSON and tool-result content count too — they +/// occupy real context. +fn countWords(msg: Message) u64 { + var total: u64 = 0; + for (msg.content.items) |block| { + switch (block) { + .Text => |b| total += countWordsIn(b.items), + .Thinking => |b| total += countWordsIn(b.text.items), + .ToolUse => |b| total += countWordsIn(b.input.items), + .ToolResult => |b| { + // Text parts count by words. Media parts (base64 image / + // PDF blobs) would wildly distort the retention window if + // counted by length, so each is charged a fixed estimate + // (matching the pi reference impl's ~4800-char image cost, + // divided back out to words via `word_to_token_factor`). + for (b.parts.items) |p| switch (p) { + .text => |t| total += countWordsIn(t.items), + .media => total += media_part_word_estimate, + }; + }, + .System => |b| total += countWordsIn(b.text.items), + .CompactionSummary => |b| total += countWordsIn(b.text.items), + } + } + return total; +} + +/// Fixed per-media-part word estimate. The pi reference impl sizes each +/// inline image at ~4800 characters for compaction token math; expressed +/// as words (≈4800/6 chars-per-word) this lands near 800. +const media_part_word_estimate: u64 = 800; + +fn countWordsIn(text: []const u8) u64 { + var count: u64 = 0; + var in_word = false; + for (text) |c| { + const is_space = c == ' ' or c == '\t' or c == '\n' or c == '\r'; + if (is_space) { + in_word = false; + } else if (!in_word) { + in_word = true; + count += 1; + } + } + return count; +} + +/// The result of a retention split over a conversation. +pub const Split = struct { + /// Index into the *full* message list. Messages in `[0, prefix_end)` + /// are summarized; messages in `[prefix_end, len)` are kept verbatim. + /// A value equal to the message count means "summarize everything". + prefix_end: usize, + /// Number of whole turns kept verbatim. + kept_turns: usize, +}; + +/// Decide the prefix/suffix split for compaction. +/// +/// `messages` is the conversation to compact. Only the active window +/// (everything after the latest existing `.CompactionSummary`, if any) is +/// considered — an earlier compacted prefix is already summarized and is +/// never re-walked. `usages`, when provided, must be the same length as +/// `messages` and supplies per-message provider usage (null entries fall +/// back to word counting). +/// +/// Leading system messages are never part of a turn and are always treated +/// as belonging to the (kept) structural frame, not the summarized prefix: +/// the split index is reported relative to the full list but the walk only +/// accumulates user/assistant conversation turns. +/// +/// Returns the split. When the entire active conversation fits within +/// `keep_verbatim`, `prefix_end` points at the first active turn (nothing +/// to summarize) — callers should treat that as a no-op. +pub fn computeSplit( + messages: []const Message, + usages: ?[]const ?Usage, + keep_verbatim: u64, +) Split { + if (usages) |u| std.debug.assert(u.len == messages.len); + + // The summary message itself anchors the window; active turns begin after it. + const active_start = if (conversation.latestCompactionIndex(messages)) |anchor| anchor + 1 else 0; + + // Identify turn boundaries within the active window. A turn starts at a + // user message that is NOT purely tool-results (a fresh human/user + // prompt or compaction-follow user prompt) and runs until the next such + // boundary. Tool-result user messages and assistant messages attach to + // the current turn. + // + // We collect the start index of each turn, then walk turns backward. + var turn_starts_buf: [4096]usize = undefined; + var n_turns: usize = 0; + var i = active_start; + // Skip leading system messages (structural frame, not a turn). + while (i < messages.len and messages[i].role == .system) : (i += 1) {} + const first_turn_msg = i; + while (i < messages.len) : (i += 1) { + const msg = messages[i]; + if (msg.role == .system) continue; + if (isTurnStart(messages, i)) { + if (n_turns < turn_starts_buf.len) { + turn_starts_buf[n_turns] = i; + } + n_turns += 1; + } + } + const turn_starts = turn_starts_buf[0..@min(n_turns, turn_starts_buf.len)]; + + if (turn_starts.len == 0) { + // No turns to keep or summarize. + return .{ .prefix_end = messages.len, .kept_turns = 0 }; + } + + // Walk turns backward, accumulating token totals. Stop as soon as the + // running total exceeds the budget: the crossing turn is summarized. + var running: u64 = 0; + var kept_turns: usize = 0; + var t = turn_starts.len; + while (t > 0) { + t -= 1; + const start = turn_starts[t]; + const end = if (t + 1 < turn_starts.len) turn_starts[t + 1] else messages.len; + var turn_tokens: u64 = 0; + // Prefer cumulative-usage deltas: provider `input` is the whole + // prior conversation, so a turn's true cost is the delta between + // its final assistant footprint and the previous turn's. Fall back + // to per-message word counting when usage is unavailable. + const turn_estimate: ?u64 = if (usages) |u| + turnTokenEstimate(messages, u, start, end, priorAssistantUsage(messages, u, start)) + else + null; + if (turn_estimate) |est| { + turn_tokens = est; + } else { + var j = start; + while (j < end) : (j += 1) { + const usage: ?Usage = if (usages) |u| u[j] else null; + turn_tokens += messageTokenEstimate(messages[j], usage); + } + } + if (running + turn_tokens > keep_verbatim) { + // This turn crosses the budget: it (and everything before it) + // is summarized. Kept suffix starts at the next turn. + const kept_start = if (t + 1 < turn_starts.len) turn_starts[t + 1] else messages.len; + return .{ .prefix_end = kept_start, .kept_turns = kept_turns }; + } + running += turn_tokens; + kept_turns += 1; + } + + // Everything fits: nothing to summarize. Prefix ends at the first turn. + return .{ .prefix_end = first_turn_msg, .kept_turns = kept_turns }; +} + +/// Whether the message at `index` begins a new turn. +/// +/// A turn starts at a user message that is both: +/// (a) not a tool-result message — a user message carrying any tool-result +/// block continues the assistant's current tool round, and +/// (b) the *first* user message after a non-user message (or the start of +/// the conversation). +/// +/// Condition (b) collapses a run of consecutive plain user messages into a +/// single turn: only the first is a boundary. Without it, each bare user +/// message would be its own turn, and the ones with no trailing assistant +/// would fall to the per-message word-count fallback and be counted a second +/// time on top of the turn's cumulative-usage delta (which already includes +/// them). System and assistant messages never start a turn; preceding system +/// messages are transparent for the "after a non-user message" test. +fn isTurnStart(messages: []const Message, index: usize) bool { + const msg = messages[index]; + if (msg.role != .user) return false; + for (msg.content.items) |block| { + if (block == .ToolResult) return false; + } + // First user message after a non-user message, or the start of the + // active window. Structural frame messages are transparent here: leading + // system prompts and the compaction-summary anchor (a synthetic `.user` + // message) are not conversational user turns, so a real user message + // following one still opens a turn. + var j = index; + while (j > 0) { + j -= 1; + if (isStructuralFrame(messages[j])) continue; + return messages[j].role != .user; + } + return true; +} + +/// Whether a message is part of the structural frame rather than a +/// conversational turn: a system message, or the synthetic `.user` message +/// carrying a compaction summary that anchors an active window. +fn isStructuralFrame(msg: Message) bool { + if (msg.role == .system) return true; + for (msg.content.items) |block| { + if (block == .CompactionSummary) return true; + } + return false; +} + +// ============================================================================= +// Transcript serialization +// ============================================================================= + +/// Serialize a range of conversation messages into a plain-text transcript +/// artifact. The output marks message roles and structure so the +/// compaction model treats it as material under analysis rather than live +/// chat to continue. Caller owns the returned bytes. +/// +/// Format (one labelled section per block, blank-line separated): +/// [User]: ... +/// [Assistant]: ... +/// [Assistant thinking]: ... +/// [Assistant tool call: <name> (<id>)]: <input> +/// [Tool result (<id>)]: ... +/// [Previous summary]: ... +/// +/// System messages are skipped: the system prompt survives compaction and +/// is not part of the material being summarized. +pub fn serializeTranscript( + allocator: Allocator, + messages: []const Message, +) ![]u8 { + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(allocator); + const w = &out; + + var first = true; + for (messages) |msg| { + if (msg.role == .system) continue; + for (msg.content.items) |block| { + if (!first) try w.appendSlice(allocator, "\n\n"); + first = false; + switch (block) { + .Text => |b| { + const label = if (msg.role == .assistant) "[Assistant]: " else "[User]: "; + try w.appendSlice(allocator, label); + try w.appendSlice(allocator, b.items); + }, + .Thinking => |b| { + try w.appendSlice(allocator, "[Assistant thinking]: "); + try w.appendSlice(allocator, b.text.items); + }, + .ToolUse => |b| { + const line = try std.fmt.allocPrint(allocator, "[Assistant tool call: {s} ({s})]: {s}", .{ + b.name, b.id, b.input.items, + }); + defer allocator.free(line); + try w.appendSlice(allocator, line); + }, + .ToolResult => |b| { + var body: conversation.TextualBlock = .empty; + defer body.deinit(allocator); + for (b.parts.items) |p| switch (p) { + .text => |t| try body.appendSlice(allocator, t.items), + .media => |m| { + const note = try std.fmt.allocPrint(allocator, "[{s} attachment]", .{m.media_type}); + defer allocator.free(note); + try body.appendSlice(allocator, note); + }, + }; + const line = try std.fmt.allocPrint(allocator, "[Tool result ({s})]: {s}", .{ + b.tool_use_id, body.items, + }); + defer allocator.free(line); + try w.appendSlice(allocator, line); + }, + .CompactionSummary => |b| { + try w.appendSlice(allocator, "[Previous summary]: "); + try w.appendSlice(allocator, b.text.items); + }, + .System => {}, + } + } + } + return out.toOwnedSlice(allocator); +} + +// ============================================================================= +// Compaction request user-prompt assembly +// ============================================================================= + +/// Build the user-prompt body for a compaction request: a brief framing, +/// the optional previous summary in a `<previous-summary>` section, and the +/// transcript of the prefix being summarized in a `<transcript>` section. +/// +/// `transcript` is the serialized prefix (see `serializeTranscript`). +/// `previous_summary`, when non-null, is the latest active compaction +/// summary text carried forward (the chained-compaction invariant: at most +/// one previous summary, never an accumulating stack). Caller owns the +/// returned bytes. +pub fn buildRequestBody( + allocator: Allocator, + transcript: []const u8, + previous_summary: ?[]const u8, +) ![]u8 { + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(allocator); + + if (previous_summary) |prev| { + try out.appendSlice(allocator, + "The previous compaction generated this summary:\n\n<previous-summary>\n"); + try out.appendSlice(allocator, prev); + try out.appendSlice(allocator, + "\n</previous-summary>\n\nThe conversation since that previous compaction:\n\n<transcript>\n"); + } else { + try out.appendSlice(allocator, + "The conversation to summarize:\n\n<transcript>\n"); + } + try out.appendSlice(allocator, transcript); + try out.appendSlice(allocator, "\n</transcript>"); + return out.toOwnedSlice(allocator); +} + +/// The latest active compaction summary text in `messages`, or null if the +/// conversation has never been compacted. Borrowed from `messages`. +pub fn latestSummaryText(messages: []const Message) ?[]const u8 { + const anchor = conversation.latestCompactionIndex(messages) orelse return null; + for (messages[anchor].content.items) |block| { + if (block == .CompactionSummary) return block.CompactionSummary.text.items; + } + return null; +} + +// ============================================================================= +// Tests +// ============================================================================= + +const testing = std.testing; + +fn userMsg(alloc: Allocator, text: []const u8) !Message { + var content: std.ArrayList(conversation.ContentBlock) = .empty; + try content.append(alloc, .{ .Text = try conversation.textualBlockFromSlice(alloc, text) }); + return .{ .role = .user, .content = content }; +} + +fn asstMsg(alloc: Allocator, text: []const u8) !Message { + var content: std.ArrayList(conversation.ContentBlock) = .empty; + try content.append(alloc, .{ .Text = try conversation.textualBlockFromSlice(alloc, text) }); + return .{ .role = .assistant, .content = content }; +} + +fn freeMsgs(alloc: Allocator, msgs: []Message) void { + for (msgs) |*m| m.deinit(alloc); +} + +/// Build a `.ToolResult` content block with a single text part. +fn textToolResult(alloc: Allocator, id: []const u8, text: []const u8) !conversation.ContentBlock { + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(alloc, .{ .text = try conversation.textualBlockFromSlice(alloc, text) }); + return .{ .ToolResult = .{ + .tool_use_id = try alloc.dupe(u8, id), + .parts = parts, + } }; +} + +test "countWordsIn - basic word counting" { + try testing.expectEqual(@as(u64, 0), countWordsIn("")); + try testing.expectEqual(@as(u64, 0), countWordsIn(" \n\t ")); + try testing.expectEqual(@as(u64, 1), countWordsIn("hello")); + try testing.expectEqual(@as(u64, 3), countWordsIn(" hello there world ")); +} + +test "messageTokenEstimate - usage wins when present" { + const a = testing.allocator; + var m = try userMsg(a, "one two three"); + defer m.deinit(a); + const u: Usage = .{ .input = 100, .output = 7, .cache_read = 3 }; + try testing.expectEqual(@as(u64, 110), messageTokenEstimate(m, u)); +} + +test "messageTokenEstimate - word-count fallback when usage absent" { + const a = testing.allocator; + var m = try userMsg(a, "one two three four"); // 4 words * 1.3 = 5.2 -> ceil 6 + defer m.deinit(a); + try testing.expectEqual(@as(u64, 6), messageTokenEstimate(m, null)); +} + +test "computeSplit - everything fits keeps all turns, summarizes nothing" { + const a = testing.allocator; + var msgs = [_]Message{ + try userMsg(a, "q1"), + try asstMsg(a, "a1"), + try userMsg(a, "q2"), + try asstMsg(a, "a2"), + }; + defer freeMsgs(a, &msgs); + + const split = computeSplit(&msgs, null, 1_000_000); + try testing.expectEqual(@as(usize, 0), split.prefix_end); + try testing.expectEqual(@as(usize, 2), split.kept_turns); +} + +test "computeSplit - tiny budget summarizes all but the last turn" { + const a = testing.allocator; + // Each message ~ a few words; usage forces deterministic sizes. + var msgs = [_]Message{ + try userMsg(a, "q1"), + try asstMsg(a, "a1"), + try userMsg(a, "q2"), + try asstMsg(a, "a2"), + try userMsg(a, "q3"), + try asstMsg(a, "a3"), + }; + defer freeMsgs(a, &msgs); + + // Each turn contributes ~200 incremental tokens (100 user + 100 + // output). Provider `input` is cumulative: it is the whole prior + // conversation as of that assistant turn. So the assistant inputs grow + // 100, 300, 500 and the per-turn delta is a steady 200. Budget 250 + // keeps only the last turn (200 <= 250); adding the prior turn (400) + // exceeds it. + const usages = [_]?Usage{ + .{ .input = 0 }, .{ .input = 100, .output = 100 }, + .{ .input = 0 }, .{ .input = 300, .output = 100 }, + .{ .input = 0 }, .{ .input = 500, .output = 100 }, + }; + const split = computeSplit(&msgs, &usages, 250); + // Kept suffix = turn starting at index 4 (q3/a3). + try testing.expectEqual(@as(usize, 4), split.prefix_end); + try testing.expectEqual(@as(usize, 1), split.kept_turns); +} + +test "computeSplit - crossing turn goes into the summary (upper-bound semantics)" { + const a = testing.allocator; + var msgs = [_]Message{ + try userMsg(a, "q1"), + try asstMsg(a, "a1"), + try userMsg(a, "q2"), + try asstMsg(a, "a2"), + }; + defer freeMsgs(a, &msgs); + + // 200 incremental tokens per turn via cumulative inputs (100, 300). + // Budget exactly 200: last turn (delta 200) does NOT exceed 200, so + // it's kept. Adding turn 1 => 400 > 200, so turn 1 summarized. + const usages = [_]?Usage{ + .{ .input = 0 }, .{ .input = 100, .output = 100 }, + .{ .input = 0 }, .{ .input = 300, .output = 100 }, + }; + const split = computeSplit(&msgs, &usages, 200); + try testing.expectEqual(@as(usize, 2), split.prefix_end); + try testing.expectEqual(@as(usize, 1), split.kept_turns); +} + +test "computeSplit - tool-result user messages attach to the current turn" { + const a = testing.allocator; + + // Turn 1: user q1 -> assistant(tool_use) -> user(tool_result) -> assistant a1 + var tr_content: std.ArrayList(conversation.ContentBlock) = .empty; + try tr_content.append(a, try textToolResult(a, "t1", "result")); + + var msgs = [_]Message{ + try userMsg(a, "q1"), + try asstMsg(a, "calling tool"), + .{ .role = .user, .content = tr_content }, + try asstMsg(a, "a1"), + try userMsg(a, "q2"), + try asstMsg(a, "a2"), + }; + defer freeMsgs(a, &msgs); + + // Budget small enough to keep only the last turn. The tool-result + // user message must NOT be mistaken for a turn boundary. Turn 1's + // final assistant (index 3) carries the cumulative input; the inner + // tool-round assistant (index 1) is subsumed by it. Turn 1 delta = + // 300; turn 2 delta = cumAt(a2) - cumAt(a1) = 500 - 300 = 200. + const usages = [_]?Usage{ + .{ .input = 0 }, .{ .input = 100, .output = 50 }, .{ .input = 0 }, .{ .input = 200, .output = 100 }, + .{ .input = 0 }, .{ .input = 400, .output = 100 }, + }; + const split = computeSplit(&msgs, &usages, 250); + // Kept suffix = q2/a2 turn at index 4. The whole 4-message first turn + // is summarized. + try testing.expectEqual(@as(usize, 4), split.prefix_end); + try testing.expectEqual(@as(usize, 1), split.kept_turns); +} + +test "computeSplit - consecutive user messages collapse into a single turn" { + const a = testing.allocator; + // Three plain user messages in a row open ONE turn, not three: only the + // first follows a non-user message; the others are continuations. + var msgs = [_]Message{ + try userMsg(a, "a"), + try userMsg(a, "b"), + try userMsg(a, "c"), + try asstMsg(a, "a1"), + try userMsg(a, "q2"), + try asstMsg(a, "a2"), + }; + defer freeMsgs(a, &msgs); + + // Whole conversation fits: nothing is summarized and the leading run of + // user messages counts as a single turn, so kept_turns == 2 (not 4). + { + const split = computeSplit(&msgs, null, 1_000_000); + try testing.expectEqual(@as(usize, 0), split.prefix_end); + try testing.expectEqual(@as(usize, 2), split.kept_turns); + } + + // With usage, the first turn's cumulative delta already accounts for + // a+b+c+a1, so the per-message fallback must not charge a/b a second + // time. Turn deltas: turn 1 = cumAt(a1) - 0 = 400; turn 2 = cumAt(a2) - + // cumAt(a1) = 600 - 400 = 200. Budget 250 keeps only turn 2 and + // summarizes the entire collapsed first turn, so the kept suffix begins + // at q2 (index 4). + { + const usages = [_]?Usage{ + .{ .input = 0 }, .{ .input = 0 }, .{ .input = 0 }, .{ .input = 300, .output = 100 }, + .{ .input = 0 }, .{ .input = 500, .output = 100 }, + }; + const split = computeSplit(&msgs, &usages, 250); + try testing.expectEqual(@as(usize, 4), split.prefix_end); + try testing.expectEqual(@as(usize, 1), split.kept_turns); + } +} + +test "computeSplit - active window starts after an existing compaction summary" { + const a = testing.allocator; + var msgs = [_]Message{ + try userMsg(a, "old q"), + try asstMsg(a, "old a"), + undefined, // compaction summary, filled below + try userMsg(a, "new q"), + try asstMsg(a, "new a"), + }; + var cs_content: std.ArrayList(conversation.ContentBlock) = .empty; + try cs_content.append(a, .{ .CompactionSummary = .{ + .text = try conversation.textualBlockFromSlice(a, "S1"), + } }); + msgs[2] = .{ .role = .user, .content = cs_content }; + defer freeMsgs(a, &msgs); + + // Large budget: the whole *active* window (new q / new a) fits, so + // nothing new is summarized. prefix_end points at first active turn. + const split = computeSplit(&msgs, null, 1_000_000); + try testing.expectEqual(@as(usize, 3), split.prefix_end); + try testing.expectEqual(@as(usize, 1), split.kept_turns); +} + +test "computeSplit - leading system messages are not turns" { + const a = testing.allocator; + var sys_content: std.ArrayList(conversation.ContentBlock) = .empty; + try sys_content.append(a, .{ .System = .{ + .text = try conversation.textualBlockFromSlice(a, "you are helpful"), + .mode = .append, + } }); + var msgs = [_]Message{ + .{ .role = .system, .content = sys_content }, + try userMsg(a, "q1"), + try asstMsg(a, "a1"), + }; + defer freeMsgs(a, &msgs); + + const split = computeSplit(&msgs, null, 1_000_000); + // First turn is index 1 (after the system message). + try testing.expectEqual(@as(usize, 1), split.prefix_end); + try testing.expectEqual(@as(usize, 1), split.kept_turns); +} + +test "computeSplit - regression: real cumulative session overflows keep_verbatim" { + // Reproduces a real session whose final assistant reported + // usage.input=28098, usage.output=5477 (total context 33,575). Earlier + // code treated cumulative `input` as a per-message cost and so either + // double-counted or (when usages were absent) word-counted below the + // budget, yielding a spurious "nothing to compact". With the + // turn-delta model this conversation must split at keep_verbatim=20000. + const a = testing.allocator; + // 12 user/assistant turns. User messages carry null usage; assistant + // messages carry the cumulative inputs observed on disk. + var msgs = [_]Message{ + try userMsg(a, "q1"), try asstMsg(a, "a1"), + try userMsg(a, "q2"), try asstMsg(a, "a2"), + try userMsg(a, "q3"), try asstMsg(a, "a3"), + try userMsg(a, "q4"), try asstMsg(a, "a4"), + try userMsg(a, "q5"), try asstMsg(a, "a5"), + try userMsg(a, "q6"), try asstMsg(a, "a6"), + try userMsg(a, "q7"), try asstMsg(a, "a7"), + try userMsg(a, "q8"), try asstMsg(a, "a8"), + try userMsg(a, "q9"), try asstMsg(a, "a9"), + try userMsg(a, "q10"), try asstMsg(a, "a10"), + try userMsg(a, "q11"), try asstMsg(a, "a11"), + try userMsg(a, "q12"), try asstMsg(a, "a12"), + }; + defer freeMsgs(a, &msgs); + + const U = struct { + fn mk(input: u64, output: u64) ?Usage { + return .{ .input = input, .output = output }; + } + }; + const usages = [_]?Usage{ + null, U.mk(1286, 118), + null, U.mk(2167, 62), + null, U.mk(2352, 146), + null, U.mk(9710, 153), + null, U.mk(10233, 180), + null, U.mk(12562, 127), + null, U.mk(13087, 110), + null, U.mk(14153, 68), + null, U.mk(16794, 838), + null, U.mk(17666, 228), + null, U.mk(24762, 203), + null, U.mk(28098, 5477), + }; + + const split = computeSplit(&msgs, &usages, 20_000); + // Something must be summarized: the split cannot sit at/before the + // first active turn (index 0). + try testing.expect(split.prefix_end > 0); + // The total context (33,575) exceeds the 20k budget, so at least the + // oldest turn(s) are shed and fewer than all 12 turns are kept. + try testing.expect(split.kept_turns < 12); +} + +test "serializeTranscript - labels roles and skips system" { + const a = testing.allocator; + var sys_content: std.ArrayList(conversation.ContentBlock) = .empty; + try sys_content.append(a, .{ .System = .{ + .text = try conversation.textualBlockFromSlice(a, "sys prompt"), + .mode = .append, + } }); + var msgs = [_]Message{ + .{ .role = .system, .content = sys_content }, + try userMsg(a, "hello"), + try asstMsg(a, "hi there"), + }; + defer freeMsgs(a, &msgs); + + const t = try serializeTranscript(a, &msgs); + defer a.free(t); + try testing.expect(std.mem.indexOf(u8, t, "sys prompt") == null); + try testing.expect(std.mem.indexOf(u8, t, "[User]: hello") != null); + try testing.expect(std.mem.indexOf(u8, t, "[Assistant]: hi there") != null); +} + +test "serializeTranscript - tool call and result framing" { + const a = testing.allocator; + var tu_content: std.ArrayList(conversation.ContentBlock) = .empty; + try tu_content.append(a, .{ .ToolUse = .{ + .id = try a.dupe(u8, "tc1"), + .name = try a.dupe(u8, "bash"), + .input = try conversation.textualBlockFromSlice(a, "{\"cmd\":\"ls\"}"), + } }); + var tr_content: std.ArrayList(conversation.ContentBlock) = .empty; + try tr_content.append(a, try textToolResult(a, "tc1", "file.txt")); + var msgs = [_]Message{ + .{ .role = .assistant, .content = tu_content }, + .{ .role = .user, .content = tr_content }, + }; + defer freeMsgs(a, &msgs); + + const t = try serializeTranscript(a, &msgs); + defer a.free(t); + try testing.expect(std.mem.indexOf(u8, t, "[Assistant tool call: bash (tc1)]: {\"cmd\":\"ls\"}") != null); + try testing.expect(std.mem.indexOf(u8, t, "[Tool result (tc1)]: file.txt") != null); +} + +test "buildRequestBody - without previous summary" { + const a = testing.allocator; + const body = try buildRequestBody(a, "TRANSCRIPT", null); + defer a.free(body); + try testing.expect(std.mem.indexOf(u8, body, "previous-summary") == null); + try testing.expect(std.mem.indexOf(u8, body, "<transcript>\nTRANSCRIPT\n</transcript>") != null); +} + +test "buildRequestBody - with previous summary" { + const a = testing.allocator; + const body = try buildRequestBody(a, "TRANSCRIPT", "PRIOR"); + defer a.free(body); + try testing.expect(std.mem.indexOf(u8, body, "<previous-summary>\nPRIOR\n</previous-summary>") != null); + try testing.expect(std.mem.indexOf(u8, body, "<transcript>\nTRANSCRIPT\n</transcript>") != null); +} + +test "latestSummaryText - returns latest, null when none" { + const a = testing.allocator; + { + var msgs = [_]Message{ try userMsg(a, "hi") }; + defer freeMsgs(a, &msgs); + try testing.expect(latestSummaryText(&msgs) == null); + } + { + var cs: std.ArrayList(conversation.ContentBlock) = .empty; + try cs.append(a, .{ .CompactionSummary = .{ + .text = try conversation.textualBlockFromSlice(a, "S2"), + } }); + var msgs = [_]Message{ + try userMsg(a, "hi"), + .{ .role = .user, .content = cs }, + }; + defer freeMsgs(a, &msgs); + try testing.expectEqualStrings("S2", latestSummaryText(&msgs).?); + } +} diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..f2f04e7 --- /dev/null +++ b/src/config.zig @@ -0,0 +1,403 @@ +//! Active configuration the agent consults on every turn. +//! +//! `ProviderConfig` is a tagged union keyed by `APIStyle`; each variant +//! carries the settings specific to one wire dialect. New providers add a +//! new tag and a new payload struct here; nothing else in libpanto needs a +//! central enum refresh. +//! +//! `Config` carries the active `ProviderConfig` (plus retry/compaction +//! policy). It is an **immutable snapshot**: the agent holds a `*const +//! Config` and re-reads it at the top of every turn, so swapping that +//! pointer (e.g. from a `panto.configure` hook) changes provider, model, +//! and base_url atomically at the next turn boundary. Because the snapshot +//! is read-only while a turn is in flight, concurrent tool workers reading +//! the old snapshot stay consistent. +//! +//! The tool set is **not** part of `Config` — it lives on the `Agent` +//! (`Agent.registerTool`/`registerToolSource`), so swapping provider/model +//! between turns no longer means rebuilding the tool list. + +const std = @import("std"); +const Io = std.Io; + +/// The wire dialect a provider speaks. +pub const APIStyle = enum { + openai_chat, + anthropic_messages, + /// OpenAI Responses API (`/responses`). Used by the ChatGPT-subscription + /// Codex backend, whose request/stream shape differs from Chat Completions. + openai_responses, + /// ChatGPT-subscription Codex Responses dialect. User config selects this + /// with `style = "openai_responses"` plus `dialect = "codex"`. + openai_codex_responses, +}; + +/// A single HTTP request header (name/value). Used for provider +/// `extra_headers` — caller-supplied headers merged onto a provider's +/// built-in request headers (e.g. GitHub Copilot's editor-identity headers, +/// or auth-derived headers from an OAuth exchange). Borrowed slices; valid +/// as long as the owning config is. +pub const Header = struct { + name: []const u8, + value: []const u8, +}; + +/// Reasoning intensity hint sent to providers that support it. +/// +/// `.default` omits the field entirely so the provider's own default applies. +/// `.off` sends `"none"` (supported by OpenRouter/NanoGPT; ignored or rejected +/// elsewhere). The remaining values mirror the `reasoning_effort` parameter +/// accepted by OpenAI reasoning models and OpenAI-compatible proxies. +pub const ReasoningEffort = enum { + default, + off, + minimal, + low, + medium, + high, +}; + +/// Anthropic extended-thinking mode. +pub const Thinking = enum { + /// Do not request extended thinking. + disabled, + /// Manual extended thinking with an explicit token budget (`thinking_budget_tokens`). + /// Supported on Haiku 4.5 and older models, but NOT on Opus 4.8+ (which + /// only accepts adaptive). Not safe as a cross-model default. + enabled, + /// Adaptive thinking: Claude decides when and how much to think. + /// Requires Opus 4.6 / Sonnet 4.6 or newer; automatic on Opus 4.7+. + adaptive, +}; + +/// Effort level sent with Anthropic adaptive thinking (`thinking = .adaptive`). +/// Ignored when `thinking` is `.enabled` or `.disabled`. +pub const Effort = enum { + low, + medium, + high, + xhigh, + max, +}; + +pub const OpenAIChatConfig = struct { + api_key: []const u8, + base_url: []const u8, + model: []const u8, + reasoning: ReasoningEffort = .default, + max_tokens: u32 = 64_000, + /// Caller-supplied request headers merged onto the built-in ones + /// (content-type/accept/authorization). Used for provider identity + /// headers and auth-exchange-derived headers. Empty by default. + /// Borrowed; valid as long as this config is. + extra_headers: []const Header = &.{}, +}; + +pub const AnthropicMessagesConfig = struct { + api_key: []const u8, + base_url: []const u8, + model: []const u8, + /// Value sent in the `anthropic-version` header. + api_version: []const u8 = "2023-06-01", + /// Required by Anthropic's Messages API. + max_tokens: u32 = 64_000, + /// Extended-thinking mode. `.enabled` sends a manual thinking block with + /// `thinking_budget_tokens`; `.adaptive` lets Claude decide and uses + /// `effort` instead. `.disabled` omits thinking entirely. + /// + /// Defaults to `.disabled`: it is the only mode accepted by every current + /// model. `.enabled` fails on Opus 4.8+ (adaptive-only) and `.adaptive` + /// fails on Haiku 4.5 (no adaptive support), so neither is a safe default + /// without parsing model strings. + thinking: Thinking = .disabled, + /// Effort level for adaptive thinking. Only emitted on the wire when + /// `thinking == .adaptive`; ignored otherwise. + effort: Effort = .medium, + /// Maximum tokens Claude may spend on internal reasoning when + /// `thinking == .enabled`. `null` falls back to `max_tokens - 1`. + /// Ignored when `thinking == .adaptive` or `.disabled`. + thinking_budget_tokens: ?u32 = 32_000, + /// When true and `thinking == .enabled`, sends the + /// `interleaved-thinking-2025-05-14` beta header so Claude can think + /// between tool calls. Ignored when `thinking == .adaptive` (interleaving + /// is automatic there) or `.disabled`. + thinking_interleaved: bool = false, + /// Use `Authorization: Bearer ...` instead of `x-api-key`. This is set + /// by the embedder from the configured auth family (not guessed from the + /// base URL): standard Anthropic uses `false`, while OAuth-backed + /// Anthropic-compatible providers such as Copilot set `true`. + use_bearer_auth: bool = false, + /// Place one `cache_control` breakpoint on the last cacheable block of + /// each request, replicating Anthropic's "automatic caching" (a single + /// advancing breakpoint) via the broadly-supported per-block marker + /// rather than the top-level field. + /// + /// Defaults on: for a normal append-only multi-turn session it's a + /// near-pure win (reads 0.1x base input vs. a one-time 1.25x write). + /// Set false when the workload makes that write premium unrecoverable + /// — e.g. one-shot requests, or an embedder that aggressively rewrites + /// history so prefixes are never reused. There the 1.25x write is pure + /// overhead with no read to amortize it. + prompt_cache: bool = true, + /// Caller-supplied request headers merged onto the built-in ones. See + /// `OpenAIChatConfig.extra_headers`. Empty by default. + extra_headers: []const Header = &.{}, +}; + +/// OpenAI Responses API config. Same transport knobs as `openai_chat`, but a +/// distinct request body (`input` items, `reasoning`, `store`, `include`) and +/// streaming event shape. Auth-derived headers (`chatgpt-account-id`) and the +/// static Codex identity headers (`OpenAI-Beta`, `originator`) ride on +/// `extra_headers`. +pub const OpenAIResponsesConfig = struct { + api_key: []const u8, + base_url: []const u8, + model: []const u8, + reasoning: ReasoningEffort = .default, + max_tokens: u32 = 64_000, + extra_headers: []const Header = &.{}, +}; + +/// Per-provider transport/auth/model configuration. Tagged by `APIStyle`. +pub const ProviderConfig = union(APIStyle) { + openai_chat: OpenAIChatConfig, + anthropic_messages: AnthropicMessagesConfig, + openai_responses: OpenAIResponsesConfig, + openai_codex_responses: OpenAIResponsesConfig, + + pub fn style(self: ProviderConfig) APIStyle { + return @as(APIStyle, self); + } + + /// The wire-format provider identity for this config: the ground-truth + /// `{api_style, base_url, model, reasoning}` that a turn is sent with. + /// Anthropic carries thinking/effort/budget/interleaved instead of + /// reasoning. Borrowed slices; valid as long as the config is. + pub fn wireIdentity(self: ProviderConfig) WireIdentity { + return switch (self) { + .openai_chat => |c| .{ + .api_style = .openai_chat, + .base_url = c.base_url, + .model = c.model, + .reasoning = c.reasoning, + }, + .anthropic_messages => |c| .{ + .api_style = .anthropic_messages, + .base_url = c.base_url, + .model = c.model, + .thinking = c.thinking, + .effort = c.effort, + .thinking_budget_tokens = c.thinking_budget_tokens, + .thinking_interleaved = c.thinking_interleaved, + }, + .openai_responses => |c| .{ + .api_style = .openai_responses, + .base_url = c.base_url, + .model = c.model, + .reasoning = c.reasoning, + }, + .openai_codex_responses => |c| .{ + .api_style = .openai_codex_responses, + .base_url = c.base_url, + .model = c.model, + .reasoning = c.reasoning, + }, + }; + } +}; + +/// Wire-format provider identity (see `ProviderConfig.wireIdentity`). This +/// is the same shape as `session_store.WireIdentity`; defined here to avoid +/// a module cycle (config must not import session_store). +/// +/// OpenAI uses `reasoning`; Anthropic uses `thinking`/`effort`/ +/// `thinking_budget_tokens`/`thinking_interleaved`. Fields unused by the +/// active provider carry their zero/default values. +pub const WireIdentity = struct { + api_style: APIStyle, + base_url: []const u8, + model: []const u8, + /// OpenAI only. + reasoning: ReasoningEffort = .default, + /// Anthropic only. + thinking: Thinking = .enabled, + /// Anthropic only; only meaningful when `thinking == .adaptive`. + effort: Effort = .medium, + /// Anthropic only; only meaningful when `thinking == .enabled`. + thinking_budget_tokens: ?u32 = 32_000, + /// Anthropic only; only meaningful when `thinking == .enabled`. + thinking_interleaved: bool = false, +}; + +/// Compaction settings the agent consults when summarizing old history. +/// +/// `keep_verbatim` is the budget (in tokens) governing how much recent +/// conversation is kept verbatim after compaction. The retention walk +/// accumulates whole turns backward and stops once the running total +/// exceeds this value, so `keep_verbatim` is an *upper bound* on the size +/// of the kept suffix (the turn that crosses the threshold is summarized, +/// not kept). +/// +/// `model`, when set, is the provider/model used to run the compaction +/// request itself. On failure (e.g. the compaction model rejects the +/// transcript for context length) the agent falls back to the active chat +/// model. When null, compaction uses the active chat model directly. +pub const CompactionConfig = struct { + keep_verbatim: u32 = 20_000, + model: ?ProviderConfig = null, + /// The compaction system prompt. Used both for automatic compaction on + /// context overflow and as the default for an explicit `Agent.compact` + /// call (which may override it per-call). Borrowed; set by the embedder + /// (e.g. resolved from its `COMPACTION.md` layers, or a built-in + /// default). When null, auto-compaction is disabled and a + /// context-overflow error propagates unchanged, and an explicit + /// `compact` with no override is a no-op error. + compaction_prompt: ?[]const u8 = null, +}; + +/// Policy for retrying transient provider/API failures. Conservative +/// defaults: four attempts (one initial + three retries) with exponential +/// backoff and jitter, capped at 10s per delay. +pub const RetryConfig = struct { + /// Total attempts including the first. `4` => initial try + up to 3 + /// retries. Must be >= 1. + max_attempts: usize = 4, + /// Base delay before the first retry, in milliseconds. + initial_delay_ms: u64 = 500, + /// Upper bound on any single backoff delay, in milliseconds. Also caps + /// a provider-supplied `Retry-After`. + max_delay_ms: u64 = 10_000, + /// Exponential growth factor applied per retry. + multiplier: f64 = 2.0, + /// When true, apply random jitter in `[0, computed_delay)` (full + /// jitter) to avoid thundering-herd retries. + jitter: bool = true, +}; + +/// An immutable snapshot of the provider/model the agent talks to, plus +/// retry and compaction policy. The agent holds a `*const Config` and +/// re-reads it each turn; replacing the pointer swaps the active +/// configuration at the next turn boundary. The tool set is owned by the +/// `Agent`, not the snapshot, so a swap never touches tools. +pub const Config = struct { + provider: ProviderConfig, + compaction: CompactionConfig = .{}, + retry: RetryConfig = .{}, + + pub fn style(self: Config) APIStyle { + return self.provider.style(); + } +}; + +// =========================================================================== +// Process-global HTTP client +// =========================================================================== +// +// `std.http.Client`'s connection pool is mutex-guarded and keyed by host, +// so a single client safely multiplexes every provider/base_url the agent +// ever switches to, across concurrent turns. We keep exactly one for the +// whole process: switching `base_url` simply leaves the old host's idle +// connections to time out (and reuses them if the user switches back). +// +// Embedders must call `initHttp` once before any turn and `deinitHttp` +// once at shutdown. + +var global_http: ?std.http.Client = null; + +/// Initialize the process-global HTTP client. Call once from the embedder's +/// `main()` before driving any agent turns. Idempotent: a second call with +/// an already-initialized client is a no-op. +pub fn initHttp(allocator: std.mem.Allocator, io: Io) void { + if (global_http != null) return; + global_http = .{ .allocator = allocator, .io = io }; +} + +/// Tear down the process-global HTTP client. Call once at shutdown, after +/// all turns have completed. +pub fn deinitHttp() void { + if (global_http) |*c| { + c.deinit(); + global_http = null; + } +} + +/// Borrow the process-global HTTP client. Asserts `initHttp` has run. +pub fn httpClient() *std.http.Client { + return &(global_http orelse @panic("libpanto: httpClient() called before initHttp()")); +} + +const t = std.testing; + +test "ProviderConfig - openai_chat variant" { + const cfg: ProviderConfig = .{ .openai_chat = .{ + .api_key = "sk-test", + .base_url = "https://api.openai.com/v1", + .model = "gpt-4o", + .reasoning = .high, + } }; + try t.expectEqual(APIStyle.openai_chat, cfg.style()); + try t.expectEqualStrings("sk-test", cfg.openai_chat.api_key); + try t.expectEqual(ReasoningEffort.high, cfg.openai_chat.reasoning); +} + +test "ProviderConfig - anthropic_messages variant" { + const cfg: ProviderConfig = .{ .anthropic_messages = .{ + .api_key = "sk-ant-test", + .base_url = "https://api.anthropic.com", + .model = "claude-sonnet-4-20250514", + } }; + try t.expectEqual(APIStyle.anthropic_messages, cfg.style()); + try t.expectEqualStrings("2023-06-01", cfg.anthropic_messages.api_version); + try t.expectEqual(@as(u32, 64_000), cfg.anthropic_messages.max_tokens); + try t.expectEqual(false, cfg.anthropic_messages.use_bearer_auth); + try t.expectEqual(Thinking.disabled, cfg.anthropic_messages.thinking); + try t.expectEqual(Effort.medium, cfg.anthropic_messages.effort); + try t.expectEqual(@as(?u32, 32_000), cfg.anthropic_messages.thinking_budget_tokens); + try t.expectEqual(false, cfg.anthropic_messages.thinking_interleaved); +} + +test "ProviderConfig - anthropic_messages wireIdentity carries thinking fields" { + const cfg: ProviderConfig = .{ .anthropic_messages = .{ + .api_key = "k", + .base_url = "https://api.anthropic.com", + .model = "claude-opus-4-8", + .thinking = .adaptive, + .effort = .high, + .thinking_budget_tokens = null, + .thinking_interleaved = false, + } }; + const id = cfg.wireIdentity(); + try t.expectEqual(APIStyle.anthropic_messages, id.api_style); + try t.expectEqual(Thinking.adaptive, id.thinking); + try t.expectEqual(Effort.high, id.effort); + try t.expectEqual(@as(?u32, null), id.thinking_budget_tokens); + try t.expectEqual(false, id.thinking_interleaved); + // reasoning field carries its zero default; not meaningful for Anthropic + try t.expectEqual(ReasoningEffort.default, id.reasoning); +} + +test "ProviderConfig - openai_chat reasoning defaults to .default" { + const cfg: ProviderConfig = .{ .openai_chat = .{ + .api_key = "k", + .base_url = "u", + .model = "m", + } }; + try t.expectEqual(ReasoningEffort.default, cfg.openai_chat.reasoning); +} + +test "Config carries provider and forwards style" { + const cfg: Config = .{ + .provider = .{ .openai_chat = .{ .api_key = "k", .base_url = "u", .model = "m" } }, + }; + try t.expectEqual(APIStyle.openai_chat, cfg.style()); +} + +test "global http client: init/borrow/deinit" { + var threaded: std.Io.Threaded = .init(t.allocator, .{}); + defer threaded.deinit(); + const io = threaded.io(); + initHttp(t.allocator, io); + defer deinitHttp(); + initHttp(t.allocator, io); // idempotent + _ = httpClient(); +} diff --git a/src/conversation.zig b/src/conversation.zig new file mode 100644 index 0000000..ea4309c --- /dev/null +++ b/src/conversation.zig @@ -0,0 +1,730 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const config = @import("config.zig"); + +/// A streaming text buffer used by content blocks. +/// Thin alias over ArrayList(u8) — amortized O(1) appends, +/// no O(n²) re-copying. +pub const TextualBlock = std.ArrayList(u8); + +/// Create a TextualBlock with initial content (copies the slice). +pub fn textualBlockFromSlice(alloc: Allocator, slice: []const u8) !TextualBlock { + var buf: TextualBlock = .empty; + try buf.appendSlice(alloc, slice); + return buf; +} + +/// Provenance for a replayable thinking signature. Scoped conservatively to +/// the exact provider endpoint + wire model that produced the enclosing +/// assistant message. +pub const SignatureOrigin = struct { + api_style: config.APIStyle, + base_url: []const u8, + model: []const u8, + + pub fn init( + alloc: Allocator, + api_style: config.APIStyle, + base_url: []const u8, + model: []const u8, + ) !SignatureOrigin { + const burl = try alloc.dupe(u8, base_url); + errdefer alloc.free(burl); + const mdl = try alloc.dupe(u8, model); + return .{ .api_style = api_style, .base_url = burl, .model = mdl }; + } + + pub fn deinit(self: *SignatureOrigin, alloc: Allocator) void { + alloc.free(self.base_url); + alloc.free(self.model); + } + + pub fn dupe(self: SignatureOrigin, alloc: Allocator) !SignatureOrigin { + return init(alloc, self.api_style, self.base_url, self.model); + } + + pub fn matches(self: SignatureOrigin, api_style: config.APIStyle, base_url: []const u8, model: []const u8) bool { + return self.api_style == api_style and + std.mem.eql(u8, self.base_url, base_url) and + std.mem.eql(u8, self.model, model); + } +}; + +/// A reasoning/thinking block from the assistant. +/// +/// `text` is the streamed reasoning content. `signature` is the opaque +/// integrity token Anthropic emits with extended-thinking responses and +/// requires echoed back verbatim on follow-up turns. It is optional because +/// other providers (OpenAI-compatible APIs) do not produce it. When present, +/// `signature_origin` records the exact provider/style/model that emitted the +/// enclosing assistant message so serializers can decide whether that opaque +/// signature is safe to replay on a later turn. +pub const ThinkingBlock = struct { + text: TextualBlock = .empty, + signature: ?[]const u8 = null, + signature_origin: ?SignatureOrigin = null, + + pub fn deinit(self: *ThinkingBlock, alloc: Allocator) void { + self.text.deinit(alloc); + if (self.signature) |sig| alloc.free(sig); + if (self.signature_origin) |*origin| origin.deinit(alloc); + } +}; + +pub const ToolUseBlock = struct { + id: []const u8, + name: []const u8, + input: TextualBlock = .empty, + + pub fn deinit(self: *ToolUseBlock, alloc: Allocator) void { + alloc.free(self.id); + alloc.free(self.name); + self.input.deinit(alloc); + } +}; + +/// A binary attachment stored in a tool result. `media_type` is owned +/// (e.g. "image/png"); `data` is the streaming text buffer holding +/// base64-encoded bytes. +pub const StoredMediaPart = struct { + media_type: []const u8, + data: TextualBlock = .empty, + + pub fn deinit(self: *StoredMediaPart, alloc: Allocator) void { + alloc.free(self.media_type); + self.data.deinit(alloc); + } +}; + +/// One part of a tool result as stored in the conversation. Text uses the +/// streaming `TextualBlock` form to preserve incremental-append semantics. +pub const ResultPartStored = union(enum) { + text: TextualBlock, + media: StoredMediaPart, + + pub fn deinit(self: *ResultPartStored, alloc: Allocator) void { + switch (self.*) { + .text => |*t| t.deinit(alloc), + .media => |*m| m.deinit(alloc), + } + } +}; + +pub const ToolResultBlock = struct { + tool_use_id: []const u8, + parts: std.ArrayList(ResultPartStored) = .empty, + /// True when this result reports a tool failure rather than success. + /// Serialized to providers that support it (Anthropic's `is_error`); + /// recorded but unserialized for OpenAI Chat (the error text in `parts` + /// carries the signal there). Defaults to false for backward + /// compatibility with sessions written before this field existed. + is_error: bool = false, + + pub fn deinit(self: *ToolResultBlock, alloc: Allocator) void { + alloc.free(self.tool_use_id); + for (self.parts.items) |*p| p.deinit(alloc); + self.parts.deinit(alloc); + } + + /// Concatenate all text parts into `out`. Media parts are skipped. + /// Used by callers (compaction, OpenAI fan-out) that want only the + /// textual portion. + pub fn appendTextInto(self: ToolResultBlock, alloc: Allocator, out: *TextualBlock) !void { + for (self.parts.items) |p| { + if (p == .text) try out.appendSlice(alloc, p.text.items); + } + } + + /// True if any part is a media attachment. + pub fn hasMedia(self: ToolResultBlock) bool { + for (self.parts.items) |p| { + if (p == .media) return true; + } + return false; + } +}; + +/// How a `.system` content block combines with the system text collected +/// before it. `append` adds to the running effective prompt; `replace` +/// discards everything collected so far and starts fresh from this block. +pub const SystemMode = enum { append, replace }; + +/// A system-prompt content block. System prompts remain `.system`-role +/// messages; this block records the mode that governs how its text folds +/// into the effective system prompt (see `effectiveSystemBlocks`). +pub const SystemBlock = struct { + text: TextualBlock = .empty, + mode: SystemMode = .append, + + pub fn deinit(self: *SystemBlock, alloc: Allocator) void { + self.text.deinit(alloc); + } +}; + +/// A compaction summary block. Carries the synthetic seed text that +/// stands in for a compacted conversation prefix. Like a `replace`-mode +/// `.System` block, it changes replay semantics: when the effective +/// conversation is rebuilt, a `.CompactionSummary` block resets all prior +/// *conversation* turns (user/assistant), and only the latest summary +/// plus the messages after it contribute to active context. System blocks +/// are unaffected — they are derived separately by `effectiveSystemBlocks` +/// and survive compaction untouched. +/// +/// A `.CompactionSummary` block sits alone in a `user`-role message. +pub const CompactionSummaryBlock = struct { + text: TextualBlock = .empty, + + pub fn deinit(self: *CompactionSummaryBlock, alloc: Allocator) void { + self.text.deinit(alloc); + } +}; + +pub const ContentBlock = union(enum) { + Text: TextualBlock, + Thinking: ThinkingBlock, + ToolUse: ToolUseBlock, + ToolResult: ToolResultBlock, + System: SystemBlock, + CompactionSummary: CompactionSummaryBlock, + + pub fn deinit(self: *ContentBlock, alloc: Allocator) void { + switch (self.*) { + .Text => |*b| b.deinit(alloc), + .Thinking => |*b| b.deinit(alloc), + .ToolUse => |*b| b.deinit(alloc), + .ToolResult => |*b| b.deinit(alloc), + .System => |*b| b.deinit(alloc), + .CompactionSummary => |*b| b.deinit(alloc), + } + } +}; + +pub const MessageRole = enum { + system, + user, + assistant, +}; + +/// Token usage reported by a provider for a single assistant turn. +/// +/// All input categories sum to the total prompt tokens billed for the turn: +/// `input + cache_read + cache_write` = total prompt tokens. +/// +/// Crucially, `input` is *cumulative*: it is the entire prompt sent for the +/// turn (the whole prior conversation), not the size of the turn's own +/// message. Compaction relies on this to back out per-turn sizes from +/// neighbor deltas (see `compaction.computeSplit`). +/// +/// `reasoning` is a **subset** of `output`, not additive — the portion of +/// the output spent on internal reasoning (OpenAI o-series, Anthropic +/// extended thinking). Cost is computed from `output` only; `reasoning` is +/// tracked for display. +/// +/// All fields are `u64` and default to 0. Providers that don't report a +/// given category leave it at 0. +/// +/// Defined here (rather than in `session.zig`) so in-memory `Message`s can +/// carry usage without a module cycle; `session.zig` re-exports it. +pub const Usage = struct { + /// Fresh input tokens billed at the model's base input rate. + input: u64 = 0, + /// Output tokens billed at the model's base output rate. + output: u64 = 0, + /// Input tokens served from cache (typ. 0.1× base input rate on + /// Anthropic, 0.5× on OpenAI). + cache_read: u64 = 0, + /// Input tokens written to a new cache entry (Anthropic only at + /// time of writing; typ. 1.25× base input rate). OpenAI's cache + /// hits don't bill a write premium, so this stays 0 there. + cache_write: u64 = 0, + /// Subset of `output` spent on reasoning. For OpenAI o-series + /// models and Anthropic extended thinking. Display-only — cost is + /// already accounted for via `output`. + reasoning: u64 = 0, +}; + +pub const Message = struct { + role: MessageRole, + content: std.ArrayList(ContentBlock) = .empty, + /// Provider-reported token usage for this message's turn. Set on + /// assistant messages (live, via the provider; replayed, from disk); + /// null on user/system messages and when the provider emitted no usage. + /// Used by compaction to size the retention window. + usage: ?Usage = null, + /// Opaque per-message metadata bag. `libpanto` never interprets these + /// bytes; the documented contract is that, when present, they are valid + /// JSON (so a store may keep them as a JSON column and tools may + /// deserialize them). Round-trips through persistence: set before a turn + /// commits, read back off the `Message` after `load`. Borrowed; owned by + /// whoever set it (the conversation's allocator on the replay path). + metadata: ?[]const u8 = null, + /// The wire-format provider identity (`api_style`/`base_url`/`model` and + /// the reasoning/thinking knobs) that actually produced this message. + /// Stamped once, when the message is first persisted, and from the disk + /// stamp on replay. Preserved verbatim through compaction's clone, so a + /// kept-verbatim turn keeps the identity of the model that generated it + /// rather than being re-stamped with the compaction model. This is the + /// per-message source of truth for thinking-signature replay — no need to + /// pin the identity onto each thinking block. When set, the `base_url` + /// and `model` slices are owned by the conversation's allocator. + identity: ?config.WireIdentity = null, + + pub fn deinit(self: *Message, alloc: Allocator) void { + for (self.content.items) |*block| { + block.deinit(alloc); + } + self.content.deinit(alloc); + if (self.metadata) |m| alloc.free(m); + if (self.identity) |id| freeWireIdentity(alloc, id); + } +}; + +/// Duplicate a `WireIdentity`'s owned slices (`base_url`, `model`) into +/// `alloc`; scalar fields are copied as-is. The result owns its strings and +/// must be released with `freeWireIdentity`. +pub fn dupeWireIdentity(alloc: Allocator, id: config.WireIdentity) !config.WireIdentity { + const burl = try alloc.dupe(u8, id.base_url); + errdefer alloc.free(burl); + const mdl = try alloc.dupe(u8, id.model); + var out = id; + out.base_url = burl; + out.model = mdl; + return out; +} + +/// Free the owned slices of a `WireIdentity` produced by `dupeWireIdentity`. +pub fn freeWireIdentity(alloc: Allocator, id: config.WireIdentity) void { + alloc.free(id.base_url); + alloc.free(id.model); +} + +pub const Conversation = struct { + messages: std.ArrayList(Message) = .empty, + allocator: Allocator, + + pub fn init(allocator: Allocator) Conversation { + return .{ + .allocator = allocator, + }; + } + + /// Append a system message in `append` mode. Adds to the effective + /// system prompt. (Back-compatible: same external behavior as before + /// the `.System` block existed.) + pub fn addSystemMessage(self: *Conversation, text: []const u8) !void { + return self.appendSystemBlock(text, .append); + } + + /// Append a system message in `replace` mode. When the effective + /// prompt is rebuilt (see `effectiveSystemBlocks`), this discards all + /// prior system text and starts fresh. + pub fn replaceSystemMessage(self: *Conversation, text: []const u8) !void { + return self.appendSystemBlock(text, .replace); + } + + /// Append a `.system`-role message whose single content block is a + /// `.System` block carrying `mode`. + fn appendSystemBlock(self: *Conversation, text: []const u8, mode: SystemMode) !void { + const tb = try textualBlockFromSlice(self.allocator, text); + var content: std.ArrayList(ContentBlock) = .empty; + errdefer { + for (content.items) |*b| b.deinit(self.allocator); + content.deinit(self.allocator); + } + try content.append(self.allocator, .{ .System = .{ .text = tb, .mode = mode } }); + try self.messages.append(self.allocator, .{ + .role = .system, + .content = content, + }); + } + + /// Append a `user`-role message from a slice of content blocks. + /// Symmetric with `addAssistantMessage`: ownership of the blocks (and + /// every byte they reference) transfers to the conversation; the caller + /// must not deinit them after this call. This is the general user-side + /// builder — a user turn may carry plain text, multiple text blocks + /// (e.g. messages queued while the agent was mid-turn), one or more + /// `.ToolResult` blocks, or any mix. For the common single-text case, + /// build a one-element `.{ .Text = ... }` slice (see the `addUserText` + /// test helpers across the codebase). + /// + /// Blocks must use this conversation's allocator for any owned bytes, + /// since the conversation will free them with that allocator. + pub fn addUserMessage(self: *Conversation, blocks: []const ContentBlock) !void { + return self.addMessage(.user, blocks, null); + } + + /// Append a `user`-role message carrying a single `.CompactionSummary` + /// block holding `text` (copied). This is the seed that stands in for + /// a compacted conversation prefix; see `CompactionSummaryBlock`. + pub fn addCompactionSummary(self: *Conversation, text: []const u8) !void { + const tb = try textualBlockFromSlice(self.allocator, text); + var content: std.ArrayList(ContentBlock) = .empty; + errdefer { + for (content.items) |*b| b.deinit(self.allocator); + content.deinit(self.allocator); + } + try content.append(self.allocator, .{ .CompactionSummary = .{ .text = tb } }); + try self.messages.append(self.allocator, .{ + .role = .user, + .content = content, + }); + } + + /// Append an assistant message, optionally tagged with its + /// provider-reported `usage` (pass `null` for none). Ownership of the + /// blocks is transferred to the conversation; the caller must not + /// deinit them after this call. + pub fn addAssistantMessage( + self: *Conversation, + blocks: []const ContentBlock, + usage: ?Usage, + ) !void { + return self.addMessage(.assistant, blocks, usage); + } + + /// Append a message of any role from a slice of content blocks, with + /// optional usage. The general form behind `addUserMessage` / + /// `addAssistantMessage`; used to rebuild a persisted message (tool calls, + /// thinking, mixed blocks) losslessly. Ownership of the blocks transfers + /// to the conversation; the caller must not deinit them after this call. + pub fn addMessage(self: *Conversation, role: MessageRole, blocks: []const ContentBlock, usage: ?Usage) !void { + var content: std.ArrayList(ContentBlock) = .empty; + try content.ensureTotalCapacity(self.allocator, blocks.len); + for (blocks) |block| { + content.appendAssumeCapacity(block); + } + try self.messages.append(self.allocator, .{ + .role = role, + .content = content, + .usage = usage, + }); + } + + pub fn deinit(self: *Conversation) void { + for (self.messages.items) |*msg| { + msg.deinit(self.allocator); + } + self.messages.deinit(self.allocator); + } +}; + +/// Whether a thinking block's opaque signature may be replayed to a request +/// targeting `(api_style, base_url, model)`. A signature is portable only +/// back to the exact endpoint/model that produced it; replaying it elsewhere +/// is rejected (Anthropic) or meaningless (OpenAI encrypted reasoning). +/// +/// Provenance is resolved per block first (`signature_origin`, set live by +/// the producing provider), then falls back to the enclosing message's +/// `identity` (the per-message source of truth that survives compaction). A +/// block with no signature, or with no provenance from either source, is not +/// replayable — sending a signature to an unverified endpoint is unsafe. +pub fn thinkingSignatureMatches( + block: ThinkingBlock, + msg_identity: ?config.WireIdentity, + api_style: config.APIStyle, + base_url: []const u8, + model: []const u8, +) bool { + if (block.signature == null) return false; + if (block.signature_origin) |origin| { + return origin.matches(api_style, base_url, model); + } + if (msg_identity) |id| { + return id.api_style == api_style and + std.mem.eql(u8, id.base_url, base_url) and + std.mem.eql(u8, id.model, model); + } + return false; +} + +pub fn setThinkingOrigins( + allocator: Allocator, + blocks: []ContentBlock, + api_style: config.APIStyle, + base_url: []const u8, + model: []const u8, +) !void { + for (blocks) |*block| { + if (block.* != .Thinking) continue; + const origin = try SignatureOrigin.init(allocator, api_style, base_url, model); + if (block.Thinking.signature_origin) |*old| old.deinit(allocator); + block.Thinking.signature_origin = origin; + } +} + +/// Derive the effective ordered list of system-text blocks from a slice of +/// messages. This is the single shared rule that governs both provider +/// serialization and session rebuild. +/// +/// Walk the messages in order; for each `.system` message's `.System` +/// block: +/// - `append`: add the block's text to the running list. +/// - `replace`: clear the running list, then add this block's text. +/// +/// The returned slices are **borrowed** from `messages` — valid only as +/// long as the underlying conversation is unmodified. The caller owns the +/// returned `ArrayList` itself and must `deinit` it (this frees the slice +/// storage, not the borrowed text). +/// +/// Running this walk over a *prefix* of the messages reconstructs the +/// effective prompt as of that point — the `/tree` faithfulness property. +pub fn effectiveSystemBlocks( + alloc: Allocator, + messages: []const Message, +) !std.ArrayList([]const u8) { + var out: std.ArrayList([]const u8) = .empty; + errdefer out.deinit(alloc); + for (messages) |msg| { + if (msg.role != .system) continue; + for (msg.content.items) |block| { + switch (block) { + .System => |sb| { + if (sb.mode == .replace) out.clearRetainingCapacity(); + try out.append(alloc, sb.text.items); + }, + // Be tolerant of plain `.Text` blocks on a system message + // (e.g. hand-built test conversations): treat them as + // append-mode text. + .Text => |tb| try out.append(alloc, tb.items), + else => {}, + } + } + } + return out; +} + +/// Index of the message carrying the latest `.CompactionSummary` block, +/// or null if the conversation has never been compacted. +/// +/// Compaction replay is reset-like: only the latest compaction summary and +/// the messages after it contribute to active conversation context. This +/// returns the anchor message (the summary itself); active conversation is +/// `messages[anchor..]`. System messages are unaffected and are derived +/// independently via `effectiveSystemBlocks`. +pub fn latestCompactionIndex(messages: []const Message) ?usize { + var anchor: ?usize = null; + for (messages, 0..) |msg, i| { + for (msg.content.items) |block| { + if (block == .CompactionSummary) { + anchor = i; + break; + } + } + } + return anchor; +} + +/// The active (post-compaction) conversation message window that should be +/// sent to a provider. If the conversation has been compacted, this is the +/// latest compaction summary message plus everything after it; otherwise +/// it is the whole message list. System messages within the window are +/// still emitted/handled by the caller's own role filtering — this only +/// trims the *prefix* superseded by compaction. +/// +/// The returned slice borrows from `messages`. +pub fn activeMessageWindow(messages: []const Message) []const Message { + if (latestCompactionIndex(messages)) |anchor| { + return messages[anchor..]; + } + return messages; +} + +/// Test helper: append a single-text user message. `addUserMessage` takes +/// a block slice; this wraps the plain-text case the tests below use. +fn addUserText(conv: *Conversation, text: []const u8) !void { + const tb = try textualBlockFromSlice(conv.allocator, text); + var block: ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + +test "Conversation - add messages and verify content" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("You are a helpful assistant."); + try addUserText(&conv, "Hello!"); + try conv.addAssistantMessage(&.{ + .{ .Text = try textualBlockFromSlice(allocator, "Hi there!") }, + }, null); + + try std.testing.expectEqual(@as(usize, 3), conv.messages.items.len); + + try std.testing.expectEqual(MessageRole.system, conv.messages.items[0].role); + try std.testing.expectEqualStrings( + "You are a helpful assistant.", + conv.messages.items[0].content.items[0].System.text.items, + ); + try std.testing.expectEqual( + SystemMode.append, + conv.messages.items[0].content.items[0].System.mode, + ); + + try std.testing.expectEqual(MessageRole.user, conv.messages.items[1].role); + try std.testing.expectEqualStrings("Hello!", conv.messages.items[1].content.items[0].Text.items); + + try std.testing.expectEqual(MessageRole.assistant, conv.messages.items[2].role); + try std.testing.expectEqualStrings("Hi there!", conv.messages.items[2].content.items[0].Text.items); +} + +test "TextualBlock - incremental append" { + const allocator = std.testing.allocator; + + var tb = TextualBlock.empty; + defer tb.deinit(allocator); + + try tb.appendSlice(allocator, "Hello"); + try tb.appendSlice(allocator, " world"); + try std.testing.expectEqualStrings("Hello world", tb.items); +} + +test "Conversation - deinit frees without leaks" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + try conv.addSystemMessage("system"); + try addUserText(&conv, "user message"); + try conv.addAssistantMessage(&.{ + .{ .Text = try textualBlockFromSlice(allocator, "response") }, + }, null); + conv.deinit(); +} + +test "ContentBlock - Thinking variant" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ .text = try textualBlockFromSlice(allocator, "hmm...") } }, + .{ .Text = try textualBlockFromSlice(allocator, "answer") }, + }, null); + + try std.testing.expectEqual(@as(usize, 2), conv.messages.items[0].content.items.len); + try std.testing.expectEqualStrings("hmm...", conv.messages.items[0].content.items[0].Thinking.text.items); + try std.testing.expectEqualStrings("answer", conv.messages.items[0].content.items[1].Text.items); +} + +test "System block - addSystemMessage records append mode, replaceSystemMessage records replace mode" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("base"); + try conv.replaceSystemMessage("fresh"); + + try std.testing.expectEqual(SystemMode.append, conv.messages.items[0].content.items[0].System.mode); + try std.testing.expectEqualStrings("base", conv.messages.items[0].content.items[0].System.text.items); + try std.testing.expectEqual(SystemMode.replace, conv.messages.items[1].content.items[0].System.mode); + try std.testing.expectEqualStrings("fresh", conv.messages.items[1].content.items[0].System.text.items); +} + +test "effectiveSystemBlocks - append accumulates in order" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("a"); + try conv.addSystemMessage("b"); + try addUserText(&conv, "hi"); + try conv.addSystemMessage("c"); + + var blocks = try effectiveSystemBlocks(allocator, conv.messages.items); + defer blocks.deinit(allocator); + + try std.testing.expectEqual(@as(usize, 3), blocks.items.len); + try std.testing.expectEqualStrings("a", blocks.items[0]); + try std.testing.expectEqualStrings("b", blocks.items[1]); + try std.testing.expectEqualStrings("c", blocks.items[2]); +} + +test "effectiveSystemBlocks - replace wipes everything collected so far" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("a"); + try conv.addSystemMessage("b"); + try conv.replaceSystemMessage("fresh"); + try conv.addSystemMessage("after"); + + var blocks = try effectiveSystemBlocks(allocator, conv.messages.items); + defer blocks.deinit(allocator); + + try std.testing.expectEqual(@as(usize, 2), blocks.items.len); + try std.testing.expectEqualStrings("fresh", blocks.items[0]); + try std.testing.expectEqualStrings("after", blocks.items[1]); +} + +test "effectiveSystemBlocks - prefix reconstructs prompt as of that point" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("a"); + try conv.replaceSystemMessage("fresh"); + try conv.addSystemMessage("after"); + + // Truncate at position 1 (only the first `addSystemMessage`). + var blocks = try effectiveSystemBlocks(allocator, conv.messages.items[0..1]); + defer blocks.deinit(allocator); + try std.testing.expectEqual(@as(usize, 1), blocks.items.len); + try std.testing.expectEqualStrings("a", blocks.items[0]); +} + +test "addCompactionSummary - sits alone in a user message" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try addUserText(&conv, "hi"); + try conv.addCompactionSummary("summary of earlier history"); + + const m = conv.messages.items[1]; + try std.testing.expectEqual(MessageRole.user, m.role); + try std.testing.expectEqual(@as(usize, 1), m.content.items.len); + try std.testing.expectEqualStrings( + "summary of earlier history", + m.content.items[0].CompactionSummary.text.items, + ); +} + +test "latestCompactionIndex - null when never compacted" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("sys"); + try addUserText(&conv, "hi"); + try conv.addAssistantMessage(&.{ + .{ .Text = try textualBlockFromSlice(allocator, "hello") }, + }, null); + + try std.testing.expect(latestCompactionIndex(conv.messages.items) == null); +} + +test "latestCompactionIndex - returns the latest summary anchor" { + const allocator = std.testing.allocator; + + var conv = Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("sys"); + try addUserText(&conv, "old"); + try conv.addCompactionSummary("S1"); + try addUserText(&conv, "mid"); + try conv.addCompactionSummary("S2"); // index 4 + try addUserText(&conv, "recent"); + + try std.testing.expectEqual(@as(?usize, 4), latestCompactionIndex(conv.messages.items)); +} diff --git a/src/file_system_jsonl_store.zig b/src/file_system_jsonl_store.zig new file mode 100644 index 0000000..9bc9436 --- /dev/null +++ b/src/file_system_jsonl_store.zig @@ -0,0 +1,2103 @@ +//! Session lifecycle: create, open, replay, append. +//! +//! Backed by an append-only JSONL file on disk. The on-disk types live in +//! `session.zig`. This module owns: +//! +//! - Path resolution (sessions dir is supplied by the caller; we own the +//! filename and writes within it). +//! - The in-memory entry index (`by_id` map + leaf pointer). +//! - Deferred file creation: the file is not written until the first +//! assistant message persists. Until that point, all entries are +//! buffered in memory. +//! - Append semantics: once flushed, every completed entry is written +//! and synced to disk immediately. +//! - Crash recovery: on open, the file is parsed line-by-line; the first +//! line that fails to parse causes everything from that line onward to +//! be truncated from the file. +//! - One-time format migration when a future version reads a v1 file +//! (currently a no-op; the hook is in place). +//! - Rebuilding a `Conversation` from the entry tree, plus determining +//! the active provider/model. +//! +//! The library-vs-CLI boundary: callers pass an absolute path to the +//! per-cwd sessions directory. We compute the per-session filename +//! ourselves (`<uuidv7>.jsonl`) and lazily mkdir the directory on the +//! first flush. The CLI owns XDG resolution, encoded-cwd grouping, and +//! the `--resume` flag plumbing. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Io = std.Io; + +const session_mod = @import("session.zig"); +const conversation_mod = @import("conversation.zig"); +const session_store_mod = @import("session_store.zig"); +const turn_persist = @import("turn_persist.zig"); + +pub const SessionHeader = session_mod.SessionHeader; +pub const SessionEntry = session_mod.SessionEntry; +pub const MessageEntry = session_mod.MessageEntry; +pub const StoredMessage = session_mod.StoredMessage; +pub const StoredMessageRole = session_mod.StoredMessageRole; +pub const StoredSystemMode = session_mod.StoredSystemMode; +pub const StoredContentBlock = session_mod.StoredContentBlock; +pub const Usage = session_mod.Usage; +pub const CURRENT_VERSION = session_mod.CURRENT_VERSION; + +// ============================================================================= +// IDs and timestamps +// ============================================================================= + +/// Generate a UUIDv7 (RFC 9562 §5.7). Returns a 36-character canonical +/// hex string with hyphens. Caller owns. +/// +/// Layout: +/// - 48 bits: unix_ts_ms (big-endian) +/// - 4 bits: version (7) +/// - 12 bits: random +/// - 2 bits: variant (10) +/// - 62 bits: random +pub fn newUuidV7(allocator: Allocator, io: Io) ![]u8 { + const ts = Io.Timestamp.now(io, .real); + const now_ms: u64 = @intCast(@max(ts.toMilliseconds(), 0)); + + var rand_bytes: [10]u8 = undefined; + io.random(&rand_bytes); + + var b: [16]u8 = undefined; + // Timestamp (48 bits, big-endian). + b[0] = @intCast((now_ms >> 40) & 0xFF); + b[1] = @intCast((now_ms >> 32) & 0xFF); + b[2] = @intCast((now_ms >> 24) & 0xFF); + b[3] = @intCast((now_ms >> 16) & 0xFF); + b[4] = @intCast((now_ms >> 8) & 0xFF); + b[5] = @intCast(now_ms & 0xFF); + // Version (4 high bits = 0x7) + 12 bits random. + b[6] = 0x70 | (rand_bytes[0] & 0x0F); + b[7] = rand_bytes[1]; + // Variant (2 high bits = 10) + 62 bits random. + b[8] = 0x80 | (rand_bytes[2] & 0x3F); + b[9] = rand_bytes[3]; + b[10] = rand_bytes[4]; + b[11] = rand_bytes[5]; + b[12] = rand_bytes[6]; + b[13] = rand_bytes[7]; + b[14] = rand_bytes[8]; + b[15] = rand_bytes[9]; + + return try std.fmt.allocPrint( + allocator, + "{x:0>2}{x:0>2}{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}{x:0>2}{x:0>2}{x:0>2}{x:0>2}", + .{ b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15] }, + ); +} + +/// Generate a fresh 8-character hex entry id. Caller owns. +fn newEntryIdInto(buf: []u8, io: Io) void { + std.debug.assert(buf.len == 8); + var bytes: [4]u8 = undefined; + io.random(&bytes); + _ = std.fmt.bufPrint(buf, "{x:0>2}{x:0>2}{x:0>2}{x:0>2}", .{ bytes[0], bytes[1], bytes[2], bytes[3] }) catch unreachable; +} + +/// Format `now` as an ISO 8601 UTC string with millisecond precision. +/// Example: `2026-04-25T17:40:15.990Z`. Caller owns. +pub fn isoTimestamp(allocator: Allocator, io: Io) ![]u8 { + const ts = Io.Timestamp.now(io, .real); + const ms_total: i64 = ts.toMilliseconds(); + const seconds_total: i64 = @divTrunc(ms_total, 1000); + const ms: u64 = @intCast(@mod(ms_total, 1000)); + + const epoch_secs = std.time.epoch.EpochSeconds{ .secs = @intCast(seconds_total) }; + const epoch_day = epoch_secs.getEpochDay(); + const day_secs = epoch_secs.getDaySeconds(); + const year_day = epoch_day.calculateYearDay(); + const month_day = year_day.calculateMonthDay(); + + return try std.fmt.allocPrint( + allocator, + "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>3}Z", + .{ + @as(u32, year_day.year), + month_day.month.numeric(), + @as(u32, month_day.day_index) + 1, + day_secs.getHoursIntoDay(), + day_secs.getMinutesIntoHour(), + day_secs.getSecondsIntoMinute(), + ms, + }, + ); +} + +// ============================================================================= +// FileInfo (per-file listing scan result) +// ============================================================================= + +/// Internal scan result for one session file. Carries the file `path` (the +/// catalog needs it to open/resolve) plus everything the public +/// `session_store.SessionInfo` needs. +const FileInfo = struct { + path: []u8, + id: []u8, + created: []u8, // ISO 8601 from header timestamp + modified: []u8, // ISO 8601 from last activity, falling back to header + message_count: usize, + last_user_message: []u8, + stamp: ?session_mod.WireStamp, // last-used wire identity + + pub fn deinit(self: FileInfo, alloc: Allocator) void { + alloc.free(self.path); + alloc.free(self.id); + alloc.free(self.created); + alloc.free(self.modified); + alloc.free(self.last_user_message); + if (self.stamp) |s| s.deinit(alloc); + } +}; + +// ============================================================================= +// SessionFile +// ============================================================================= + +pub const Error = error{ + NoSessionsFound, + AmbiguousSessionId, + SessionNotFound, + InvalidSessionFile, +} || Allocator.Error || Io.Cancelable; + +pub const SessionFile = struct { + allocator: Allocator, + io: Io, + + /// Absolute path to the per-cwd sessions directory. Lazily created. + session_dir: []u8, + /// Absolute path to the file we *will* write to (computed at init). + /// May not yet exist on disk if `flushed = false`. + session_file: []u8, + + /// Header. Allocated at init for new sessions; reloaded from the file + /// on resume. + header: SessionHeader, + + /// Entries indexed in insertion order. The first entry's `parent_id` + /// is null; each subsequent entry's `parent_id` points to its parent + /// (currently always the previous entry). + entries: std.ArrayList(SessionEntry), + /// id → entry index in `entries`. Used both for parent-id lookups + /// and for collision detection in `newEntryId`. + by_id: std.StringHashMap(usize), + /// id of the most recently appended entry, or null if no entries yet. + /// Borrowed from the entry; do not free. + leaf_id: ?[]const u8, + + /// True once the file exists on disk. False during the "buffered" + /// pre-assistant phase. See module-level docs. + flushed: bool, + /// Number of bytes written to `session_file` so far. Used as the + /// offset for the next positional write. Only meaningful when + /// `flushed = true`. + written_bytes: u64, + + // ---------- Construction ---------- + + /// Create a new session in memory. Allocates a UUIDv7, computes the + /// file path, but does NOT touch the filesystem. The file is created + /// on the first assistant-message flush. + /// + /// `session_dir` is duplicated; the caller retains ownership of the + /// passed slice. + pub fn init( + allocator: Allocator, + io: Io, + session_dir: []const u8, + metadata: ?[]const u8, + ) !SessionFile { + const id = try newUuidV7(allocator, io); + defer allocator.free(id); + return initWithId(allocator, io, session_dir, metadata, id); + } + + /// Like `init`, but uses a caller-supplied session id (duped here) + /// rather than minting a fresh UUIDv7. Used by the catalog when a + /// `Session` handle was minted (with its id) before the first append. + pub fn initWithId( + allocator: Allocator, + io: Io, + session_dir: []const u8, + metadata: ?[]const u8, + session_id: []const u8, + ) !SessionFile { + const dir = try allocator.dupe(u8, session_dir); + errdefer allocator.free(dir); + + const id = try allocator.dupe(u8, session_id); + errdefer allocator.free(id); + + const timestamp = try isoTimestamp(allocator, io); + errdefer allocator.free(timestamp); + + const metadata_copy: ?[]const u8 = if (metadata) |m| try allocator.dupe(u8, m) else null; + errdefer if (metadata_copy) |m| allocator.free(m); + + const filename = try std.fmt.allocPrint(allocator, "{s}.jsonl", .{id}); + defer allocator.free(filename); + const file_path = try std.fs.path.join(allocator, &.{ dir, filename }); + errdefer allocator.free(file_path); + + return .{ + .allocator = allocator, + .io = io, + .session_dir = dir, + .session_file = file_path, + .header = .{ + .version = CURRENT_VERSION, + .id = id, + .timestamp = timestamp, + .metadata = metadata_copy, + }, + .entries = .empty, + .by_id = std.StringHashMap(usize).init(allocator), + .leaf_id = null, + .flushed = false, + .written_bytes = 0, + }; + } + + /// Open and replay an existing session file. Truncates from the first + /// corrupted line. Runs format migration if needed and rewrites the + /// file once. + pub fn open( + allocator: Allocator, + io: Io, + file_path: []const u8, + ) !SessionFile { + const path_copy = try allocator.dupe(u8, file_path); + errdefer allocator.free(path_copy); + + // The session dir is the file's parent directory. + const dir_path = std.fs.path.dirname(path_copy) orelse "."; + const dir = try allocator.dupe(u8, dir_path); + errdefer allocator.free(dir); + + const bytes = try readWholeFile(allocator, io, path_copy); + defer allocator.free(bytes); + + // Walk line-by-line. The first failure causes a truncation back to + // the start of that line. + var entries: std.ArrayList(SessionEntry) = .empty; + errdefer { + for (entries.items) |e| e.deinit(allocator); + entries.deinit(allocator); + } + var by_id = std.StringHashMap(usize).init(allocator); + errdefer by_id.deinit(); + + var header_opt: ?SessionHeader = null; + errdefer if (header_opt) |h| h.deinit(allocator); + + var cursor: usize = 0; + var valid_bytes: u64 = 0; // length of the file prefix that parses cleanly + var saw_corruption: bool = false; + + while (cursor < bytes.len) { + // Find the next newline (or EOF). + const rest = bytes[cursor..]; + const nl_rel = std.mem.indexOfScalar(u8, rest, '\n'); + const line_end_excl: usize = if (nl_rel) |n| cursor + n else bytes.len; + const line = bytes[cursor..line_end_excl]; + const next_cursor: usize = if (nl_rel != null) line_end_excl + 1 else bytes.len; + + // Allow blank lines silently (just whitespace), but a non-empty + // trimmed line that won't parse triggers truncation. + const trimmed = std.mem.trim(u8, line, " \t\r"); + if (trimmed.len == 0) { + if (nl_rel == null) break; + cursor = next_cursor; + valid_bytes = cursor; + continue; + } + + // If the final line has no trailing newline AND we hit EOF, it + // is presumed truncated mid-write. Treat as corruption. + if (nl_rel == null) { + saw_corruption = true; + break; + } + + const fe = session_mod.parseLine(allocator, line) catch { + saw_corruption = true; + break; + }; + + switch (fe) { + .header => |h| { + if (header_opt != null) { + // Two headers — treat as corruption from this line on. + h.deinit(allocator); + saw_corruption = true; + break; + } + if (entries.items.len != 0) { + // Header arrived after entries — malformed. + h.deinit(allocator); + saw_corruption = true; + break; + } + header_opt = h; + }, + .entry => |e| { + const idx = entries.items.len; + entries.append(allocator, e) catch |err| { + e.deinit(allocator); + return err; + }; + by_id.put(e.base().id, idx) catch |err| { + // Rolling back the append is awkward; in practice + // OOM here is fatal anyway. + return err; + }; + }, + } + + cursor = next_cursor; + valid_bytes = cursor; + } + + // No header at all — refuse to load. + const header = header_opt orelse return error.InvalidSessionFile; + + // Truncate the file if anything beyond `valid_bytes` is corrupt. + if (saw_corruption and valid_bytes < bytes.len) { + try truncateFileTo(io, path_copy, valid_bytes); + } + + // Drop assistant tool_use blocks left dangling (no matching + // tool_result) by an interrupted prior turn. If anything changed, + // rebuild the id index and rewrite the file once. + if (elideDanglingToolUses(allocator, &entries)) { + by_id.clearRetainingCapacity(); + for (entries.items, 0..) |e, i| { + try by_id.put(e.base().id, i); + } + try rewriteFile(allocator, io, path_copy, header, entries.items); + } + + const leaf_id: ?[]const u8 = if (entries.items.len > 0) + entries.items[entries.items.len - 1].base().id + else + null; + + // Compute final file length on disk so future appends use the + // correct offset. + const stat = try statFileForLength(io, path_copy); + + return .{ + .allocator = allocator, + .io = io, + .session_dir = dir, + .session_file = path_copy, + .header = header, + .entries = entries, + .by_id = by_id, + .leaf_id = leaf_id, + .flushed = true, + .written_bytes = stat, + }; + } + + pub fn deinit(self: *SessionFile) void { + self.header.deinit(self.allocator); + for (self.entries.items) |e| e.deinit(self.allocator); + self.entries.deinit(self.allocator); + self.by_id.deinit(); + self.allocator.free(self.session_dir); + self.allocator.free(self.session_file); + } + + // ---------- Accessors ---------- + + pub fn getSessionFile(self: *const SessionFile) []const u8 { + return self.session_file; + } + + pub fn isFlushed(self: *const SessionFile) bool { + return self.flushed; + } + + // ---------- Appending ---------- + + /// Append a single message, returning its entry id. A thin wrapper over + /// `appendMessagesAtomic` (which handles `len == 1`); used by the tests. + /// `msg` is consumed (ownership transferred). + pub fn appendMessage( + self: *SessionFile, + msg: StoredMessage, + // Wire-format provider identity for the entry. Null on system + // messages. Borrowed; duplicated into the entry. + stamp: ?session_mod.WireStamp, + ) ![]const u8 { + var msgs = [_]StoredMessage{msg}; + const stamps = [_]?session_mod.WireStamp{stamp}; + try self.appendMessagesAtomic(&msgs, &stamps); + return self.leaf_id.?; + } + + /// Returns a freshly allocated 8-character hex id, guaranteed not to + /// collide with any existing entry id in this session. + fn newEntryId(self: *SessionFile) ![]u8 { + const max_tries = 100; + var i: usize = 0; + while (i < max_tries) : (i += 1) { + const buf = try self.allocator.alloc(u8, 8); + errdefer self.allocator.free(buf); + newEntryIdInto(buf[0..8], self.io); + if (!self.by_id.contains(buf)) { + return buf; + } + self.allocator.free(buf); + } + // Fall back to a UUID prefix if 100 retries all collided. With 4 + // random bytes per id and a session with <<2^16 entries, the + // probability of getting here is effectively zero, but we want a + // hard guarantee. + const long = try newUuidV7(self.allocator, self.io); + defer self.allocator.free(long); + const buf = try self.allocator.alloc(u8, 8); + @memcpy(buf, long[0..8]); + return buf; + } + + // ---------- Persistence ---------- + + /// Write the header + all currently-buffered entries + `new_entries` + /// to the file as a single batch. Creates the directory and file. + fn flushBufferedMany(self: *SessionFile, new_entries: []const SessionEntry) !void { + try mkdirP(self.io, self.session_dir); + + const file = try Io.Dir.cwd().createFile(self.io, self.session_file, .{ + .truncate = true, + .read = false, + }); + defer file.close(self.io); + + var offset: u64 = 0; + const header_line = try session_mod.serializeHeader(self.allocator, self.header); + defer self.allocator.free(header_line); + try file.writePositionalAll(self.io, header_line, offset); + offset += header_line.len; + try file.writePositionalAll(self.io, "\n", offset); + offset += 1; + + for (self.entries.items) |e| { + const line = try session_mod.serializeEntry(self.allocator, e); + defer self.allocator.free(line); + try file.writePositionalAll(self.io, line, offset); + offset += line.len; + try file.writePositionalAll(self.io, "\n", offset); + offset += 1; + } + + for (new_entries) |entry| { + const line = try session_mod.serializeEntry(self.allocator, entry); + defer self.allocator.free(line); + try file.writePositionalAll(self.io, line, offset); + offset += line.len; + try file.writePositionalAll(self.io, "\n", offset); + offset += 1; + } + + file.sync(self.io) catch {}; + self.flushed = true; + self.written_bytes = offset; + } + + pub fn appendMessagesAtomic( + self: *SessionFile, + messages: []StoredMessage, + stamps: []const ?session_mod.WireStamp, + ) !void { + std.debug.assert(messages.len == stamps.len); + if (messages.len == 0) return; + + const base_len = self.entries.items.len; + try self.entries.ensureUnusedCapacity(self.allocator, messages.len); + try self.by_id.ensureUnusedCapacity(@intCast(messages.len)); + + var entries = try self.allocator.alloc(SessionEntry, messages.len); + defer self.allocator.free(entries); + var built: usize = 0; + errdefer { + for (entries[0..built]) |*e| e.deinit(self.allocator); + } + + var prev_leaf = self.leaf_id; + for (messages, 0..) |msg, i| { + const msg_local = msg; + const id_buf = try self.newEntryId(); + errdefer self.allocator.free(id_buf); + const timestamp = try isoTimestamp(self.allocator, self.io); + errdefer self.allocator.free(timestamp); + const parent_id_copy: ?[]const u8 = if (prev_leaf) |l| try self.allocator.dupe(u8, l) else null; + errdefer if (parent_id_copy) |p| self.allocator.free(p); + const stamp_copy: ?session_mod.WireStamp = if (stamps[i]) |st| try st.dupe(self.allocator) else null; + errdefer if (stamp_copy) |st| st.deinit(self.allocator); + entries[i] = .{ .message = .{ + .base = .{ .id = id_buf, .parent_id = parent_id_copy, .timestamp = timestamp }, + .stamp = stamp_copy, + .message = msg_local, + } }; + built += 1; + prev_leaf = entries[i].base().id; + } + + if (self.flushed) { + try self.persistEntries(entries); + } else { + // File is created on the first assistant message (see module docs). + for (messages) |m| if (m.role == .assistant) { + try self.flushBufferedMany(entries); + break; + }; + } + + for (entries, 0..) |entry, i| { + self.entries.appendAssumeCapacity(entry); + self.by_id.putAssumeCapacity(entry.base().id, base_len + i); + } + self.leaf_id = entries[entries.len - 1].base().id; + built = 0; + } + + fn persistEntries(self: *SessionFile, entries: []const SessionEntry) !void { + const file = try Io.Dir.cwd().openFile(self.io, self.session_file, .{ + .mode = .write_only, + }); + defer file.close(self.io); + + var offset = self.written_bytes; + for (entries) |entry| { + const line = try session_mod.serializeEntry(self.allocator, entry); + defer self.allocator.free(line); + try file.writePositionalAll(self.io, line, offset); + offset += line.len; + try file.writePositionalAll(self.io, "\n", offset); + offset += 1; + } + file.sync(self.io) catch {}; + self.written_bytes = offset; + } + + // ============================================================================= + // Conversation rebuild + // ============================================================================= + + /// Build a fresh `Conversation` from the entry log. Caller owns the + /// returned conversation (call `deinit`). + pub fn rebuildConversation(self: *const SessionFile) !conversation_mod.Conversation { + var conv = conversation_mod.Conversation.init(self.allocator); + errdefer conv.deinit(); + + for (self.entries.items) |entry| { + switch (entry) { + .message => |me| try appendMessageToConv(&conv, self.allocator, me.message, me.stamp), + } + } + return conv; + } +}; + +/// Best-effort extraction of plain prompt text from a user `StoredMessage`. +/// Used to populate `SessionInfo.last_user_message`. Returns null if the +/// message carries no plain text block. Caller owns the returned slice. +fn extractUserText(alloc: Allocator, msg: StoredMessage) !?[]u8 { + for (msg.content) |block| { + if (block == .text) { + return try alloc.dupe(u8, block.text.text); + } + } + return null; +} + +fn appendMessageToConv( + conv: *conversation_mod.Conversation, + allocator: Allocator, + disk_msg: StoredMessage, + stamp: ?session_mod.WireStamp, +) !void { + var content: std.ArrayList(conversation_mod.ContentBlock) = .empty; + errdefer { + for (content.items) |*b| { + var mut = b.*; + mut.deinit(allocator); + } + content.deinit(allocator); + } + try content.ensureTotalCapacity(allocator, disk_msg.content.len); + const sys_mode: conversation_mod.SystemMode = switch (disk_msg.mode) { + .append => .append, + .replace => .replace, + }; + for (disk_msg.content) |db| { + var block = try session_mod.diskContentBlockToInternal(allocator, db); + // System-role text blocks become `.System` blocks carrying the + // message's recorded mode, so the append/replace derivation works + // on the rebuilt conversation exactly as it did when written. + if (disk_msg.role == .system and block == .Text) { + const tb = block.Text; + block = .{ .System = .{ .text = tb, .mode = sys_mode } }; + } + if (block == .Thinking and stamp != null) { + block.Thinking.signature_origin = try conversation_mod.SignatureOrigin.init( + allocator, + stamp.?.api_style, + stamp.?.base_url, + stamp.?.model, + ); + } + content.appendAssumeCapacity(block); + } + const role: conversation_mod.MessageRole = switch (disk_msg.role) { + .system => .system, + .user => .user, + .assistant => .assistant, + }; + // Reconstruct the per-message producing identity from the wire stamp, so + // a later in-session compaction preserves it (rather than re-stamping the + // restated turn with the compaction model). Borrowed `stamp` slices are + // duped into the conversation allocator. + const identity: ?session_store_mod.WireIdentity = if (stamp) |st| + try conversation_mod.dupeWireIdentity(allocator, .{ + .api_style = st.api_style, + .base_url = st.base_url, + .model = st.model, + .reasoning = st.reasoning, + .thinking = st.thinking, + .effort = st.effort, + .thinking_budget_tokens = st.thinking_budget_tokens, + .thinking_interleaved = st.thinking_interleaved, + }) + else + null; + errdefer if (identity) |id| conversation_mod.freeWireIdentity(allocator, id); + // Carry the recorded usage forward so compaction can size the retention + // window after a session is reopened (it's null for user/system). + try conv.messages.append(allocator, .{ + .role = role, + .content = content, + .usage = disk_msg.usage, + .identity = identity, + }); +} + +// ============================================================================= +// Entry repair on load +// ============================================================================= + +fn elideDanglingToolUses(allocator: Allocator, entries: *std.ArrayList(SessionEntry)) bool { + var needed: std.StringHashMap(void) = .init(allocator); + defer needed.deinit(); + var changed = false; + + var i = entries.items.len; + while (i > 0) { + i -= 1; + const entry = &entries.items[i]; + if (entry.* != .message) continue; + const msg = &entry.message.message; + + if (msg.role == .user) { + for (msg.content) |block| { + if (block == .tool_result) { + needed.put(block.tool_result.tool_use_id, {}) catch {}; + } + } + continue; + } + + if (msg.role != .assistant) continue; + var kept: std.ArrayList(StoredContentBlock) = .empty; + defer kept.deinit(allocator); + var removed = false; + for (msg.content) |block| { + if (block == .tool_use and !needed.contains(block.tool_use.id)) { + block.deinit(allocator); + removed = true; + continue; + } + kept.append(allocator, block) catch unreachable; + } + if (!removed) continue; + allocator.free(msg.content); + msg.content = kept.toOwnedSlice(allocator) catch unreachable; + changed = true; + } + return changed; +} + +// ============================================================================= +// File utilities +// ============================================================================= + +fn readWholeFile(allocator: Allocator, io: Io, path: []const u8) ![]u8 { + const file = Io.Dir.cwd().openFile(io, path, .{ .mode = .read_only }) catch |err| switch (err) { + error.FileNotFound => return error.InvalidSessionFile, + else => return err, + }; + defer file.close(io); + + const len = file.length(io) catch { + // Fall back to a streaming read of a reasonable upper bound. + // Sessions over ~10 MB are out of scope for phase 4. + var list: std.ArrayList(u8) = .empty; + defer list.deinit(allocator); + var chunk: [4096]u8 = undefined; + while (true) { + const n = file.readStreaming(io, &.{&chunk}) catch break; + if (n == 0) break; + try list.appendSlice(allocator, chunk[0..n]); + } + return try list.toOwnedSlice(allocator); + }; + + const buf = try allocator.alloc(u8, @intCast(len)); + errdefer allocator.free(buf); + _ = try file.readPositionalAll(io, buf, 0); + return buf; +} + +fn statFileForLength(io: Io, path: []const u8) !u64 { + const file = try Io.Dir.cwd().openFile(io, path, .{ .mode = .read_only }); + defer file.close(io); + return try file.length(io); +} + +fn truncateFileTo(io: Io, path: []const u8, new_length: u64) !void { + const file = try Io.Dir.cwd().openFile(io, path, .{ .mode = .write_only }); + defer file.close(io); + try file.setLength(io, new_length); + file.sync(io) catch {}; +} + +/// Write a fresh file containing `header` followed by `entries`. Truncates +/// any existing content. Used after a migration rewrites the format. +fn rewriteFile( + allocator: Allocator, + io: Io, + path: []const u8, + header: SessionHeader, + entries: []const SessionEntry, +) !void { + const file = try Io.Dir.cwd().createFile(io, path, .{ + .truncate = true, + .read = false, + }); + defer file.close(io); + + var offset: u64 = 0; + const header_line = try session_mod.serializeHeader(allocator, header); + defer allocator.free(header_line); + try file.writePositionalAll(io, header_line, offset); + offset += header_line.len; + try file.writePositionalAll(io, "\n", offset); + offset += 1; + for (entries) |e| { + const line = try session_mod.serializeEntry(allocator, e); + defer allocator.free(line); + try file.writePositionalAll(io, line, offset); + offset += line.len; + try file.writePositionalAll(io, "\n", offset); + offset += 1; + } + file.sync(io) catch {}; +} + +fn mkdirP(io: Io, path: []const u8) !void { + Io.Dir.cwd().createDirPath(io, path) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; +} + +// ============================================================================= +// Listing +// ============================================================================= + +/// List sessions in `session_dir`. Returns a slice of `SessionInfo`s +/// sorted by `modified` descending (most recent first). Caller owns the +/// slice and each `SessionInfo`. +/// +/// If the directory does not exist, returns an empty slice (no error). +/// +/// If `on_progress` is non-null, it is invoked after each file is parsed. +pub fn listSessions( + allocator: Allocator, + io: Io, + session_dir: []const u8, + on_progress: ?*const fn (loaded: usize, total: usize) void, +) ![]FileInfo { + var dir = Io.Dir.cwd().openDir(io, session_dir, .{}) catch |err| switch (err) { + error.FileNotFound => return try allocator.alloc(FileInfo, 0), + else => return err, + }; + defer dir.close(io); + + var names: std.ArrayList([]u8) = .empty; + defer { + for (names.items) |n| allocator.free(n); + names.deinit(allocator); + } + + var it = dir.iterate(); + while (try it.next(io)) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.name, ".jsonl")) continue; + const copy = try allocator.dupe(u8, entry.name); + errdefer allocator.free(copy); + try names.append(allocator, copy); + } + + var infos: std.ArrayList(FileInfo) = .empty; + errdefer { + for (infos.items) |i| i.deinit(allocator); + infos.deinit(allocator); + } + try infos.ensureTotalCapacity(allocator, names.items.len); + + var loaded: usize = 0; + for (names.items) |name| { + const full = try std.fs.path.join(allocator, &.{ session_dir, name }); + defer allocator.free(full); + const info_opt = buildFileInfo(allocator, io, full) catch null; + if (info_opt) |info| { + infos.appendAssumeCapacity(info); + } + loaded += 1; + if (on_progress) |cb| cb(loaded, names.items.len); + } + + const slice = try infos.toOwnedSlice(allocator); + std.sort.pdq(FileInfo, slice, {}, fileInfoNewerFirst); + return slice; +} + +fn fileInfoNewerFirst(_: void, a: FileInfo, b: FileInfo) bool { + return std.mem.order(u8, a.modified, b.modified) == .gt; +} + +fn buildFileInfo( + allocator: Allocator, + io: Io, + file_path: []const u8, +) !?FileInfo { + const bytes = readWholeFile(allocator, io, file_path) catch return null; + defer allocator.free(bytes); + + var header_opt: ?SessionHeader = null; + defer if (header_opt) |h| h.deinit(allocator); + + var message_count: usize = 0; + var last_activity: ?[]u8 = null; + defer if (last_activity) |la| allocator.free(la); + var last_user: ?[]u8 = null; + defer if (last_user) |lu| allocator.free(lu); + var last_stamp: ?session_mod.WireStamp = null; + defer if (last_stamp) |st| st.deinit(allocator); + + var lines = std.mem.splitScalar(u8, bytes, '\n'); + while (lines.next()) |line| { + const trimmed = std.mem.trim(u8, line, " \t\r"); + if (trimmed.len == 0) continue; + const fe = session_mod.parseLine(allocator, trimmed) catch break; + switch (fe) { + .header => |h| { + if (header_opt != null) { + h.deinit(allocator); + } else { + header_opt = h; + } + }, + .entry => |e| { + defer e.deinit(allocator); + switch (e) { + .message => |m| { + if (m.message.role == .user or m.message.role == .assistant) { + message_count += 1; + if (last_activity) |la| allocator.free(la); + last_activity = try allocator.dupe(u8, m.base.timestamp); + } + if (m.stamp) |st| { + if (last_stamp) |old| old.deinit(allocator); + last_stamp = try st.dupe(allocator); + } + if (m.message.role == .user) { + if (try extractUserText(allocator, m.message)) |ut| { + if (last_user) |lu| allocator.free(lu); + last_user = ut; + } + } + }, + } + }, + } + } + + const header = header_opt orelse return null; + + const path = try allocator.dupe(u8, file_path); + errdefer allocator.free(path); + const id = try allocator.dupe(u8, header.id); + errdefer allocator.free(id); + const created = try allocator.dupe(u8, header.timestamp); + errdefer allocator.free(created); + const modified = if (last_activity) |la| blk: { + last_activity = null; + break :blk la; + } else try allocator.dupe(u8, header.timestamp); + errdefer allocator.free(modified); + const last_user_message = if (last_user) |lu| blk: { + last_user = null; + break :blk lu; + } else try allocator.dupe(u8, ""); + errdefer allocator.free(last_user_message); + const stamp_out = if (last_stamp) |st| blk: { + last_stamp = null; + break :blk st; + } else null; + + return .{ + .path = path, + .id = id, + .created = created, + .modified = modified, + .message_count = message_count, + .last_user_message = last_user_message, + .stamp = stamp_out, + }; +} + +// ============================================================================= +// Recent / resume helpers +// ============================================================================= + +/// Resolve a (possibly abbreviated) session id to a session file path +/// within `session_dir`. Errors if no match or ambiguous prefix. +pub fn resolveSessionId( + allocator: Allocator, + io: Io, + session_dir: []const u8, + id_or_prefix: []const u8, +) ![]u8 { + var dir = Io.Dir.cwd().openDir(io, session_dir, .{}) catch |err| switch (err) { + error.FileNotFound => return error.SessionNotFound, + else => return err, + }; + defer dir.close(io); + + var match: ?[]u8 = null; + errdefer if (match) |m| allocator.free(m); + + var it = dir.iterate(); + while (try it.next(io)) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.name, ".jsonl")) continue; + // Strip `.jsonl` for the prefix match. + const stem = entry.name[0 .. entry.name.len - ".jsonl".len]; + if (!std.mem.startsWith(u8, stem, id_or_prefix)) continue; + if (match != null) return error.AmbiguousSessionId; + match = try allocator.dupe(u8, entry.name); + } + + const name = match orelse return error.SessionNotFound; + defer allocator.free(name); + match = null; + return try std.fs.path.join(allocator, &.{ session_dir, name }); +} + +// ============================================================================= +// FileSystemJSONLStore — the directory-backed catalog (SessionStore impl) +// ============================================================================= + +/// A directory-backed `SessionStore`: each session is one `<id>.jsonl` file +/// under `dir`. The catalog mints `Session` handles, lists/resolves files, +/// loads conversations, and routes appends to the right `SessionFile`. +/// +/// Open `SessionFile`s are cached by id for the catalog's lifetime so the +/// buffered-until-first-assistant write discipline survives across the +/// separate user-prompt and assistant-turn appends of a single turn. +/// +/// `dir` is the already-resolved sessions directory (the panto CLI derives +/// the per-cwd grouping; the store itself is cwd-agnostic). Optional +/// `metadata` is stamped into new session headers for display/provenance only. +pub const FileSystemJSONLStore = struct { + allocator: Allocator, + io: Io, + dir: []u8, // owned: the sessions directory + metadata: ?[]u8, // owned: recorded in new session headers + open: std.StringHashMap(*SessionFile), + + pub fn init(allocator: Allocator, io: Io, dir: []const u8) !FileSystemJSONLStore { + return initWithMetadata(allocator, io, dir, null); + } + + pub fn initWithMetadata(allocator: Allocator, io: Io, dir: []const u8, metadata: ?[]const u8) !FileSystemJSONLStore { + const dir_copy = try allocator.dupe(u8, dir); + errdefer allocator.free(dir_copy); + const metadata_copy: ?[]u8 = if (metadata) |m| try allocator.dupe(u8, m) else null; + return .{ + .allocator = allocator, + .io = io, + .dir = dir_copy, + .metadata = metadata_copy, + .open = std.StringHashMap(*SessionFile).init(allocator), + }; + } + + pub fn deinit(self: *FileSystemJSONLStore) void { + var it = self.open.iterator(); + while (it.next()) |e| { + e.value_ptr.*.deinit(); + self.allocator.destroy(e.value_ptr.*); + self.allocator.free(e.key_ptr.*); + } + self.open.deinit(); + self.allocator.free(self.dir); + if (self.metadata) |m| self.allocator.free(m); + } + + /// Borrow (opening if needed) the `SessionFile` for `id`, caching it. + /// Returns null if the file does not exist on disk and `create_missing` + /// is false. + fn fileFor(self: *FileSystemJSONLStore, id: []const u8, create_missing: bool) !?*SessionFile { + if (self.open.get(id)) |sf| return sf; + // Locate the file by exact id. + const path = try std.fs.path.join(self.allocator, &.{ self.dir, id }); + defer self.allocator.free(path); + const full = try std.fmt.allocPrint(self.allocator, "{s}.jsonl", .{path}); + defer self.allocator.free(full); + + const exists = blk: { + Io.Dir.cwd().access(self.io, full, .{}) catch break :blk false; + break :blk true; + }; + if (!exists and !create_missing) return null; + + const sf = try self.allocator.create(SessionFile); + errdefer self.allocator.destroy(sf); + sf.* = if (exists) + try SessionFile.open(self.allocator, self.io, full) + else + try SessionFile.initWithId(self.allocator, self.io, self.dir, self.metadata, id); + errdefer sf.deinit(); + + const key = try self.allocator.dupe(u8, id); + errdefer self.allocator.free(key); + try self.open.put(key, sf); + return sf; + } + + fn infoFromFileInfo(self: *FileSystemJSONLStore, fi: FileInfo) !session_store_mod.SessionInfo { + const id = try self.allocator.dupe(u8, fi.id); + errdefer self.allocator.free(id); + const created = try self.allocator.dupe(u8, fi.created); + errdefer self.allocator.free(created); + const modified = try self.allocator.dupe(u8, fi.modified); + errdefer self.allocator.free(modified); + const last_user = try self.allocator.dupe(u8, fi.last_user_message); + errdefer self.allocator.free(last_user); + const base_url = try self.allocator.dupe(u8, if (fi.stamp) |s| s.base_url else ""); + errdefer self.allocator.free(base_url); + const model = try self.allocator.dupe(u8, if (fi.stamp) |s| s.model else ""); + return .{ + .id = id, + .created = created, + .modified = modified, + .message_count = fi.message_count, + .last_user_message = last_user, + .api_style = if (fi.stamp) |s| s.api_style else .openai_chat, + .base_url = base_url, + .model = model, + .reasoning = if (fi.stamp) |s| s.reasoning else .default, + }; + } + + // ---------- vtable ---------- + + fn createVT(ctx: *anyopaque) session_store_mod.Session { + const self: *FileSystemJSONLStore = @ptrCast(@alignCast(ctx)); + // Mint a fresh id; nothing hits disk until the first append. + const id = newUuidV7(self.allocator, self.io) catch ""; + // The SessionFile is created lazily on first append via fileFor. + const info: session_store_mod.SessionInfo = .{ + .id = id, + .created = self.allocator.dupe(u8, "") catch "", + .modified = self.allocator.dupe(u8, "") catch "", + .message_count = 0, + .last_user_message = self.allocator.dupe(u8, "") catch "", + .api_style = .openai_chat, + .base_url = self.allocator.dupe(u8, "") catch "", + .model = self.allocator.dupe(u8, "") catch "", + .reasoning = .default, + }; + return .{ .info = info, .store = self.store() }; + } + + fn listVT(ctx: *anyopaque) anyerror![]session_store_mod.SessionInfo { + const self: *FileSystemJSONLStore = @ptrCast(@alignCast(ctx)); + const fis = try listSessions(self.allocator, self.io, self.dir, null); + defer { + for (fis) |fi| fi.deinit(self.allocator); + self.allocator.free(fis); + } + var out = try self.allocator.alloc(session_store_mod.SessionInfo, fis.len); + var built: usize = 0; + errdefer { + for (out[0..built]) |i| i.deinit(self.allocator); + self.allocator.free(out); + } + for (fis, 0..) |fi, i| { + out[i] = try self.infoFromFileInfo(fi); + built += 1; + } + return out; + } + + fn freeSessionInfosVT(ctx: *anyopaque, infos: []session_store_mod.SessionInfo) void { + const self: *FileSystemJSONLStore = @ptrCast(@alignCast(ctx)); + for (infos) |i| i.deinit(self.allocator); + self.allocator.free(infos); + } + + fn resolveVT(ctx: *anyopaque, id_or_prefix: []const u8) anyerror!?session_store_mod.Session { + const self: *FileSystemJSONLStore = @ptrCast(@alignCast(ctx)); + const path = resolveSessionId(self.allocator, self.io, self.dir, id_or_prefix) catch |err| switch (err) { + error.SessionNotFound => return null, + else => return err, + }; + defer self.allocator.free(path); + return try self.sessionFromPath(path); + } + + fn latestVT(ctx: *anyopaque) anyerror!?session_store_mod.Session { + const self: *FileSystemJSONLStore = @ptrCast(@alignCast(ctx)); + // Most-recently-*modified* wins, matching the `list()` sort order — + // not newest-created (lexicographic UUIDv7 filename). + const fis = try listSessions(self.allocator, self.io, self.dir, null); + defer { + for (fis) |fi| fi.deinit(self.allocator); + self.allocator.free(fis); + } + if (fis.len == 0) return null; + const info = try self.infoFromFileInfo(fis[0]); + return .{ .info = info, .store = self.store() }; + } + + fn sessionFromPath(self: *FileSystemJSONLStore, path: []const u8) !?session_store_mod.Session { + const fi = (try buildFileInfo(self.allocator, self.io, path)) orelse return null; + defer fi.deinit(self.allocator); + const info = try self.infoFromFileInfo(fi); + return .{ .info = info, .store = self.store() }; + } + + fn loadVT(ctx: *anyopaque, id: []const u8) anyerror!?conversation_mod.Conversation { + const self: *FileSystemJSONLStore = @ptrCast(@alignCast(ctx)); + const sf = (try self.fileFor(id, false)) orelse return null; + return try sf.rebuildConversation(); + } + + fn appendMessagesVT( + ctx: *anyopaque, + session_id: []const u8, + messages: []session_store_mod.PersistentMessage, + ) anyerror!void { + const self: *FileSystemJSONLStore = @ptrCast(@alignCast(ctx)); + if (messages.len == 0) return; + const sf = (try self.fileFor(session_id, true)).?; + + // Convert each rich PersistentMessage to a StoredMessage + wire stamp. + // The FS store deliberately ignores the `conversation` and + // `tools_available` provenance fields. + var stored = try self.allocator.alloc(StoredMessage, messages.len); + var stamps = try self.allocator.alloc(?session_mod.WireStamp, messages.len); + defer self.allocator.free(stored); + defer self.allocator.free(stamps); + var built: usize = 0; + errdefer for (stored[0..built]) |sm| sm.deinit(self.allocator); + + for (messages, 0..) |pm, i| { + stored[i] = try persistentToStored(self.allocator, pm); + // System entries carry no wire stamp; user/assistant do. + stamps[i] = if (pm.message.role == .system) null else .{ + .api_style = pm.identity.api_style, + .base_url = pm.identity.base_url, + .model = pm.identity.model, + .reasoning = pm.identity.reasoning, + .thinking = pm.identity.thinking, + .effort = pm.identity.effort, + .thinking_budget_tokens = pm.identity.thinking_budget_tokens, + .thinking_interleaved = pm.identity.thinking_interleaved, + }; + built += 1; + } + + // appendMessagesAtomic consumes the StoredMessages; it dupes stamps. + try sf.appendMessagesAtomic(stored, stamps); + } + + const store_vtable: session_store_mod.SessionStore.VTable = .{ + .create = createVT, + .list = listVT, + .freeSessionInfos = freeSessionInfosVT, + .resolve = resolveVT, + .latest = latestVT, + .load = loadVT, + .appendMessages = appendMessagesVT, + }; + + /// Wrap this catalog as a neutral `SessionStore`. The handle borrows + /// `self`; `self` must outlive it. + pub fn store(self: *FileSystemJSONLStore) session_store_mod.SessionStore { + return .{ .ptr = self, .vtable = &store_vtable }; + } +}; + +/// Convert a rich in-memory `PersistentMessage` to the on-disk +/// `StoredMessage`. Strings are duplicated; the source is untouched. +fn persistentToStored( + alloc: Allocator, + pm: session_store_mod.PersistentMessage, +) !StoredMessage { + const msg = pm.message; + const blocks = try alloc.alloc(session_mod.StoredContentBlock, msg.content.items.len); + var allocated: usize = 0; + errdefer { + for (blocks[0..allocated]) |b| b.deinit(alloc); + alloc.free(blocks); + } + for (msg.content.items) |block| { + blocks[allocated] = try session_mod.contentBlockToDisk(alloc, block); + allocated += 1; + } + const mode: session_mod.StoredSystemMode = blk: { + for (msg.content.items) |block| { + if (block == .System and block.System.mode == .replace) break :blk .replace; + } + break :blk .append; + }; + const stop_reason: ?[]const u8 = if (msg.role == .assistant) try alloc.dupe(u8, "stop") else null; + errdefer if (stop_reason) |s| alloc.free(s); + const metadata: ?[]const u8 = if (msg.metadata) |m| try alloc.dupe(u8, m) else null; + const role: session_mod.StoredMessageRole = switch (msg.role) { + .system => .system, + .user => .user, + .assistant => .assistant, + }; + return .{ + .role = role, + .content = blocks, + .mode = mode, + .stop_reason = stop_reason, + .usage = pm.usage, + .metadata = metadata, + }; +} + +// ============================================================================= +// Tests +// ============================================================================= + +const testing = std.testing; + +/// Borrowed wire stamps for tests (no allocation; the manager dupes them). +fn oaStamp() session_mod.WireStamp { + return .{ .api_style = .openai_chat, .base_url = "https://api.openai.com/v1", .model = "gpt-4o" }; +} +fn anStamp() session_mod.WireStamp { + return .{ .api_style = .anthropic_messages, .base_url = "https://api.anthropic.com", .model = "claude-sonnet-4-20250514" }; +} + +/// Test helper: append a single-text user message (see the `addUserMessage` +/// signature change to a block slice). +fn addUserText(conv: *conversation_mod.Conversation, text: []const u8) !void { + const tb = try conversation_mod.textualBlockFromSlice(conv.allocator, text); + var block: conversation_mod.ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + +test "newUuidV7: produces 36-char hyphenated string with version 7" { + const io = testing.io; + const id = try newUuidV7(testing.allocator, io); + defer testing.allocator.free(id); + try testing.expectEqual(@as(usize, 36), id.len); + // Position 14 is the version nibble — should be '7'. + try testing.expectEqual(@as(u8, '7'), id[14]); + // Hyphens at canonical positions. + try testing.expectEqual(@as(u8, '-'), id[8]); + try testing.expectEqual(@as(u8, '-'), id[13]); + try testing.expectEqual(@as(u8, '-'), id[18]); + try testing.expectEqual(@as(u8, '-'), id[23]); +} + +test "isoTimestamp: well-formed ISO 8601 with millisecond precision" { + const ts = try isoTimestamp(testing.allocator, testing.io); + defer testing.allocator.free(ts); + try testing.expectEqual(@as(usize, 24), ts.len); + try testing.expectEqual(@as(u8, '-'), ts[4]); + try testing.expectEqual(@as(u8, 'T'), ts[10]); + try testing.expectEqual(@as(u8, '.'), ts[19]); + try testing.expectEqual(@as(u8, 'Z'), ts[23]); +} + +// ---- In-memory + filesystem tests (use a tmp dir) ---- + +const TmpSessionDir = struct { + parent: std.testing.TmpDir, + abs_path: []u8, + + fn init(allocator: Allocator) !TmpSessionDir { + var parent = std.testing.tmpDir(.{}); + errdefer parent.cleanup(); + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const n = try parent.dir.realPath(testing.io, &path_buf); + const abs = try allocator.dupe(u8, path_buf[0..n]); + return .{ .parent = parent, .abs_path = abs }; + } + + fn deinit(self: *TmpSessionDir, allocator: Allocator) void { + allocator.free(self.abs_path); + self.parent.cleanup(); + } +}; + +test "SessionFile.init: does not create file yet" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + + // Use a non-existent subdirectory inside the tmp dir to also exercise + // lazy directory creation. + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var mgr = try SessionFile.init( + testing.allocator, + io, + sessions, + "{\"cwd\":\"/some/cwd\"}", + ); + defer mgr.deinit(); + + try testing.expect(!mgr.isFlushed()); + + // The directory should not exist yet. + const stat_err = Io.Dir.cwd().openDir(io, sessions, .{}); + try testing.expectError(error.FileNotFound, stat_err); +} + +test "SessionFile: full flow — buffer, flush on assistant, append, resume" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + const session_file: []u8 = blk: { + var mgr = try SessionFile.init( + testing.allocator, + io, + sessions, + "{\"cwd\":\"/proj/foo\"}", + ); + defer mgr.deinit(); + + // System message buffers in memory — nothing on disk yet. + const sys_blocks = try testing.allocator.alloc(StoredContentBlock, 1); + sys_blocks[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "You are helpful.") } }; + _ = try mgr.appendMessage( + .{ .role = .system, .content = sys_blocks }, + null, + ); + try testing.expect(!mgr.isFlushed()); + + // User message — still buffered. + const usr_blocks = try testing.allocator.alloc(StoredContentBlock, 1); + usr_blocks[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hi there") } }; + _ = try mgr.appendMessage( + .{ .role = .user, .content = usr_blocks }, + oaStamp(), + ); + try testing.expect(!mgr.isFlushed()); + + // Assistant message — first flush: header + all buffered entries. + const a_blocks = try testing.allocator.alloc(StoredContentBlock, 1); + a_blocks[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hello") } }; + _ = try mgr.appendMessage( + .{ + .role = .assistant, + .content = a_blocks, + .stop_reason = try testing.allocator.dupe(u8, "stop"), + }, + oaStamp(), + ); + try testing.expect(mgr.isFlushed()); + + // Append another user/assistant round. + const u_two = try testing.allocator.alloc(StoredContentBlock, 1); + u_two[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "what's 2+2?") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u_two }, oaStamp()); + + const a2 = try testing.allocator.alloc(StoredContentBlock, 1); + a2[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "4") } }; + _ = try mgr.appendMessage( + .{ .role = .assistant, .content = a2, .stop_reason = try testing.allocator.dupe(u8, "stop") }, + oaStamp(), + ); + + try testing.expectEqual(@as(usize, 5), mgr.entries.items.len); + + break :blk try testing.allocator.dupe(u8, mgr.getSessionFile()); + }; + defer testing.allocator.free(session_file); + + // Verify the file exists and is well-formed. + { + const bytes = try readWholeFile(testing.allocator, io, session_file); + defer testing.allocator.free(bytes); + // 1 header + 5 entries + trailing \n on each = 6 newlines. + var nl_count: usize = 0; + for (bytes) |b| if (b == '\n') { + nl_count += 1; + }; + try testing.expectEqual(@as(usize, 6), nl_count); + } + + // Resume. + var resumed = try SessionFile.open(testing.allocator, io, session_file); + defer resumed.deinit(); + try testing.expect(resumed.isFlushed()); + try testing.expectEqual(@as(usize, 5), resumed.entries.items.len); + try testing.expectEqualStrings("{\"cwd\":\"/proj/foo\"}", resumed.header.metadata.?); + + // Continue the conversation. + const u_three = try testing.allocator.alloc(StoredContentBlock, 1); + u_three[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "thanks") } }; + _ = try resumed.appendMessage(.{ .role = .user, .content = u_three }, oaStamp()); + try testing.expectEqual(@as(usize, 6), resumed.entries.items.len); +} + +test "SessionFile: assistant message tags the message metadata and the entry leaf id is the assistant entry" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + + const u_blocks = try testing.allocator.alloc(StoredContentBlock, 1); + u_blocks[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "ping") } }; + const user_id = try mgr.appendMessage(.{ .role = .user, .content = u_blocks }, oaStamp()); + + const a_blocks = try testing.allocator.alloc(StoredContentBlock, 1); + a_blocks[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "pong") } }; + const asst_id = try mgr.appendMessage(.{ .role = .assistant, .content = a_blocks }, null); + + // Leaf is the assistant entry. + try testing.expectEqualStrings(asst_id, mgr.leaf_id.?); + // Parent of assistant is the user entry. + const assistant_entry = mgr.entries.items[mgr.by_id.get(asst_id).?]; + try testing.expectEqualStrings(user_id, assistant_entry.base().parent_id.?); + // User entry's parent is null (no system). + const user_entry = mgr.entries.items[mgr.by_id.get(user_id).?]; + try testing.expect(user_entry.base().parent_id == null); +} + +/// Test helper: the active provider/model stamp — the last entry carrying a +/// wire stamp, walking leaf→root. Null when no stamped message exists yet. +fn activeStamp(sf: *const SessionFile) ?session_mod.WireStamp { + var i = sf.entries.items.len; + while (i > 0) : (i -= 1) { + switch (sf.entries.items[i - 1]) { + .message => |m| if (m.stamp) |st| return st, + } + } + return null; +} + +test "SessionFile: activeStamp is null before any user message, then tracks the latest user stamp" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + + // No user messages yet — there is no "active" model on disk yet. + try testing.expect(activeStamp(&mgr) == null); + + // Stamp a user message with anthropic. + const u_blocks = try testing.allocator.alloc(StoredContentBlock, 1); + u_blocks[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hi") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u_blocks }, anStamp()); + + { + const am = activeStamp(&mgr).?; + try testing.expectEqual(session_mod.APIStyle.anthropic_messages, am.api_style); + try testing.expectEqualStrings("claude-sonnet-4-20250514", am.model); + } +} + +test "SessionFile: rebuildConversation reconstructs system/user/assistant turn" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + + const sys = try testing.allocator.alloc(StoredContentBlock, 1); + sys[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "you are helpful") } }; + _ = try mgr.appendMessage(.{ .role = .system, .content = sys }, null); + + const u = try testing.allocator.alloc(StoredContentBlock, 1); + u[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hello") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u }, oaStamp()); + + const a = try testing.allocator.alloc(StoredContentBlock, 1); + a[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hi!") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = a }, null); + + var conv = try mgr.rebuildConversation(); + defer conv.deinit(); + try testing.expectEqual(@as(usize, 3), conv.messages.items.len); + try testing.expectEqual(conversation_mod.MessageRole.system, conv.messages.items[0].role); + try testing.expectEqualStrings("you are helpful", conv.messages.items[0].content.items[0].System.text.items); + try testing.expectEqual(conversation_mod.MessageRole.user, conv.messages.items[1].role); + try testing.expectEqualStrings("hello", conv.messages.items[1].content.items[0].Text.items); + try testing.expectEqual(conversation_mod.MessageRole.assistant, conv.messages.items[2].role); + try testing.expectEqualStrings("hi!", conv.messages.items[2].content.items[0].Text.items); +} + +test "SessionFile: crash recovery truncates corrupted trailing line" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + // Build a valid session first. + const session_file: []u8 = blk: { + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + const u = try testing.allocator.alloc(StoredContentBlock, 1); + u[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "ping") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u }, oaStamp()); + const a = try testing.allocator.alloc(StoredContentBlock, 1); + a[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "pong") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = a }, null); + break :blk try testing.allocator.dupe(u8, mgr.getSessionFile()); + }; + defer testing.allocator.free(session_file); + + // Corrupt the file: append a partial JSON line at the end. + const garbage = "{\"type\":\"message\",\"id\":\"deadbeef\",\"parent"; + { + const file = try Io.Dir.cwd().openFile(io, session_file, .{ .mode = .write_only }); + defer file.close(io); + const len = try file.length(io); + try file.writePositionalAll(io, garbage, len); + } + // Confirm the file got bigger. + { + const f = try Io.Dir.cwd().openFile(io, session_file, .{ .mode = .read_only }); + defer f.close(io); + const corrupted_len = try f.length(io); + try testing.expect(corrupted_len > garbage.len); + } + + // Now resume — the partial line should be truncated. + var resumed = try SessionFile.open(testing.allocator, io, session_file); + defer resumed.deinit(); + try testing.expectEqual(@as(usize, 2), resumed.entries.items.len); + + // And the file on disk should match. + { + const bytes = try readWholeFile(testing.allocator, io, session_file); + defer testing.allocator.free(bytes); + try testing.expect(!std.mem.endsWith(u8, bytes, "parent")); + // Should end with a newline after the assistant entry. + try testing.expectEqual(@as(u8, '\n'), bytes[bytes.len - 1]); + } +} + +test "listSessions: returns most recent first, with counts" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + // Create two sessions. + for (0..2) |i| { + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + const u = try testing.allocator.alloc(StoredContentBlock, 1); + u[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "u") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u }, oaStamp()); + const a = try testing.allocator.alloc(StoredContentBlock, 1); + a[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "a") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = a }, null); + // Small sleep so UUIDv7 timestamps differ. + io.sleep(.fromMilliseconds(2), .real) catch {}; + _ = i; + } + + const infos = try listSessions(testing.allocator, io, sessions, null); + defer { + for (infos) |fi| fi.deinit(testing.allocator); + testing.allocator.free(infos); + } + try testing.expectEqual(@as(usize, 2), infos.len); + try testing.expectEqual(@as(usize, 2), infos[0].message_count); + try testing.expect(std.mem.order(u8, infos[0].modified, infos[1].modified) != .lt); +} + +test "latest: picks most-recently-modified, not newest-created" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var catalog = try FileSystemJSONLStore.init(testing.allocator, io, sessions); + defer catalog.deinit(); + const st = catalog.store(); + + // Before any sessions exist → null. + try testing.expect((try st.latest()) == null); + + // Create session A, then session B (newer id), then append to A again + // so A is the most recently *modified*. + var first_id: ?[]u8 = null; + defer if (first_id) |s| testing.allocator.free(s); + + for (0..2) |i| { + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + const u = try testing.allocator.alloc(StoredContentBlock, 1); + u[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "u") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u }, oaStamp()); + const a = try testing.allocator.alloc(StoredContentBlock, 1); + a[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "a") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = a }, oaStamp()); + if (i == 0) first_id = try testing.allocator.dupe(u8, mgr.header.id); + io.sleep(.fromMilliseconds(2), .real) catch {}; + } + { + const path = try resolveSessionId(testing.allocator, io, sessions, first_id.?); + defer testing.allocator.free(path); + var mgr = try SessionFile.open(testing.allocator, io, path); + defer mgr.deinit(); + const a = try testing.allocator.alloc(StoredContentBlock, 1); + a[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "a") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = a }, oaStamp()); + } + + var latest = (try st.latest()).?; + defer latest.info.deinit(testing.allocator); + try testing.expectEqualStrings(first_id.?, latest.info.id); +} + +test "SessionFile: tool-use round-trip — assistant w/ ToolUse, user w/ ToolResult, assistant" { + const io = testing.io; + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + const session_file: []u8 = blk: { + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + + const u = try testing.allocator.alloc(StoredContentBlock, 1); + u[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "list files") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u }, oaStamp()); + + // Assistant emits a ToolUse. + const am1 = try testing.allocator.alloc(StoredContentBlock, 2); + am1[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "checking...") } }; + am1[1] = .{ .tool_use = .{ + .id = try testing.allocator.dupe(u8, "tool_abc"), + .name = try testing.allocator.dupe(u8, "bash"), + .input = try testing.allocator.dupe(u8, "{\"command\":\"ls\"}"), + } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = am1 }, null); + + // Tool-result user message. + const tr = try testing.allocator.alloc(StoredContentBlock, 1); + const trp = try testing.allocator.alloc(session_mod.StoredResultPart, 1); + trp[0] = .{ .text = try testing.allocator.dupe(u8, "a.txt\nb.txt") }; + tr[0] = .{ .tool_result = .{ + .tool_use_id = try testing.allocator.dupe(u8, "tool_abc"), + .parts = trp, + } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = tr }, oaStamp()); + + // Final assistant reply. + const a2 = try testing.allocator.alloc(StoredContentBlock, 1); + a2[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "two files: a.txt and b.txt") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = a2 }, null); + + break :blk try testing.allocator.dupe(u8, mgr.getSessionFile()); + }; + defer testing.allocator.free(session_file); + + // Reopen and verify content blocks survive. + var resumed = try SessionFile.open(testing.allocator, io, session_file); + defer resumed.deinit(); + const entries = resumed.entries.items; + try testing.expectEqual(@as(usize, 4), entries.len); + + // [1] = assistant with ToolUse + try testing.expectEqual(StoredMessageRole.assistant, entries[1].message.message.role); + try testing.expectEqual(@as(usize, 2), entries[1].message.message.content.len); + try testing.expect(entries[1].message.message.content[1] == .tool_use); + try testing.expectEqualStrings("bash", entries[1].message.message.content[1].tool_use.name); + try testing.expectEqualStrings("{\"command\":\"ls\"}", entries[1].message.message.content[1].tool_use.input); + + // [2] = user with ToolResult, stamped with wire identity. + try testing.expectEqual(StoredMessageRole.user, entries[2].message.message.role); + try testing.expectEqual(session_mod.APIStyle.openai_chat, entries[2].message.stamp.?.api_style); + try testing.expect(entries[2].message.message.content[0] == .tool_result); + try testing.expectEqualStrings("tool_abc", entries[2].message.message.content[0].tool_result.tool_use_id); + try testing.expectEqualStrings("a.txt\nb.txt", entries[2].message.message.content[0].tool_result.parts[0].text); + + // Conversation rebuild yields the same shape. + var conv = try resumed.rebuildConversation(); + defer conv.deinit(); + try testing.expectEqual(@as(usize, 4), conv.messages.items.len); + try testing.expect(conv.messages.items[1].content.items[1] == .ToolUse); + try testing.expect(conv.messages.items[2].content.items[0] == .ToolResult); +} + +test "SessionFile: linear chain — each entry's parent_id is the previous entry's id" { + const io = testing.io; + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + + // Three rounds: sys, user, asst, user, asst. + const sys = try testing.allocator.alloc(StoredContentBlock, 1); + sys[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "sys") } }; + _ = try mgr.appendMessage(.{ .role = .system, .content = sys }, null); + + const u_one = try testing.allocator.alloc(StoredContentBlock, 1); + u_one[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "u1") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u_one }, oaStamp()); + + const a_one = try testing.allocator.alloc(StoredContentBlock, 1); + a_one[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "a1") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = a_one }, null); + + const u_two = try testing.allocator.alloc(StoredContentBlock, 1); + u_two[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "u2") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u_two }, oaStamp()); + + const a_two = try testing.allocator.alloc(StoredContentBlock, 1); + a_two[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "a2") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = a_two }, null); + + const entries = mgr.entries.items; + try testing.expectEqual(@as(usize, 5), entries.len); + try testing.expect(entries[0].base().parent_id == null); + for (entries[1..], 1..) |e, i| { + try testing.expectEqualStrings(entries[i - 1].base().id, e.base().parent_id.?); + } +} + +test "resolveSessionId: unique prefix → match, ambiguous → error" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + // Create one session. + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + const u = try testing.allocator.alloc(StoredContentBlock, 1); + u[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "u") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = u }, oaStamp()); + const a = try testing.allocator.alloc(StoredContentBlock, 1); + a[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "a") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = a }, null); + + const id = mgr.header.id; + const prefix = id[0..8]; + + const resolved = try resolveSessionId(testing.allocator, io, sessions, prefix); + defer testing.allocator.free(resolved); + try testing.expectEqualStrings(mgr.getSessionFile(), resolved); + + try testing.expectError(error.SessionNotFound, resolveSessionId(testing.allocator, io, sessions, "ffffffff")); +} + +test "compaction summary round-trips through persist + resume + rebuild" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + const session_file: []u8 = blk: { + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + + // System + an old turn that will be superseded. + const sys = try testing.allocator.alloc(StoredContentBlock, 1); + sys[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "you are helpful") } }; + _ = try mgr.appendMessage(.{ .role = .system, .content = sys }, null); + + const uo = try testing.allocator.alloc(StoredContentBlock, 1); + uo[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "old q") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = uo }, oaStamp()); + const ao = try testing.allocator.alloc(StoredContentBlock, 1); + ao[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "old a") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = ao }, null); + + // Compaction: summary message + duplicated kept suffix. + const cs = try testing.allocator.alloc(StoredContentBlock, 1); + cs[0] = .{ .compaction_summary = .{ .text = try testing.allocator.dupe(u8, "SUMMARY") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = cs }, oaStamp()); + + const ur = try testing.allocator.alloc(StoredContentBlock, 1); + ur[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "recent q") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = ur }, oaStamp()); + + break :blk try testing.allocator.dupe(u8, mgr.getSessionFile()); + }; + defer testing.allocator.free(session_file); + + var resumed = try SessionFile.open(testing.allocator, io, session_file); + defer resumed.deinit(); + var conv = try resumed.rebuildConversation(); + defer conv.deinit(); + + // The compaction summary block survived as a CompactionSummary. + const anchor = conversation_mod.latestCompactionIndex(conv.messages.items).?; + try testing.expectEqualStrings( + "SUMMARY", + conv.messages.items[anchor].content.items[0].CompactionSummary.text.items, + ); + + // The active window is [summary, recent q]. + const window = conversation_mod.activeMessageWindow(conv.messages.items); + try testing.expectEqual(@as(usize, 2), window.len); + try testing.expectEqualStrings("recent q", window[1].content.items[0].Text.items); + + // System prompt survives (derived independently). + var sys_blocks = try conversation_mod.effectiveSystemBlocks(testing.allocator, conv.messages.items); + defer sys_blocks.deinit(testing.allocator); + try testing.expectEqual(@as(usize, 1), sys_blocks.items.len); + try testing.expectEqualStrings("you are helpful", sys_blocks.items[0]); +} + +test "loadConversation: trailing user prompt is split out as dangling, excluded from conversation" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var mgr = try SessionFile.init(testing.allocator, io, sessions, null); + defer mgr.deinit(); + + // A completed user/assistant round, then a trailing user prompt with no + // following assistant. The dangling-prompt recovery feature was dropped + // in R2: the trailing user message simply round-trips into the rebuilt + // conversation like any other. + const um1 = try testing.allocator.alloc(StoredContentBlock, 1); + um1[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hi") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = um1 }, oaStamp()); + const am1 = try testing.allocator.alloc(StoredContentBlock, 1); + am1[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "hello") } }; + _ = try mgr.appendMessage(.{ .role = .assistant, .content = am1 }, null); + const um2 = try testing.allocator.alloc(StoredContentBlock, 1); + um2[0] = .{ .text = .{ .text = try testing.allocator.dupe(u8, "what's 2+2?") } }; + _ = try mgr.appendMessage(.{ .role = .user, .content = um2 }, oaStamp()); + + var conv = try mgr.rebuildConversation(); + defer conv.deinit(); + // All three messages are present (dangling recovery dropped). + try testing.expectEqual(@as(usize, 3), conv.messages.items.len); + try testing.expectEqual(conversation_mod.MessageRole.user, conv.messages.items[2].role); +} + +test "FileSystemJSONLStore catalog: create → append → load round-trips" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var catalog = try FileSystemJSONLStore.init(testing.allocator, io, sessions); + defer catalog.deinit(); + const store = catalog.store(); + + var sess = store.create(); + defer sess.info.deinit(testing.allocator); + + // Build a user + assistant PersistentMessage batch (borrows in-memory + // messages owned here). + var conv = conversation_mod.Conversation.init(testing.allocator); + defer conv.deinit(); + try addUserText(&conv, "ping"); + try conv.addAssistantMessage(&.{}, null); + + const id: session_store_mod.WireIdentity = .{ .api_style = .openai_chat, .base_url = "u", .model = "m" }; + var batch = [_]session_store_mod.PersistentMessage{ + .{ .message = conv.messages.items[0], .identity = id }, + .{ .message = conv.messages.items[1], .identity = id }, + }; + try sess.append(&batch); + + // The session's last-used api_style updated after append. + try testing.expectEqual(session_store_mod.APIStyle.openai_chat, sess.info.api_style); + + // Load it back by id. + var loaded = (try store.load(sess.info.id)).?; + defer loaded.deinit(); + try testing.expectEqual(@as(usize, 2), loaded.messages.items.len); + try testing.expectEqual(conversation_mod.MessageRole.user, loaded.messages.items[0].role); +} + +test "FileSystemJSONLStore load restores thinking signature origin from message stamp" { + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var catalog = try FileSystemJSONLStore.init(testing.allocator, io, sessions); + defer catalog.deinit(); + const store = catalog.store(); + + var sess = store.create(); + defer sess.info.deinit(testing.allocator); + + var conv = conversation_mod.Conversation.init(testing.allocator); + defer conv.deinit(); + try addUserText(&conv, "ping"); + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ + .text = try conversation_mod.textualBlockFromSlice(testing.allocator, "thinking..."), + .signature = try testing.allocator.dupe(u8, "sig123"), + } }, + .{ .Text = try conversation_mod.textualBlockFromSlice(testing.allocator, "pong") }, + }, null); + + const id: session_store_mod.WireIdentity = .{ + .api_style = .anthropic_messages, + .base_url = "https://api.anthropic.com", + .model = "claude-sonnet-4-20250514", + }; + var batch = [_]session_store_mod.PersistentMessage{ + .{ .message = conv.messages.items[0], .identity = id }, + .{ .message = conv.messages.items[1], .identity = id }, + }; + try sess.append(&batch); + + var loaded = (try store.load(sess.info.id)).?; + defer loaded.deinit(); + const thinking = loaded.messages.items[1].content.items[0].Thinking; + try testing.expect(thinking.signature_origin != null); + try testing.expect(thinking.signature_origin.?.matches( + .anthropic_messages, + "https://api.anthropic.com", + "claude-sonnet-4-20250514", + )); +} + +test "persistTurn: a message's own identity overrides the uniform persist identity" { + // Regression for the compaction re-stamping bug: a kept-verbatim turn + // produced by model A must persist with A's wire identity even when the + // turn is persisted under model B (the compaction model). persistTurn + // stamps the uniform identity only on messages that don't already carry + // one; a message with its own identity keeps it. + const io = testing.io; + + var td = try TmpSessionDir.init(testing.allocator); + defer td.deinit(testing.allocator); + const sessions = try std.fs.path.join(testing.allocator, &.{ td.abs_path, "sessions" }); + defer testing.allocator.free(sessions); + + var catalog = try FileSystemJSONLStore.init(testing.allocator, io, sessions); + defer catalog.deinit(); + const store = catalog.store(); + var sess = store.create(); + defer sess.info.deinit(testing.allocator); + + var conv = conversation_mod.Conversation.init(testing.allocator); + defer conv.deinit(); + // User turn carries no identity (will be stamped with the persist id). + try addUserText(&conv, "ping"); + // Assistant turn was produced by model A — stamp its identity directly, + // as a live turn / a reloaded turn would have. + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ + .text = try conversation_mod.textualBlockFromSlice(testing.allocator, "thinking..."), + .signature = try testing.allocator.dupe(u8, "sig123"), + } }, + .{ .Text = try conversation_mod.textualBlockFromSlice(testing.allocator, "pong") }, + }, null); + conv.messages.items[1].identity = try conversation_mod.dupeWireIdentity(testing.allocator, .{ + .api_style = .anthropic_messages, + .base_url = "https://api.anthropic.com", + .model = "claude-sonnet-4-20250514", + }); + + // Persist under model B (a different, "compaction" identity). + const persist_id: session_store_mod.WireIdentity = .{ + .api_style = .openai_chat, + .base_url = "https://api.openai.com/v1", + .model = "gpt-4o", + }; + try turn_persist.persistTurn(testing.allocator, &sess, &conv, 0, persist_id, &.{}); + + // Reload: the user turn took model B's identity; the assistant turn kept + // model A's — so its thinking signature replays to A, not B. + var loaded = (try store.load(sess.info.id)).?; + defer loaded.deinit(); + + const asst_origin = loaded.messages.items[1].content.items[0].Thinking.signature_origin.?; + try testing.expect(asst_origin.matches(.anthropic_messages, "https://api.anthropic.com", "claude-sonnet-4-20250514")); + try testing.expect(!asst_origin.matches(.openai_chat, "https://api.openai.com/v1", "gpt-4o")); + + const user_id = loaded.messages.items[0].identity.?; + try testing.expectEqual(session_store_mod.APIStyle.openai_chat, user_id.api_style); + try testing.expectEqualStrings("gpt-4o", user_id.model); +} diff --git a/src/http_helper.zig b/src/http_helper.zig new file mode 100644 index 0000000..a075d72 --- /dev/null +++ b/src/http_helper.zig @@ -0,0 +1,160 @@ +//! Small non-streaming HTTP request/response helper over the process-global +//! `std.http.Client`. The streaming providers deliberately avoid `fetch()` +//! (it buffers); auth flows want the opposite — request a URL, read the whole +//! response body into one buffer, inspect status + JSON. This is that. +//! +//! Used by `auth.zig` for OAuth device flows and token exchanges. Not part of +//! the streaming hot path, so simplicity wins: one request, full-body read, +//! owned result. + +const std = @import("std"); +const config_mod = @import("config.zig"); + +pub const Header = config_mod.Header; +pub const Method = std.http.Method; + +/// A fully-read HTTP response. `body` is owned by `allocator`. +pub const Response = struct { + allocator: std.mem.Allocator, + status: u16, + body: []u8, + + pub fn deinit(self: Response) void { + self.allocator.free(self.body); + } + + /// True for a 2xx status. + pub fn ok(self: Response) bool { + return self.status >= 200 and self.status < 300; + } +}; + +pub const Options = struct { + /// Caller headers (auth bearer, identity headers, …). `accept` and + /// `content-type` are added separately from the fields below. + headers: []const Header = &.{}, + /// Request body, or null for a bodiless request (e.g. a GET). + body: ?[]const u8 = null, + /// `content-type` header value when `body` is present. + content_type: ?[]const u8 = null, + /// `accept` header value. Defaults to JSON. + accept: ?[]const u8 = "application/json", + /// Upper bound on the response body read into memory. + max_body_bytes: usize = 1 << 20, +}; + +/// Perform one request and return the fully-read response. The caller owns +/// `Response.body` and must `deinit` it. Redirects are surfaced as their 3xx +/// status (not followed) so auth credentials never leak across a redirect. +pub fn request( + allocator: std.mem.Allocator, + client: *std.http.Client, + method: Method, + url: []const u8, + opts: Options, +) !Response { + const uri = try std.Uri.parse(url); + + var hdrs: std.ArrayList(std.http.Header) = .empty; + defer hdrs.deinit(allocator); + if (opts.accept) |a| try hdrs.append(allocator, .{ .name = "accept", .value = a }); + if (opts.content_type) |ct| { + if (opts.body != null) try hdrs.append(allocator, .{ .name = "content-type", .value = ct }); + } + for (opts.headers) |h| try hdrs.append(allocator, .{ .name = h.name, .value = h.value }); + + var req = try client.request(method, uri, .{ + .extra_headers = hdrs.items, + // Disable compression so we read the body without a decompressor. + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .keep_alive = false, + // Surface 3xx to us rather than following with auth headers attached. + .redirect_behavior = .unhandled, + }); + defer req.deinit(); + + if (opts.body) |body| { + req.transfer_encoding = .{ .content_length = body.len }; + var send_buf: [4096]u8 = undefined; + var bw = try req.sendBodyUnflushed(&send_buf); + try bw.writer.writeAll(body); + try bw.end(); + try req.connection.?.flush(); + } else { + try req.sendBodiless(); + } + + var redirect_buf: [2048]u8 = undefined; + var response = try req.receiveHead(&redirect_buf); + const status: u16 = @intFromEnum(response.head.status); + + var transfer_buf: [4096]u8 = undefined; + const body_reader = response.reader(&transfer_buf); + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(allocator); + var tmp: [4096]u8 = undefined; + while (true) { + const n = body_reader.readSliceShort(&tmp) catch break; + if (n == 0) break; + try out.appendSlice(allocator, tmp[0..n]); + if (out.items.len > opts.max_body_bytes) break; + } + + return .{ .allocator = allocator, .status = status, .body = try out.toOwnedSlice(allocator) }; +} + +// =========================================================================== +// JSON helpers (used by auth flows to read fields out of a response body) +// =========================================================================== + +/// Read a string at a dotted JSON path (e.g. `endpoints.api`) from a parsed +/// JSON value. Returns null if any segment is missing or the leaf is not a +/// string. Borrows from `root`. +pub fn jsonStringAtPath(root: std.json.Value, path: []const u8) ?[]const u8 { + const leaf = jsonAtPath(root, path) orelse return null; + return switch (leaf) { + .string => |s| s, + else => null, + }; +} + +/// Read an integer (unix-seconds expiry, etc.) at a dotted JSON path. Accepts +/// JSON integers and integer-valued floats. Returns null otherwise. +pub fn jsonIntAtPath(root: std.json.Value, path: []const u8) ?i64 { + const leaf = jsonAtPath(root, path) orelse return null; + return switch (leaf) { + .integer => |i| i, + .float => |f| @intFromFloat(f), + .number_string => |s| std.fmt.parseInt(i64, s, 10) catch null, + else => null, + }; +} + +/// Walk a dotted path through nested JSON objects. Returns the leaf value or +/// null if any object segment is missing. +pub fn jsonAtPath(root: std.json.Value, path: []const u8) ?std.json.Value { + var cur = root; + var it = std.mem.splitScalar(u8, path, '.'); + while (it.next()) |seg| { + switch (cur) { + .object => |o| cur = o.get(seg) orelse return null, + else => return null, + } + } + return cur; +} + +const t = std.testing; + +test "jsonAtPath: nested object string + int" { + const src = + \\{"token":"abc","expires_at":1700000000,"endpoints":{"api":"https://x"}} + ; + var parsed = try std.json.parseFromSlice(std.json.Value, t.allocator, src, .{}); + defer parsed.deinit(); + try t.expectEqualStrings("abc", jsonStringAtPath(parsed.value, "token").?); + try t.expectEqual(@as(i64, 1700000000), jsonIntAtPath(parsed.value, "expires_at").?); + try t.expectEqualStrings("https://x", jsonStringAtPath(parsed.value, "endpoints.api").?); + try t.expect(jsonStringAtPath(parsed.value, "endpoints.missing") == null); + try t.expect(jsonStringAtPath(parsed.value, "nope.deep") == null); +} diff --git a/src/image.zig b/src/image.zig new file mode 100644 index 0000000..26474d1 --- /dev/null +++ b/src/image.zig @@ -0,0 +1,358 @@ +//! Native image processing for tool-returned attachments. +//! +//! Two responsibilities: +//! +//! 1. `detectCodec` — identify an attachment's codec from its leading +//! bytes (magic numbers), not its file extension. +//! 2. `maybeResize` — bound raster images to `max_dim` on each side so a +//! single screenshot can't blow out the model's context. PDFs and +//! already-small images pass through untouched. +//! +//! Raster codecs go through the vendored stb single-header trio +//! (decode -> Mitchell resize -> re-encode in the *same* codec). WEBP is +//! decode-only (jebp), so a resized WEBP is re-encoded as JPEG. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const c = @cImport({ + @cInclude("stb_image.h"); + @cInclude("stb_image_resize2.h"); + @cInclude("stb_image_write.h"); + @cInclude("jebp.h"); +}); + +/// Longest side (px) allowed before we resize. pi uses 2000x2000. +pub const max_dim: u32 = 2000; +/// JPEG quality used when re-encoding (WEBP path, and JPEG inputs). +const jpeg_quality: c_int = 80; + +pub const Codec = enum { png, jpeg, gif, bmp, webp, pdf }; + +/// The MIME type string for a codec (static; do not free). +pub fn mediaTypeForCodec(codec: Codec) []const u8 { + return switch (codec) { + .png => "image/png", + .jpeg => "image/jpeg", + .gif => "image/gif", + .bmp => "image/bmp", + .webp => "image/webp", + .pdf => "application/pdf", + }; +} + +/// Detect a supported codec from leading bytes (magic numbers). +pub fn detectCodec(bytes: []const u8) ?Codec { + if (bytes.len >= 8 and std.mem.eql(u8, bytes[0..8], &.{ 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A })) + return .png; + if (bytes.len >= 3 and bytes[0] == 0xFF and bytes[1] == 0xD8 and bytes[2] == 0xFF) + return .jpeg; + if (bytes.len >= 6 and (std.mem.eql(u8, bytes[0..6], "GIF87a") or std.mem.eql(u8, bytes[0..6], "GIF89a"))) + return .gif; + if (bytes.len >= 2 and bytes[0] == 'B' and bytes[1] == 'M') + return .bmp; + // WEBP: "RIFF"????"WEBP" + if (bytes.len >= 12 and std.mem.eql(u8, bytes[0..4], "RIFF") and std.mem.eql(u8, bytes[8..12], "WEBP")) + return .webp; + if (bytes.len >= 5 and std.mem.eql(u8, bytes[0..5], "%PDF-")) + return .pdf; + return null; +} + +/// Map a media-type string back to a codec (for callers that already have +/// the MIME string). Returns null for unsupported types. +pub fn codecForMediaType(media_type: []const u8) ?Codec { + if (std.mem.eql(u8, media_type, "image/png")) return .png; + if (std.mem.eql(u8, media_type, "image/jpeg")) return .jpeg; + if (std.mem.eql(u8, media_type, "image/gif")) return .gif; + if (std.mem.eql(u8, media_type, "image/bmp")) return .bmp; + if (std.mem.eql(u8, media_type, "image/webp")) return .webp; + if (std.mem.eql(u8, media_type, "application/pdf")) return .pdf; + return null; +} + +/// The result of `maybeResize`. `media_type` is the MIME type of `data` +/// (may differ from the input when a WEBP was re-encoded as JPEG). +/// `data` is always an owned slice the caller must free. +pub const Processed = struct { + media_type: []const u8, // static string, do not free + data: []u8, // owned by `allocator` +}; + +/// Full attachment pipeline for a tool-returned media part: resolve the +/// media type (detecting from magic bytes when `hint` is null), then +/// resize. Returns owned raw bytes + the resolved media type. +/// +/// Errors `error.UnknownMediaType` when neither the hint nor magic-byte +/// detection recognizes the bytes — the caller decides how to surface +/// that (e.g. drop the attachment, or fall back to text). +pub fn process(allocator: Allocator, bytes: []const u8, hint: ?[]const u8) !Processed { + const codec = blk: { + if (hint) |h| { + if (codecForMediaType(h)) |hinted| break :blk hinted; + } + break :blk detectCodec(bytes) orelse return error.UnknownMediaType; + }; + return maybeResize(allocator, bytes, codec); +} + +/// Resize `bytes` so neither dimension exceeds `max_dim`, preserving the +/// input codec where possible. Returns an owned copy of the (possibly +/// unchanged) bytes plus the resulting media type. +/// +/// - PDF: returned verbatim (a copy), media type unchanged. +/// - raster <= max_dim on both sides: returned verbatim (a copy) — we +/// skip the decode/encode round-trip to avoid quality loss + CPU. +/// - stb-supported raster larger than max_dim: decode -> resize -> same +/// codec. +/// - WEBP larger than max_dim: jebp decode -> resize -> JPEG. +pub fn maybeResize(allocator: Allocator, bytes: []const u8, codec: Codec) !Processed { + const media_type = mediaTypeForCodec(codec); + + if (codec == .pdf) + return .{ .media_type = media_type, .data = try allocator.dupe(u8, bytes) }; + + if (codec == .webp) return resizeWebp(allocator, bytes, media_type); + return resizeStb(allocator, bytes, media_type, codec); +} + +/// Probe a raster image's dimensions without fully decoding. Returns null +/// on failure. +fn probeDims(bytes: []const u8) ?struct { w: u32, h: u32 } { + var w: c_int = 0; + var h: c_int = 0; + var comp: c_int = 0; + const ok = c.stbi_info_from_memory(bytes.ptr, @intCast(bytes.len), &w, &h, &comp); + if (ok == 0 or w <= 0 or h <= 0) return null; + return .{ .w = @intCast(w), .h = @intCast(h) }; +} + +/// Compute target dimensions that fit within `max_dim` x `max_dim` while +/// preserving aspect ratio. Returns null when no resize is needed. +fn targetDims(w: u32, h: u32) ?struct { w: u32, h: u32 } { + if (w <= max_dim and h <= max_dim) return null; + const wf: f64 = @floatFromInt(w); + const hf: f64 = @floatFromInt(h); + const scale = @min(@as(f64, @floatFromInt(max_dim)) / wf, @as(f64, @floatFromInt(max_dim)) / hf); + const nw: u32 = @max(1, @as(u32, @intFromFloat(@round(wf * scale)))); + const nh: u32 = @max(1, @as(u32, @intFromFloat(@round(hf * scale)))); + return .{ .w = nw, .h = nh }; +} + +const StbWriteCtx = struct { + list: *std.ArrayList(u8), + allocator: Allocator, + failed: bool = false, +}; + +fn stbWriteCb(ctx_opaque: ?*anyopaque, data: ?*anyopaque, size: c_int) callconv(.c) void { + const ctx: *StbWriteCtx = @ptrCast(@alignCast(ctx_opaque.?)); + if (ctx.failed or size <= 0) return; + const bytes: [*]const u8 = @ptrCast(data.?); + ctx.list.appendSlice(ctx.allocator, bytes[0..@intCast(size)]) catch { + ctx.failed = true; + }; +} + +fn resizeStb(allocator: Allocator, bytes: []const u8, media_type: []const u8, codec: Codec) !Processed { + const dims = probeDims(bytes) orelse + // Can't parse it; pass through rather than fail the read. + return .{ .media_type = media_type, .data = try allocator.dupe(u8, bytes) }; + + const target = targetDims(dims.w, dims.h) orelse + return .{ .media_type = media_type, .data = try allocator.dupe(u8, bytes) }; + + // Decode to RGBA (4 channels) for a uniform resize path. + var w: c_int = 0; + var h: c_int = 0; + var comp: c_int = 0; + const pixels = c.stbi_load_from_memory(bytes.ptr, @intCast(bytes.len), &w, &h, &comp, 4); + if (pixels == null) + return .{ .media_type = media_type, .data = try allocator.dupe(u8, bytes) }; + defer c.stbi_image_free(pixels); + + const out_pixels = try allocator.alloc(u8, @as(usize, target.w) * @as(usize, target.h) * 4); + defer allocator.free(out_pixels); + + const res = c.stbir_resize_uint8_srgb( + pixels, + w, + h, + 0, + out_pixels.ptr, + @intCast(target.w), + @intCast(target.h), + 0, + c.STBIR_RGBA, + ); + if (res == null) + return error.ResizeFailed; + + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(allocator); + var ctx = StbWriteCtx{ .list = &out, .allocator = allocator }; + + const tw: c_int = @intCast(target.w); + const th: c_int = @intCast(target.h); + const ok = switch (codec) { + .png => c.stbi_write_png_to_func(stbWriteCb, &ctx, tw, th, 4, out_pixels.ptr, tw * 4), + .bmp => c.stbi_write_bmp_to_func(stbWriteCb, &ctx, tw, th, 4, out_pixels.ptr), + // stb has no GIF encoder; re-encode resized GIFs as PNG (lossless). + .gif => c.stbi_write_png_to_func(stbWriteCb, &ctx, tw, th, 4, out_pixels.ptr, tw * 4), + .jpeg => c.stbi_write_jpg_to_func(stbWriteCb, &ctx, tw, th, 4, out_pixels.ptr, jpeg_quality), + else => unreachable, + }; + if (ok == 0 or ctx.failed) return error.EncodeFailed; + + const result_media: []const u8 = switch (codec) { + .gif => "image/png", // re-encoded + else => media_type, + }; + return .{ .media_type = result_media, .data = try out.toOwnedSlice(allocator) }; +} + +fn resizeWebp(allocator: Allocator, bytes: []const u8, media_type: []const u8) !Processed { + var img: c.jebp_image_t = std.mem.zeroes(c.jebp_image_t); + // Peek at the header first to learn dimensions cheaply. + if (c.jebp_decode_size(&img, bytes.len, bytes.ptr) != c.JEBP_OK) + return .{ .media_type = media_type, .data = try allocator.dupe(u8, bytes) }; + + const w: u32 = @intCast(img.width); + const h: u32 = @intCast(img.height); + const target = targetDims(w, h) orelse + return .{ .media_type = media_type, .data = try allocator.dupe(u8, bytes) }; + + // Full decode to RGBA (jebp_color_t is 4 contiguous bytes per pixel). + if (c.jebp_decode(&img, bytes.len, bytes.ptr) != c.JEBP_OK) + return .{ .media_type = media_type, .data = try allocator.dupe(u8, bytes) }; + defer c.jebp_free_image(&img); + + const src: [*]const u8 = @ptrCast(img.pixels); + const out_pixels = try allocator.alloc(u8, @as(usize, target.w) * @as(usize, target.h) * 4); + defer allocator.free(out_pixels); + + const res = c.stbir_resize_uint8_srgb( + src, + @intCast(w), + @intCast(h), + 0, + out_pixels.ptr, + @intCast(target.w), + @intCast(target.h), + 0, + c.STBIR_RGBA, + ); + if (res == null) return error.ResizeFailed; + + // TODO: when the source WEBP has an alpha layer, re-encoding to JPEG + // flattens transparency, which can look wrong for screenshots and + // diagrams. Consider re-encoding to PNG when alpha is present. For now + // we always emit JPEG: there is no small single-header WEBP encoder, + // and token size matters more than fidelity for LLM input. + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(allocator); + var ctx = StbWriteCtx{ .list = &out, .allocator = allocator }; + const tw: c_int = @intCast(target.w); + const th: c_int = @intCast(target.h); + const ok = c.stbi_write_jpg_to_func(stbWriteCb, &ctx, tw, th, 4, out_pixels.ptr, jpeg_quality); + if (ok == 0 or ctx.failed) return error.EncodeFailed; + + return .{ .media_type = "image/jpeg", .data = try out.toOwnedSlice(allocator) }; +} + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const testing = std.testing; + +test "detectCodec + mediaTypeForCodec - magic bytes" { + try testing.expectEqualStrings("image/png", mediaTypeForCodec(detectCodec(&.{ 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A }).?)); + try testing.expectEqualStrings("image/jpeg", mediaTypeForCodec(detectCodec(&.{ 0xFF, 0xD8, 0xFF, 0xE0 }).?)); + try testing.expectEqualStrings("image/gif", mediaTypeForCodec(detectCodec("GIF89a....").?)); + try testing.expectEqualStrings("image/bmp", mediaTypeForCodec(detectCodec("BM....").?)); + try testing.expectEqualStrings("application/pdf", mediaTypeForCodec(detectCodec("%PDF-1.7").?)); + const webp = "RIFF" ++ &[_]u8{ 0, 0, 0, 0 } ++ "WEBP"; + try testing.expectEqualStrings("image/webp", mediaTypeForCodec(detectCodec(webp).?)); + try testing.expect(detectCodec("not an image") == null); + try testing.expect(detectCodec(&.{0x89}) == null); +} + +test "targetDims - skip when small, scale when large preserving aspect" { + try testing.expect(targetDims(100, 100) == null); + try testing.expect(targetDims(max_dim, max_dim) == null); + const t = targetDims(4000, 2000).?; + try testing.expectEqual(@as(u32, 2000), t.w); + try testing.expectEqual(@as(u32, 1000), t.h); + const t2 = targetDims(1000, 8000).?; + try testing.expectEqual(@as(u32, 250), t2.w); + try testing.expectEqual(@as(u32, 2000), t2.h); +} + +test "process - detects type from raw bytes when hint absent" { + const a = testing.allocator; + // A tiny PNG (header + IHDR enough for stbi_info) — but simplest is to + // round-trip an stb-encoded small PNG and feed it with no hint. + const w: c_int = 4; + const h: c_int = 4; + var px: [4 * 4 * 4]u8 = undefined; + @memset(&px, 0x40); + var png: std.ArrayList(u8) = .empty; + defer png.deinit(a); + var ctx = StbWriteCtx{ .list = &png, .allocator = a }; + try testing.expect(c.stbi_write_png_to_func(stbWriteCb, &ctx, w, h, 4, &px, w * 4) != 0); + + const out = try process(a, png.items, null); + defer a.free(out.data); + try testing.expectEqualStrings("image/png", out.media_type); + + // Unknown bytes -> error so the caller can fall back / drop. + try testing.expectError(error.UnknownMediaType, process(a, "totally not an image", null)); +} + +test "maybeResize - PDF passes through unchanged" { + const a = testing.allocator; + const pdf = "%PDF-1.7\nfake pdf body"; + const out = try maybeResize(a, pdf, .pdf); + defer a.free(out.data); + try testing.expectEqualStrings("application/pdf", out.media_type); + try testing.expectEqualStrings(pdf, out.data); +} + +test "maybeResize - small PNG round-trips, large PNG shrinks and stays PNG" { + const a = testing.allocator; + + // Build a small (8x8) RGBA PNG via stb and confirm pass-through. + const small_w: c_int = 8; + const small_h: c_int = 8; + var small_px: [8 * 8 * 4]u8 = undefined; + for (&small_px, 0..) |*b, i| b.* = @truncate(i); + var small_png: std.ArrayList(u8) = .empty; + defer small_png.deinit(a); + var sctx = StbWriteCtx{ .list = &small_png, .allocator = a }; + try testing.expect(c.stbi_write_png_to_func(stbWriteCb, &sctx, small_w, small_h, 4, &small_px, small_w * 4) != 0); + + const small_out = try maybeResize(a, small_png.items, .png); + defer a.free(small_out.data); + try testing.expectEqualStrings("image/png", small_out.media_type); + // Small image is returned verbatim (byte-identical copy). + try testing.expectEqualSlices(u8, small_png.items, small_out.data); + + // Build a large (2400x100) PNG and confirm it shrinks to <= max_dim. + const big_w: c_int = 2400; + const big_h: c_int = 100; + const big_px = try a.alloc(u8, @as(usize, @intCast(big_w * big_h * 4))); + defer a.free(big_px); + @memset(big_px, 0x7F); + var big_png: std.ArrayList(u8) = .empty; + defer big_png.deinit(a); + var bctx = StbWriteCtx{ .list = &big_png, .allocator = a }; + try testing.expect(c.stbi_write_png_to_func(stbWriteCb, &bctx, big_w, big_h, 4, big_px.ptr, big_w * 4) != 0); + + const big_out = try maybeResize(a, big_png.items, .png); + defer a.free(big_out.data); + try testing.expectEqualStrings("image/png", big_out.media_type); + const dims = probeDims(big_out.data).?; + try testing.expectEqual(@as(u32, 2000), dims.w); + try testing.expect(dims.w <= max_dim and dims.h <= max_dim); +} diff --git a/src/null_store.zig b/src/null_store.zig new file mode 100644 index 0000000..1f3ab74 --- /dev/null +++ b/src/null_store.zig @@ -0,0 +1,108 @@ +//! `NullStore`: a no-op `SessionStore` for embedders who opt out of +//! persistence (and the default backing for an `Agent` constructed without +//! a real store). +//! +//! Every append is dropped. `load` returns null. `list` returns an empty +//! slice. `create`/`resolve`/`latest` mint/return empty handles. The struct +//! holds an allocator (needed to satisfy the `SessionInfo` ownership +//! contract for the empty handles it mints) and is trivially copyable. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const session_store_mod = @import("session_store.zig"); +const conversation_mod = @import("conversation.zig"); + +const SessionStore = session_store_mod.SessionStore; +const Session = session_store_mod.Session; +const SessionInfo = session_store_mod.SessionInfo; +const PersistentMessage = session_store_mod.PersistentMessage; +const Conversation = conversation_mod.Conversation; + +pub const NullStore = struct { + allocator: Allocator, + + pub fn init(allocator: Allocator) NullStore { + return .{ .allocator = allocator }; + } + + fn emptyInfo(self: *NullStore) SessionInfo { + const a = self.allocator; + return .{ + .id = a.dupe(u8, "") catch "", + .created = a.dupe(u8, "") catch "", + .modified = a.dupe(u8, "") catch "", + .message_count = 0, + .last_user_message = a.dupe(u8, "") catch "", + .api_style = .openai_chat, + .base_url = a.dupe(u8, "") catch "", + .model = a.dupe(u8, "") catch "", + .reasoning = .default, + }; + } + + fn createVT(ctx: *anyopaque) Session { + const self: *NullStore = @ptrCast(@alignCast(ctx)); + return .{ .info = self.emptyInfo(), .store = self.store() }; + } + + fn listVT(ctx: *anyopaque) anyerror![]SessionInfo { + const self: *NullStore = @ptrCast(@alignCast(ctx)); + return self.allocator.alloc(SessionInfo, 0); + } + + fn freeSessionInfosVT(ctx: *anyopaque, infos: []SessionInfo) void { + const self: *NullStore = @ptrCast(@alignCast(ctx)); + for (infos) |i| i.deinit(self.allocator); + self.allocator.free(infos); + } + + fn resolveVT(_: *anyopaque, _: []const u8) anyerror!?Session { + return null; + } + + fn latestVT(_: *anyopaque) anyerror!?Session { + return null; + } + + fn loadVT(_: *anyopaque, _: []const u8) anyerror!?Conversation { + return null; + } + + fn appendMessagesVT(_: *anyopaque, _: []const u8, _: []PersistentMessage) anyerror!void { + // Drop everything. The PersistentMessages borrow in-memory data + // owned by the caller (the conversation); nothing to free here. + } + + const vtable: SessionStore.VTable = .{ + .create = createVT, + .list = listVT, + .freeSessionInfos = freeSessionInfosVT, + .resolve = resolveVT, + .latest = latestVT, + .load = loadVT, + .appendMessages = appendMessagesVT, + }; + + /// Wrap this `NullStore` as a `SessionStore`. The handle borrows + /// `self`; `self` must outlive it. + pub fn store(self: *NullStore) SessionStore { + return .{ .ptr = self, .vtable = &vtable }; + } +}; + +const testing = std.testing; + +test "NullStore: appends are dropped and load returns null" { + var ns = NullStore.init(testing.allocator); + const s = ns.store(); + + try s.appendMessages("sid", &.{}); + try testing.expect((try s.load("sid")) == null); + try testing.expect((try s.resolve("sid")) == null); + try testing.expect((try s.latest()) == null); + + const infos = try s.list(); + defer s.freeSessionInfos(infos); + try testing.expectEqual(@as(usize, 0), infos.len); +} diff --git a/src/openai_chat_json.zig b/src/openai_chat_json.zig new file mode 100644 index 0000000..f00c97f --- /dev/null +++ b/src/openai_chat_json.zig @@ -0,0 +1,1067 @@ +//! OpenAI Chat Completions JSON serialization and parsing. +//! +//! Two responsibilities: +//! 1. Serialize a `Conversation` into the OpenAI Chat Completions request body. +//! 2. Parse one streaming SSE event's JSON payload into a strongly-typed +//! `StreamDelta` that the provider can consume. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Writer = std.Io.Writer; +const conversation = @import("conversation.zig"); +const config_mod = @import("config.zig"); +const tool_registry_mod = @import("tool_registry.zig"); +const writeRawJson = @import("provider.zig").writeRawJson; + +/// A single parsed streaming chunk. Fields are populated only when present +/// in the wire payload; null fields signal "not in this chunk". +/// +/// `content` and `reasoning_content` slices are borrowed from the parsed +/// JSON value, which is owned by the caller's `std.json.Parsed`. +pub const StreamDelta = struct { + role: ?[]const u8 = null, + content: ?[]const u8 = null, + reasoning_content: ?[]const u8 = null, + finish_reason: ?[]const u8 = null, + tool_calls: []const ToolCallDelta = &.{}, + /// Mid-stream error reported by the provider. OpenAI-compatible + /// endpoints sometimes return HTTP 200 with `data: {"error":{...}}` + /// embedded in the SSE stream instead of a non-2xx response (notably + /// some OpenRouter / MiniMax / DeepSeek paths, and occasionally OpenAI + /// itself on transient overload). When present, the provider must + /// treat the turn as failed. + error_message: ?[]const u8 = null, + error_type: ?[]const u8 = null, + /// Token usage from the final chunk's top-level `usage` block. + /// Only present on the final chunk when the request was sent with + /// `stream_options.include_usage: true`. Earlier chunks have null. + usage: ?StreamUsage = null, +}; + +/// Token usage payload from OpenAI's terminating SSE chunk. Field +/// semantics: +/// +/// - `prompt_tokens`: total input tokens, **including** cached tokens. +/// - `completion_tokens`: output tokens (including reasoning tokens). +/// - `cached_prompt_tokens`: subset of `prompt_tokens` served from +/// the prompt cache (billed at a discount). +/// - `reasoning_tokens`: subset of `completion_tokens` spent on +/// internal reasoning (o-series models). +/// +/// To map to `Usage`: `input = prompt_tokens - cached_prompt_tokens`, +/// `cache_read = cached_prompt_tokens`, `output = completion_tokens`, +/// `reasoning = reasoning_tokens`, `cache_write = 0` (OpenAI doesn't +/// bill a cache-write premium). +pub const StreamUsage = struct { + prompt_tokens: ?u64 = null, + completion_tokens: ?u64 = null, + cached_prompt_tokens: ?u64 = null, + reasoning_tokens: ?u64 = null, +}; + +/// A single entry from a streaming `tool_calls` array. Multiple parallel +/// tool calls are distinguished by their `index`; identity fields (`id`, +/// `name`) typically arrive only on the first delta for each index, while +/// `arguments` arrives incrementally across many deltas. +pub const ToolCallDelta = struct { + index: usize, + id: ?[]const u8 = null, + name: ?[]const u8 = null, + arguments: ?[]const u8 = null, +}; + +/// Serialize a Conversation into a `chat/completions` request body. +/// +/// The caller owns the returned slice (allocated with `allocator`). +pub fn serializeRequest( + allocator: Allocator, + cfg: *const config_mod.OpenAIChatConfig, + conv: *const conversation.Conversation, + tools: *const tool_registry_mod.ToolRegistry, +) ![]u8 { + var aw: Writer.Allocating = .init(allocator); + errdefer aw.deinit(); + + var s: std.json.Stringify = .{ .writer = &aw.writer }; + + try s.beginObject(); + + try s.objectField("model"); + try s.write(cfg.model); + + try s.objectField("stream"); + try s.write(true); + + try s.objectField("max_completion_tokens"); + try s.write(cfg.max_tokens); + + // Ask for the final-chunk usage block. Without this the server + // sends `usage: null` and we can't stamp token counts on the + // session log. Most OpenAI-compatible proxies accept this; ones + // that don't will either ignore it or 400 — in the latter case + // the user has bigger problems than missing token counts. + try s.objectField("stream_options"); + try s.beginObject(); + try s.objectField("include_usage"); + try s.write(true); + try s.endObject(); + + switch (cfg.reasoning) { + .default => {}, + .off => { + try s.objectField("reasoning_effort"); + try s.write("none"); + }, + .minimal, .low, .medium, .high => |eff| { + try s.objectField("reasoning_effort"); + try s.write(@tagName(eff)); + }, + } + + if (tools.count() > 0) { + try s.objectField("tools"); + try s.beginArray(); + var it = tools.toolsForLLM(); + while (it.next()) |t| { + try s.beginObject(); + try s.objectField("type"); + try s.write("function"); + try s.objectField("function"); + try s.beginObject(); + try s.objectField("name"); + // `t.decl.name` is already wire-encoded by `toolsForLLM`. + try s.write(t.decl.name); + try s.objectField("description"); + try s.write(t.decl.description); + try s.objectField("parameters"); + // Emit the tool's JSON Schema verbatim. + try writeRawJson(&s, t.decl.schema_json); + try s.endObject(); + try s.endObject(); + } + try s.endArray(); + } + + try s.objectField("messages"); + try s.beginArray(); + // Hoist the effective system prompt to the front as separate, + // individually-positioned `system` messages (one per surviving block, + // in derivation order). Keeping them distinct preserves block-level + // addressability for `/tree`-style truncation — we deliberately do NOT + // concatenate them the way Anthropic's single-string format forces. + var sys_blocks = try conversation.effectiveSystemBlocks(allocator, conv.messages.items); + defer sys_blocks.deinit(allocator); + for (sys_blocks.items) |text| { + try s.beginObject(); + try s.objectField("role"); + try s.write("system"); + try s.objectField("content"); + try s.write(text); + try s.endObject(); + } + // Then every non-system message, in its original order. If the + // conversation has been compacted, only the latest compaction summary + // and the messages after it are active; the superseded prefix is + // dropped. + for (conversation.activeMessageWindow(conv.messages.items)) |msg| { + if (msg.role == .system) continue; + try writeMessage(&s, msg, allocator); + } + try s.endArray(); + + try s.endObject(); + + return try aw.toOwnedSlice(); +} + +/// Emit one `Conversation.Message` as one or more wire-level messages. +/// +/// OpenAI's wire format is awkward here: a single logical `user` turn that +/// contains ToolResult blocks must be split into separate top-level +/// `{"role":"tool",...}` messages (one per ToolResult). A single assistant +/// turn that mixes Text and ToolUse becomes one assistant message with both +/// a `content` string and a `tool_calls` array. +fn writeMessage(s: *std.json.Stringify, msg: conversation.Message, allocator: Allocator) !void { + // User messages that carry ToolResult blocks fan out into one + // `role:"tool"` message per block. Any plain Text blocks in the same + // user message become a separate user message after the tool messages. + if (msg.role == .user) { + var has_tool_result = false; + for (msg.content.items) |b| { + if (b == .ToolResult) { + has_tool_result = true; + break; + } + } + if (has_tool_result) { + // OpenAI forbids images in `role:"tool"` messages. Each tool + // result emits a `role:"tool"` message carrying only its text + // (plus a short note when media is present), and any media + // rides along afterward in a synthetic `role:"user"` message. + var any_media = false; + for (msg.content.items) |block| { + if (block != .ToolResult) continue; + const tr = block.ToolResult; + if (tr.hasMedia()) any_media = true; + try s.beginObject(); + try s.objectField("role"); + try s.write("tool"); + try s.objectField("tool_call_id"); + try s.write(tr.tool_use_id); + try s.objectField("content"); + var tbuf: std.ArrayList(u8) = .empty; + defer tbuf.deinit(allocator); + try tr.appendTextInto(allocator, &tbuf); + if (tr.hasMedia()) { + if (tbuf.items.len > 0) try tbuf.append(allocator, '\n'); + try tbuf.appendSlice(allocator, "[attachment(s) provided in the following user message]"); + } + try s.write(tbuf.items); + try s.endObject(); + } + // Synthetic user message holding the media as image_url + // data-URL parts (OpenAI's only image channel). + if (any_media) { + try s.beginObject(); + try s.objectField("role"); + try s.write("user"); + try s.objectField("content"); + try s.beginArray(); + for (msg.content.items) |block| { + if (block != .ToolResult) continue; + for (block.ToolResult.parts.items) |part| { + if (part != .media) continue; + const m = part.media; + try s.beginObject(); + try s.objectField("type"); + try s.write("image_url"); + try s.objectField("image_url"); + try s.beginObject(); + try s.objectField("url"); + var url_buf: std.ArrayList(u8) = .empty; + defer url_buf.deinit(allocator); + try url_buf.appendSlice(allocator, "data:"); + try url_buf.appendSlice(allocator, m.media_type); + try url_buf.appendSlice(allocator, ";base64,"); + try url_buf.appendSlice(allocator, m.data.items); + try s.write(url_buf.items); + try s.endObject(); + try s.endObject(); + } + } + try s.endArray(); + try s.endObject(); + } + // Trailing plain Text blocks (rare in practice) ride along as + // a follow-up user message so we don't lose them. + var has_text = false; + for (msg.content.items) |b| { + if (b == .Text) { + has_text = true; + break; + } + } + if (!has_text) return; + try s.beginObject(); + try s.objectField("role"); + try s.write("user"); + try s.objectField("content"); + var buf: std.ArrayList(u8) = .empty; + defer buf.deinit(allocator); + try concatTextBlocks(msg.content.items, &buf, allocator); + try s.write(buf.items); + try s.endObject(); + return; + } + } + + try s.beginObject(); + try s.objectField("role"); + try s.write(@tagName(msg.role)); + + // Assistant messages may carry ToolUse blocks. The wire shape is a + // `tool_calls` array alongside `content`. OpenAI requires `content` + // to be either a string or null — we always emit a string (possibly + // empty) so JSON shape is predictable. + try s.objectField("content"); + var buf: std.ArrayList(u8) = .empty; + defer buf.deinit(allocator); + try concatTextBlocks(msg.content.items, &buf, allocator); + try s.write(buf.items); + + if (msg.role == .assistant) { + var n_tool_uses: usize = 0; + for (msg.content.items) |b| if (b == .ToolUse) { + n_tool_uses += 1; + }; + if (n_tool_uses > 0) { + try s.objectField("tool_calls"); + try s.beginArray(); + for (msg.content.items) |block| { + if (block != .ToolUse) continue; + const tu = block.ToolUse; + try s.beginObject(); + try s.objectField("id"); + try s.write(tu.id); + try s.objectField("type"); + try s.write("function"); + try s.objectField("function"); + try s.beginObject(); + try s.objectField("name"); + // Replayed assistant tool_use. The conversation stores the + // internal (dotted) name; encode `.` -> `__` so it matches + // what we advertise in `tools`. + var name_buf: [tool_registry_mod.max_wire_name_len]u8 = undefined; + try s.write(tool_registry_mod.encodeName(&name_buf, tu.name)); + try s.objectField("arguments"); + // `arguments` is a string carrying JSON, per the OpenAI + // wire format — not a nested object. + try s.write(tu.input.items); + try s.endObject(); + try s.endObject(); + } + try s.endArray(); + } + } + + try s.endObject(); +} + +fn concatTextBlocks( + blocks: []const conversation.ContentBlock, + out: *std.ArrayList(u8), + allocator: Allocator, +) !void { + for (blocks) |block| { + switch (block) { + .Text => |tb| try out.appendSlice(allocator, tb.items), + // A compaction summary is the synthetic seed text standing in + // for a compacted prefix; emit it as ordinary message text. + .CompactionSummary => |cs| try out.appendSlice(allocator, cs.text.items), + // Thinking, ToolUse, ToolResult: handled elsewhere or dropped. + else => {}, + } + } +} + +/// Parse a single SSE event payload (the JSON object that follows "data: "). +/// +/// Returns a `StreamDelta` borrowed from `parsed`. The caller must keep +/// `parsed` alive for as long as the delta's slices are in use, then call +/// `parsed.deinit()`. +pub const ParsedDelta = struct { + parsed: std.json.Parsed(std.json.Value), + delta: StreamDelta, + /// Owned buffer holding the per-call deltas referenced by + /// `delta.tool_calls`. Freed by `deinit` along with `parsed`. + tool_calls_buf: ?[]ToolCallDelta = null, + allocator: Allocator, + + pub fn deinit(self: *ParsedDelta) void { + if (self.tool_calls_buf) |b| self.allocator.free(b); + self.parsed.deinit(); + } +}; + +pub fn parseStreamEvent(allocator: Allocator, payload: []const u8) !ParsedDelta { + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, payload, .{}); + errdefer parsed.deinit(); + + var delta: StreamDelta = .{}; + + const root = parsed.value; + if (root != .object) return .{ .parsed = parsed, .delta = delta, .allocator = allocator }; + + // Top-level `error` field. Some providers (and OpenAI itself on rare + // mid-stream failures) emit `data: {"error":{"message":...,"type":...}}` + // with HTTP 200, so we look for this BEFORE the choices array. + // Top-level `usage` field on the terminating chunk. Independent of + // the (often empty) choices array. + if (root.object.get("usage")) |u| { + if (u == .object) { + var su: StreamUsage = .{}; + su.prompt_tokens = readOptU64(u.object, "prompt_tokens"); + su.completion_tokens = readOptU64(u.object, "completion_tokens"); + if (u.object.get("prompt_tokens_details")) |ptd| { + if (ptd == .object) { + su.cached_prompt_tokens = readOptU64(ptd.object, "cached_tokens"); + } + } + if (u.object.get("completion_tokens_details")) |ctd| { + if (ctd == .object) { + su.reasoning_tokens = readOptU64(ctd.object, "reasoning_tokens"); + } + } + delta.usage = su; + } + } + + if (root.object.get("error")) |e| { + switch (e) { + .object => |obj| { + if (obj.get("message")) |m| if (m == .string) { + delta.error_message = m.string; + }; + if (obj.get("type")) |t| if (t == .string) { + delta.error_type = t.string; + }; + }, + .string => |s| { + // Some providers send a bare string. Surface it as the + // message so callers can still report something useful. + delta.error_message = s; + }, + else => {}, + } + } + + const choices_v = root.object.get("choices") orelse return .{ .parsed = parsed, .delta = delta, .allocator = allocator }; + if (choices_v != .array or choices_v.array.items.len == 0) { + return .{ .parsed = parsed, .delta = delta, .allocator = allocator }; + } + const choice = choices_v.array.items[0]; + if (choice != .object) return .{ .parsed = parsed, .delta = delta, .allocator = allocator }; + + if (choice.object.get("finish_reason")) |fr| { + if (fr == .string) delta.finish_reason = fr.string; + } + + var tool_calls_buf: ?[]ToolCallDelta = null; + errdefer if (tool_calls_buf) |b| allocator.free(b); + + if (choice.object.get("delta")) |d| { + if (d == .object) { + if (d.object.get("role")) |r| { + if (r == .string) delta.role = r.string; + } + if (d.object.get("content")) |c| { + if (c == .string) delta.content = c.string; + } + // Reasoning content lives under one of these names depending on + // the provider. We accept either. + if (d.object.get("reasoning_content")) |rc| { + if (rc == .string) delta.reasoning_content = rc.string; + } else if (d.object.get("reasoning")) |rc| { + if (rc == .string) delta.reasoning_content = rc.string; + } + if (d.object.get("tool_calls")) |tcs| { + if (tcs == .array and tcs.array.items.len > 0) { + const buf = try allocator.alloc(ToolCallDelta, tcs.array.items.len); + tool_calls_buf = buf; + for (tcs.array.items, 0..) |tc, i| { + var entry: ToolCallDelta = .{ .index = 0 }; + if (tc == .object) { + if (tc.object.get("index")) |iv| { + if (iv == .integer and iv.integer >= 0) { + entry.index = @intCast(iv.integer); + } + } + if (tc.object.get("id")) |idv| { + if (idv == .string) entry.id = idv.string; + } + if (tc.object.get("function")) |fnv| { + if (fnv == .object) { + if (fnv.object.get("name")) |nv| { + if (nv == .string) entry.name = nv.string; + } + if (fnv.object.get("arguments")) |av| { + if (av == .string) entry.arguments = av.string; + } + } + } + } + buf[i] = entry; + } + delta.tool_calls = buf; + } + } + } + } + + return .{ + .parsed = parsed, + .delta = delta, + .tool_calls_buf = tool_calls_buf, + .allocator = allocator, + }; +} + +fn readOptU64(obj: std.json.ObjectMap, name: []const u8) ?u64 { + const v = obj.get(name) orelse return null; + if (v != .integer) return null; + if (v.integer < 0) return null; + return @intCast(v.integer); +} + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const testing = std.testing; + +fn testConfig(model: []const u8) config_mod.OpenAIChatConfig { + return .{ + .api_key = "k", + .base_url = "u", + .model = model, + }; +} + +/// Caller deinits. +fn emptyTools() tool_registry_mod.ToolRegistry { + return tool_registry_mod.ToolRegistry.init(testing.allocator); +} + +/// Test helper: append a single-text user message. `addUserMessage` now +/// takes a block slice (symmetric with `addAssistantMessage`); this wraps +/// the common plain-text case the tests below use. +fn addUserText(conv: *conversation.Conversation, text: []const u8) !void { + const tb = try conversation.textualBlockFromSlice(conv.allocator, text); + var block: conversation.ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + +test "serializeRequest - system + user" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("You are helpful."); + try addUserText(&conv, "Hello!"); + + const cfg = testConfig("gpt-4o"); + var tools = emptyTools(); + defer tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const root = parsed.value.object; + try testing.expectEqualStrings("gpt-4o", root.get("model").?.string); + try testing.expect(root.get("stream").?.bool); + // reasoning_effort is omitted when set to .default. + try testing.expect(root.get("reasoning_effort") == null); + // No tools registered — the `tools` field must be omitted entirely. + try testing.expect(root.get("tools") == null); + + const msgs = root.get("messages").?.array.items; + try testing.expectEqual(@as(usize, 2), msgs.len); + try testing.expectEqualStrings("system", msgs[0].object.get("role").?.string); + try testing.expectEqualStrings("You are helpful.", msgs[0].object.get("content").?.string); + try testing.expectEqualStrings("user", msgs[1].object.get("role").?.string); + try testing.expectEqualStrings("Hello!", msgs[1].object.get("content").?.string); +} + +test "serializeRequest - multiple system blocks hoisted as separate leading messages" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("seed"); + try addUserText(&conv, "Hello!"); + try conv.addSystemMessage("mid-conversation append"); + try addUserText(&conv, "again"); + + const cfg = testConfig("gpt-4o"); + var tools = emptyTools(); + defer tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msgs = parsed.value.object.get("messages").?.array.items; + // Two system messages first (in derivation order), then the two users. + try testing.expectEqual(@as(usize, 4), msgs.len); + try testing.expectEqualStrings("system", msgs[0].object.get("role").?.string); + try testing.expectEqualStrings("seed", msgs[0].object.get("content").?.string); + try testing.expectEqualStrings("system", msgs[1].object.get("role").?.string); + try testing.expectEqualStrings("mid-conversation append", msgs[1].object.get("content").?.string); + try testing.expectEqualStrings("user", msgs[2].object.get("role").?.string); + try testing.expectEqualStrings("Hello!", msgs[2].object.get("content").?.string); + try testing.expectEqualStrings("user", msgs[3].object.get("role").?.string); + try testing.expectEqualStrings("again", msgs[3].object.get("content").?.string); +} + +test "serializeRequest - replace-mode system block wipes prior system messages" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("original"); + try conv.replaceSystemMessage("fresh seed"); + try conv.addSystemMessage("fresh append"); + try addUserText(&conv, "Hi"); + + const cfg = testConfig("gpt-4o"); + var tools = emptyTools(); + defer tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msgs = parsed.value.object.get("messages").?.array.items; + try testing.expectEqual(@as(usize, 3), msgs.len); + try testing.expectEqualStrings("system", msgs[0].object.get("role").?.string); + try testing.expectEqualStrings("fresh seed", msgs[0].object.get("content").?.string); + try testing.expectEqualStrings("system", msgs[1].object.get("role").?.string); + try testing.expectEqualStrings("fresh append", msgs[1].object.get("content").?.string); + try testing.expectEqualStrings("user", msgs[2].object.get("role").?.string); +} + +test "serializeRequest - assistant Thinking blocks are stripped from outbound history" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ .text = try conversation.textualBlockFromSlice(allocator, "thinking step") } }, + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer here") }, + }, null); + + const cfg = testConfig("gpt-4o"); + var tools = emptyTools(); + defer tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msg = parsed.value.object.get("messages").?.array.items[0]; + try testing.expectEqualStrings("assistant", msg.object.get("role").?.string); + // Content is a flat string, only the Text block survives. + const content = msg.object.get("content").?.string; + try testing.expectEqualStrings("answer here", content); +} + +test "serializeRequest - reasoning effort level included when set" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "Hi"); + + var cfg = testConfig("gpt-4o"); + cfg.reasoning = .high; + + var tools = emptyTools(); + defer tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + try testing.expectEqualStrings( + "high", + parsed.value.object.get("reasoning_effort").?.string, + ); +} + +test "serializeRequest - reasoning .off sends \"none\"" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "Hi"); + + var cfg = testConfig("gpt-4o"); + cfg.reasoning = .off; + + var tools = emptyTools(); + defer tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + try testing.expectEqualStrings( + "none", + parsed.value.object.get("reasoning_effort").?.string, + ); +} + +test "parseStreamEvent - role only" { + const allocator = testing.allocator; + const payload = + \\{"id":"chatcmpl-1","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]} + ; + + var pd = try parseStreamEvent(allocator, payload); + defer pd.deinit(); + + try testing.expectEqualStrings("assistant", pd.delta.role.?); + try testing.expect(pd.delta.content == null); + try testing.expect(pd.delta.finish_reason == null); +} + +test "parseStreamEvent - content delta" { + const allocator = testing.allocator; + const payload = + \\{"choices":[{"delta":{"content":"Hello"},"finish_reason":null}]} + ; + + var pd = try parseStreamEvent(allocator, payload); + defer pd.deinit(); + + try testing.expectEqualStrings("Hello", pd.delta.content.?); +} + +test "parseStreamEvent - finish_reason stop" { + const allocator = testing.allocator; + const payload = + \\{"choices":[{"delta":{},"finish_reason":"stop"}]} + ; + + var pd = try parseStreamEvent(allocator, payload); + defer pd.deinit(); + + try testing.expectEqualStrings("stop", pd.delta.finish_reason.?); + try testing.expect(pd.delta.content == null); +} + +test "parseStreamEvent - reasoning_content" { + const allocator = testing.allocator; + const payload = + \\{"choices":[{"delta":{"reasoning_content":"hmm"}}]} + ; + + var pd = try parseStreamEvent(allocator, payload); + defer pd.deinit(); + + try testing.expectEqualStrings("hmm", pd.delta.reasoning_content.?); +} + +// ----------------------------------------------------------------------------- +// Phase 3: tools serialization, tool_calls parsing +// ----------------------------------------------------------------------------- + +const tool_mod = @import("tool.zig"); + +/// Minimal in-test tool: borrows its name/description/schema slices from +/// the test's stack. The vtable's deinit is a no-op since nothing is owned. +const StaticToolVT = struct { + fn invoke(_: *anyopaque, _: []const u8, _: Allocator) anyerror!tool_mod.ResultParts { + return error.NotImplementedInTest; + } + fn deinit_(_: *anyopaque, _: Allocator) void {} + + const v: tool_mod.Tool.VTable = .{ + .invoke = invoke, + .deinit = deinit_, + }; +}; +var static_tool_ctx_sentinel: u8 = 0; +fn makeStaticTool( + name: []const u8, + description: []const u8, + schema: []const u8, +) tool_mod.Tool { + return .{ + .decl = .{ + .name = name, + .description = description, + .schema_json = schema, + }, + .ctx = &static_tool_ctx_sentinel, + .vtable = &StaticToolVT.v, + }; +} + +test "serializeRequest - emits tools array when registry non-empty" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call something"); + + var tools = emptyTools(); + defer tools.deinit(); + + try tools.register(makeStaticTool("echo", "Echo a message back.", + \\{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]} + )); + + const cfg = testConfig("gpt-4o"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const arr = parsed.value.object.get("tools").?.array.items; + try testing.expectEqual(@as(usize, 1), arr.len); + try testing.expectEqualStrings("function", arr[0].object.get("type").?.string); + + const f = arr[0].object.get("function").?.object; + try testing.expectEqualStrings("echo", f.get("name").?.string); + try testing.expectEqualStrings("Echo a message back.", f.get("description").?.string); + + const params = f.get("parameters").?.object; + try testing.expectEqualStrings("object", params.get("type").?.string); + try testing.expect(params.get("properties").? == .object); + try testing.expect(params.get("required").? == .array); +} + +test "serializeRequest - dotted tool name is wire-encoded with __" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "go"); + + var tools = emptyTools(); + defer tools.deinit(); + try tools.register(makeStaticTool("std.read", "Read a file.", "{\"type\":\"object\"}")); + + const cfg = testConfig("gpt-4o"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const f = parsed.value.object.get("tools").?.array.items[0].object.get("function").?.object; + // Internal `std.read` crosses the wire as `std__read` (OpenAI forbids dots). + try testing.expectEqualStrings("std__read", f.get("name").?.string); +} + +test "serializeRequest - assistant ToolUse becomes tool_calls" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id = try allocator.dupe(u8, "call_1"); + const name = try allocator.dupe(u8, "echo"); + var args: conversation.TextualBlock = .empty; + try args.appendSlice(allocator, "{\"message\":\"hi\"}"); + + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "calling tool") }, + .{ .ToolUse = .{ .id = id, .name = name, .input = args } }, + }, null); + + var tools = emptyTools(); + defer tools.deinit(); + const cfg = testConfig("gpt-4o"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msg = parsed.value.object.get("messages").?.array.items[0].object; + try testing.expectEqualStrings("assistant", msg.get("role").?.string); + try testing.expectEqualStrings("calling tool", msg.get("content").?.string); + + const tcs = msg.get("tool_calls").?.array.items; + try testing.expectEqual(@as(usize, 1), tcs.len); + try testing.expectEqualStrings("call_1", tcs[0].object.get("id").?.string); + try testing.expectEqualStrings("function", tcs[0].object.get("type").?.string); + + const fn_obj = tcs[0].object.get("function").?.object; + try testing.expectEqualStrings("echo", fn_obj.get("name").?.string); + // `arguments` is a string (JSON-as-string) per the OpenAI wire format. + try testing.expectEqualStrings("{\"message\":\"hi\"}", fn_obj.get("arguments").?.string); +} + +test "serializeRequest - user ToolResult fans out into tool messages" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id1 = try allocator.dupe(u8, "call_a"); + var p1: std.ArrayList(conversation.ResultPartStored) = .empty; + try p1.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "42") }); + + const id2 = try allocator.dupe(u8, "call_b"); + var p2: std.ArrayList(conversation.ResultPartStored) = .empty; + try p2.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "oops") }); + + var content: std.ArrayList(conversation.ContentBlock) = .empty; + try content.append(allocator, .{ .ToolResult = .{ .tool_use_id = id1, .parts = p1 } }); + try content.append(allocator, .{ .ToolResult = .{ .tool_use_id = id2, .parts = p2 } }); + try conv.messages.append(allocator, .{ .role = .user, .content = content }); + + var tools = emptyTools(); + defer tools.deinit(); + const cfg = testConfig("gpt-4o"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msgs = parsed.value.object.get("messages").?.array.items; + try testing.expectEqual(@as(usize, 2), msgs.len); + + try testing.expectEqualStrings("tool", msgs[0].object.get("role").?.string); + try testing.expectEqualStrings("call_a", msgs[0].object.get("tool_call_id").?.string); + try testing.expectEqualStrings("42", msgs[0].object.get("content").?.string); + + try testing.expectEqualStrings("tool", msgs[1].object.get("role").?.string); + try testing.expectEqualStrings("call_b", msgs[1].object.get("tool_call_id").?.string); + try testing.expectEqualStrings("oops", msgs[1].object.get("content").?.string); +} + +test "serializeRequest - tool result with image splits into tool + synthetic user" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id = try allocator.dupe(u8, "call_img"); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "the file:") }); + try parts.append(allocator, .{ .media = .{ + .media_type = try allocator.dupe(u8, "image/png"), + .data = try conversation.textualBlockFromSlice(allocator, "iVBOR=="), + } }); + var content: std.ArrayList(conversation.ContentBlock) = .empty; + try content.append(allocator, .{ .ToolResult = .{ .tool_use_id = id, .parts = parts } }); + try conv.messages.append(allocator, .{ .role = .user, .content = content }); + + var tools = emptyTools(); + defer tools.deinit(); + const cfg = testConfig("gpt-4o"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msgs = parsed.value.object.get("messages").?.array.items; + try testing.expectEqual(@as(usize, 2), msgs.len); + + // First: the tool message (text only, no image). + try testing.expectEqualStrings("tool", msgs[0].object.get("role").?.string); + try testing.expectEqualStrings("call_img", msgs[0].object.get("tool_call_id").?.string); + try testing.expect(std.mem.indexOf(u8, msgs[0].object.get("content").?.string, "the file:") != null); + + // Second: synthetic user message carrying the image as a data URL. + try testing.expectEqualStrings("user", msgs[1].object.get("role").?.string); + const uc = msgs[1].object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 1), uc.len); + try testing.expectEqualStrings("image_url", uc[0].object.get("type").?.string); + const url = uc[0].object.get("image_url").?.object.get("url").?.string; + try testing.expectEqualStrings("data:image/png;base64,iVBOR==", url); +} + +test "parseStreamEvent - tool_calls delta with id and partial arguments" { + const allocator = testing.allocator; + const payload = + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_xyz","type":"function","function":{"name":"echo","arguments":"{\"x\":"}}]}}]} + ; + var pd = try parseStreamEvent(allocator, payload); + defer pd.deinit(); + + try testing.expectEqual(@as(usize, 1), pd.delta.tool_calls.len); + const tc = pd.delta.tool_calls[0]; + try testing.expectEqual(@as(usize, 0), tc.index); + try testing.expectEqualStrings("call_xyz", tc.id.?); + try testing.expectEqualStrings("echo", tc.name.?); + try testing.expectEqualStrings("{\"x\":", tc.arguments.?); +} + +test "parseStreamEvent - tool_calls delta with only arguments fragment" { + const allocator = testing.allocator; + const payload = + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"1}"}}]}}]} + ; + var pd = try parseStreamEvent(allocator, payload); + defer pd.deinit(); + + try testing.expectEqual(@as(usize, 1), pd.delta.tool_calls.len); + const tc = pd.delta.tool_calls[0]; + try testing.expectEqual(@as(usize, 0), tc.index); + try testing.expect(tc.id == null); + try testing.expect(tc.name == null); + try testing.expectEqualStrings("1}", tc.arguments.?); +} + +test "parseStreamEvent - finish_reason tool_calls" { + const allocator = testing.allocator; + const payload = + \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]} + ; + var pd = try parseStreamEvent(allocator, payload); + defer pd.deinit(); + + try testing.expectEqualStrings("tool_calls", pd.delta.finish_reason.?); +} + +test "parseStreamEvent - top-level error event with type and message" { + const allocator = testing.allocator; + const payload = + \\{"error":{"message":"Rate limit exceeded","type":"rate_limit_error","code":429}} + ; + var pd = try parseStreamEvent(allocator, payload); + defer pd.deinit(); + + try testing.expectEqualStrings("Rate limit exceeded", pd.delta.error_message.?); + try testing.expectEqualStrings("rate_limit_error", pd.delta.error_type.?); +} + +test "parseStreamEvent - bare-string error" { + const allocator = testing.allocator; + const payload = + \\{"error":"something went wrong"} + ; + var pd = try parseStreamEvent(allocator, payload); + defer pd.deinit(); + + try testing.expectEqualStrings("something went wrong", pd.delta.error_message.?); + try testing.expect(pd.delta.error_type == null); +} + +test "serializeRequest - compaction summary trims superseded prefix" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + // System prompt survives compaction. + try conv.addSystemMessage("you are helpful"); + // Old prefix that must be dropped. + try addUserText(&conv, "ancient question"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "ancient answer") }, + }, null); + // Compaction summary resets conversation context. + try conv.addCompactionSummary("summary of the ancient exchange"); + // Kept-verbatim suffix replayed after the summary. + try addUserText(&conv, "recent question"); + + var tools = emptyTools(); + defer tools.deinit(); + const cfg = testConfig("gpt-4o"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const msgs = parsed.value.object.get("messages").?.array.items; + // system + compaction summary (user) + recent question (user) = 3. + try testing.expectEqual(@as(usize, 3), msgs.len); + try testing.expectEqualStrings("system", msgs[0].object.get("role").?.string); + try testing.expectEqualStrings("you are helpful", msgs[0].object.get("content").?.string); + try testing.expectEqualStrings("user", msgs[1].object.get("role").?.string); + try testing.expectEqualStrings("summary of the ancient exchange", msgs[1].object.get("content").?.string); + try testing.expectEqualStrings("user", msgs[2].object.get("role").?.string); + try testing.expectEqualStrings("recent question", msgs[2].object.get("content").?.string); +} diff --git a/src/openai_responses_json.zig b/src/openai_responses_json.zig new file mode 100644 index 0000000..358fc45 --- /dev/null +++ b/src/openai_responses_json.zig @@ -0,0 +1,941 @@ +//! OpenAI Responses API JSON serialization and streaming-event parsing. +//! +//! The Responses API (`POST /responses`) backs the ChatGPT-subscription Codex +//! provider. Its wire shape differs from Chat Completions: +//! +//! - The system prompt rides in a top-level `instructions` string. +//! - History is an `input` array of items: `{role, content:[{type, text}]}` +//! messages, `{type:"function_call", call_id, name, arguments}` for +//! assistant tool calls, and `{type:"function_call_output", call_id, +//! output}` for tool results. +//! - Tools are flat: `{type:"function", name, description, parameters}`. +//! - Reasoning is requested via `{reasoning:{effort, summary}}` and +//! `include:["reasoning.encrypted_content"]`; `store:false` keeps the +//! exchange stateless. +//! - Streaming uses typed SSE events (`response.output_text.delta`, +//! `response.function_call_arguments.delta`, `response.completed`, …) +//! rather than Chat Completions' `choices[].delta`. +//! +//! References: OpenAI Responses API streaming docs and the open-source Codex +//! client's request transformer. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Writer = std.Io.Writer; +const conversation = @import("conversation.zig"); +const config_mod = @import("config.zig"); +const tool_registry_mod = @import("tool_registry.zig"); +const writeRawJson = @import("provider.zig").writeRawJson; + +// =========================================================================== +// Request serialization +// =========================================================================== + +pub const RequestDialect = enum { + public, + codex, +}; + +/// Serialize a Conversation into a `/responses` request body. Caller owns the +/// returned slice. +pub fn serializeRequest( + allocator: Allocator, + cfg: *const config_mod.OpenAIResponsesConfig, + conv: *const conversation.Conversation, + tools: *const tool_registry_mod.ToolRegistry, + dialect: RequestDialect, +) ![]u8 { + var aw: Writer.Allocating = .init(allocator); + errdefer aw.deinit(); + var s: std.json.Stringify = .{ .writer = &aw.writer }; + + try s.beginObject(); + + try s.objectField("model"); + try s.write(cfg.model); + + try s.objectField("stream"); + try s.write(true); + + // Stateless: we replay full history each turn (no server-side state). + try s.objectField("store"); + try s.write(false); + + if (dialect == .public) { + try s.objectField("max_output_tokens"); + try s.write(cfg.max_tokens); + } + + if (dialect == .codex) { + try s.objectField("text"); + try s.beginObject(); + try s.objectField("verbosity"); + try s.write("low"); + try s.endObject(); + + try s.objectField("tool_choice"); + try s.write("auto"); + + try s.objectField("parallel_tool_calls"); + try s.write(true); + } + + // Carry encrypted reasoning so multi-turn reasoning can continue without + // server-side storage. + try s.objectField("include"); + try s.beginArray(); + try s.write("reasoning.encrypted_content"); + try s.endArray(); + + switch (cfg.reasoning) { + .default => {}, + // The Codex backend does not accept "none"; the lowest real effort is + // "low". `.off`/`.minimal` map to "low". + .off, .minimal, .low => try writeReasoning(&s, "low"), + .medium => try writeReasoning(&s, "medium"), + .high => try writeReasoning(&s, "high"), + } + + // System prompt → `instructions` (joined with blank lines). + var sys_blocks = try conversation.effectiveSystemBlocks(allocator, conv.messages.items); + defer sys_blocks.deinit(allocator); + if (sys_blocks.items.len > 0) { + var instr: std.ArrayList(u8) = .empty; + defer instr.deinit(allocator); + for (sys_blocks.items, 0..) |text, i| { + if (i != 0) try instr.appendSlice(allocator, "\n\n"); + try instr.appendSlice(allocator, text); + } + try s.objectField("instructions"); + try s.write(instr.items); + } + + if (tools.count() > 0) { + try s.objectField("tools"); + try s.beginArray(); + var it = tools.toolsForLLM(); + while (it.next()) |t| { + try s.beginObject(); + try s.objectField("type"); + try s.write("function"); + try s.objectField("name"); + try s.write(t.decl.name); // already wire-encoded + try s.objectField("description"); + try s.write(t.decl.description); + try s.objectField("parameters"); + try writeRawJson(&s, t.decl.schema_json); + try s.endObject(); + } + try s.endArray(); + } + + try s.objectField("input"); + try s.beginArray(); + for (conversation.activeMessageWindow(conv.messages.items)) |msg| { + if (msg.role == .system) continue; + try writeInputForMessage(&s, msg, allocator, cfg, dialect); + } + try s.endArray(); + + try s.endObject(); + return try aw.toOwnedSlice(); +} + +fn writeReasoning(s: *std.json.Stringify, effort: []const u8) !void { + try s.objectField("reasoning"); + try s.beginObject(); + try s.objectField("effort"); + try s.write(effort); + try s.objectField("summary"); + try s.write("auto"); + try s.endObject(); +} + +/// Emit the `input` item(s) for one conversation message. +fn writeInputForMessage( + s: *std.json.Stringify, + msg: conversation.Message, + allocator: Allocator, + cfg: *const config_mod.OpenAIResponsesConfig, + dialect: RequestDialect, +) !void { + switch (msg.role) { + .system => {}, + .user => { + // Tool results fan out into `function_call_output` items; any + // plain text becomes a `user` message. + var has_tool_result = false; + for (msg.content.items) |b| { + if (b == .ToolResult) has_tool_result = true; + } + if (has_tool_result) { + // `function_call_output` is text-only. Each result carries + // its text (plus a short note when media is present); the + // media rides along afterward in a synthetic user message. + var any_media = false; + for (msg.content.items) |block| { + if (block != .ToolResult) continue; + const tr = block.ToolResult; + if (tr.hasMedia()) any_media = true; + try s.beginObject(); + try s.objectField("type"); + try s.write("function_call_output"); + try s.objectField("call_id"); + try s.write(tr.tool_use_id); + try s.objectField("output"); + var tbuf: std.ArrayList(u8) = .empty; + defer tbuf.deinit(allocator); + try tr.appendTextInto(allocator, &tbuf); + if (tr.hasMedia()) { + if (tbuf.items.len > 0) try tbuf.append(allocator, '\n'); + try tbuf.appendSlice(allocator, "[attachment(s) provided in the following user message]"); + } + try s.write(tbuf.items); + try s.endObject(); + } + // Synthetic user message holding the media: images as + // `input_image` data URLs, PDFs as `input_file` (the + // Responses API rejects non-image data URLs in + // `input_image`). + if (any_media) { + try s.beginObject(); + try s.objectField("role"); + try s.write("user"); + try s.objectField("content"); + try s.beginArray(); + for (msg.content.items) |block| { + if (block != .ToolResult) continue; + for (block.ToolResult.parts.items) |part| { + if (part != .media) continue; + const m = part.media; + var url_buf: std.ArrayList(u8) = .empty; + defer url_buf.deinit(allocator); + try url_buf.appendSlice(allocator, "data:"); + try url_buf.appendSlice(allocator, m.media_type); + try url_buf.appendSlice(allocator, ";base64,"); + try url_buf.appendSlice(allocator, m.data.items); + const is_pdf = std.mem.eql(u8, m.media_type, "application/pdf"); + try s.beginObject(); + try s.objectField("type"); + try s.write(if (is_pdf) "input_file" else "input_image"); + if (is_pdf) { + try s.objectField("filename"); + try s.write("attachment.pdf"); + try s.objectField("file_data"); + } else { + try s.objectField("image_url"); + } + try s.write(url_buf.items); + try s.endObject(); + } + } + try s.endArray(); + try s.endObject(); + } + } + // Plain user text (skip if the message was purely tool results). + var text_buf: std.ArrayList(u8) = .empty; + defer text_buf.deinit(allocator); + try concatTextBlocks(msg.content.items, &text_buf, allocator); + if (text_buf.items.len > 0) { + try writeRoleMessage(s, "user", "input_text", text_buf.items, null); + } + }, + .assistant => { + // Replay opaque reasoning items first so stateless follow-up turns + // preserve encrypted reasoning continuity. + for (msg.content.items) |block| { + if (block != .Thinking) continue; + const tb = block.Thinking; + const sig = tb.signature orelse continue; + if (!conversation.thinkingSignatureMatches( + tb, + msg.identity, + if (dialect == .codex) .openai_codex_responses else .openai_responses, + cfg.base_url, + cfg.model, + )) continue; + if (sig.len == 0 or sig[0] != '{') continue; + try writeRawJson(s, sig); + } + + // Assistant text first (as an output_text message), then each + // tool call as a `function_call` item. + var text_buf: std.ArrayList(u8) = .empty; + defer text_buf.deinit(allocator); + try concatTextBlocks(msg.content.items, &text_buf, allocator); + if (text_buf.items.len > 0) { + try writeRoleMessage(s, "assistant", "output_text", text_buf.items, openAIPhaseFromMetadata(msg.metadata)); + } + for (msg.content.items) |block| { + if (block != .ToolUse) continue; + const tu = block.ToolUse; + try s.beginObject(); + try s.objectField("type"); + try s.write("function_call"); + try s.objectField("call_id"); + try s.write(tu.id); + try s.objectField("name"); + var name_buf: [tool_registry_mod.max_wire_name_len]u8 = undefined; + try s.write(tool_registry_mod.encodeName(&name_buf, tu.name)); + try s.objectField("arguments"); + try s.write(tu.input.items); + try s.endObject(); + } + }, + } +} + +fn writeRoleMessage( + s: *std.json.Stringify, + role: []const u8, + content_type: []const u8, + text: []const u8, + phase: ?[]const u8, +) !void { + try s.beginObject(); + try s.objectField("role"); + try s.write(role); + if (phase) |p| { + try s.objectField("phase"); + try s.write(p); + } + try s.objectField("content"); + try s.beginArray(); + try s.beginObject(); + try s.objectField("type"); + try s.write(content_type); + try s.objectField("text"); + try s.write(text); + try s.endObject(); + try s.endArray(); + try s.endObject(); +} + +fn openAIPhaseFromMetadata(metadata: ?[]const u8) ?[]const u8 { + const md = metadata orelse return null; + var parsed = std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, md, .{}) catch return null; + defer parsed.deinit(); + if (parsed.value != .object) return null; + const phase = strField(parsed.value.object, "openai_responses_phase") orelse return null; + if (std.mem.eql(u8, phase, "commentary")) return "commentary"; + if (std.mem.eql(u8, phase, "final_answer")) return "final_answer"; + return null; +} + +fn concatTextBlocks( + blocks: []const conversation.ContentBlock, + out: *std.ArrayList(u8), + allocator: Allocator, +) !void { + for (blocks) |block| { + switch (block) { + .Text => |tb| try out.appendSlice(allocator, tb.items), + .CompactionSummary => |cs| try out.appendSlice(allocator, cs.text.items), + else => {}, + } + } +} + +// =========================================================================== +// Streaming event parsing +// =========================================================================== + +/// The kinds of streaming event this provider acts on. Everything else is +/// ignored (`.other`). +pub const EventKind = enum { + output_item_added, + output_text_delta, + reasoning_summary_delta, + function_call_arguments_delta, + function_call_arguments_done, + output_item_done, + completed, + failed, + err, + other, +}; + +/// A parsed Responses streaming event. Slices borrow from `parsed`. +pub const StreamEvent = struct { + parsed: std.json.Parsed(std.json.Value), + kind: EventKind, + /// `output_text`/`reasoning_summary` delta, or function-call argument + /// fragment. + delta: ?[]const u8 = null, + /// Output array index. The stable key for a tool call across all of its + /// events (`item_id` is unreliable on the Copilot proxy, so unused). + output_index: ?usize = null, + /// Item type on add/done: "message" | "function_call" | "reasoning". + item_type: ?[]const u8 = null, + /// Assistant message phase on message output items. + item_phase: ?[]const u8 = null, + /// Raw reasoning output item JSON, used for stateless encrypted reasoning + /// replay on follow-up turns. + reasoning_item_json: ?[]const u8 = null, + /// Function-call identity (on `output_item.added`/`done`). + call_id: ?[]const u8 = null, + name: ?[]const u8 = null, + /// Full arguments string on `output_item.done` for a function_call. + arguments: ?[]const u8 = null, + /// Error/failure message (`error`, `response.failed`). + error_message: ?[]const u8 = null, + /// Usage on `response.completed`. + usage: ?Usage = null, + /// Function-call output items included in a terminal `response.completed`. + completed_items: []const OutputItem = &.{}, + + pub const Usage = struct { + input_tokens: u64 = 0, + output_tokens: u64 = 0, + cached_tokens: u64 = 0, + reasoning_tokens: u64 = 0, + }; + + pub const OutputItem = struct { + output_index: usize, + call_id: ?[]const u8 = null, + name: ?[]const u8 = null, + arguments: ?[]const u8 = null, + }; + + pub fn deinit(self: *StreamEvent) void { + self.parsed.deinit(); + } +}; + +/// Parse one SSE event payload (the JSON after `data: `). The caller must keep +/// the returned value alive while reading its slices, then `deinit` it. +pub fn parseStreamEvent(allocator: Allocator, payload: []const u8) !StreamEvent { + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, payload, .{}); + errdefer parsed.deinit(); + + var ev: StreamEvent = .{ .parsed = parsed, .kind = .other }; + const root = parsed.value; + if (root != .object) return ev; + const obj = root.object; + + const type_str = strField(obj, "type") orelse { + ev.parsed = parsed; + return ev; + }; + + if (std.mem.eql(u8, type_str, "response.output_text.delta")) { + ev.kind = .output_text_delta; + ev.delta = strField(obj, "delta"); + ev.output_index = usizeField(obj, "output_index"); + } else if (std.mem.eql(u8, type_str, "response.reasoning_summary_text.delta")) { + ev.kind = .reasoning_summary_delta; + ev.delta = strField(obj, "delta"); + ev.output_index = usizeField(obj, "output_index"); + } else if (std.mem.eql(u8, type_str, "response.function_call_arguments.delta")) { + ev.kind = .function_call_arguments_delta; + ev.delta = strField(obj, "delta") orelse + strField(obj, "arguments_delta") orelse + strField(obj, "arguments"); + ev.output_index = usizeField(obj, "output_index"); + } else if (std.mem.eql(u8, type_str, "response.function_call_arguments.done")) { + ev.kind = .function_call_arguments_done; + ev.arguments = strField(obj, "arguments"); + ev.output_index = usizeField(obj, "output_index"); + } else if (std.mem.eql(u8, type_str, "response.output_item.added")) { + ev.kind = .output_item_added; + ev.output_index = usizeField(obj, "output_index"); + try readItem(parsed.arena.allocator(), obj, &ev); + } else if (std.mem.eql(u8, type_str, "response.output_item.done")) { + ev.kind = .output_item_done; + ev.output_index = usizeField(obj, "output_index"); + try readItem(parsed.arena.allocator(), obj, &ev); + } else if (std.mem.eql(u8, type_str, "response.completed") or std.mem.eql(u8, type_str, "response.done")) { + ev.kind = .completed; + readUsage(obj, &ev); + try readCompletedOutput(parsed.arena.allocator(), obj, &ev); + } else if (std.mem.eql(u8, type_str, "response.failed") or std.mem.eql(u8, type_str, "response.incomplete")) { + ev.kind = .failed; + ev.error_message = readResponseError(obj); + } else if (std.mem.eql(u8, type_str, "error")) { + ev.kind = .err; + ev.error_message = strField(obj, "message") orelse "stream error"; + } + + ev.parsed = parsed; + return ev; +} + +fn readItem(allocator: Allocator, obj: std.json.ObjectMap, ev: *StreamEvent) !void { + const item = obj.get("item") orelse return; + if (item != .object) return; + const io = item.object; + ev.item_type = strField(io, "type"); + ev.item_phase = strField(io, "phase"); + ev.call_id = strField(io, "call_id"); + ev.name = strField(io, "name"); + ev.arguments = strField(io, "arguments"); + if (ev.item_type) |it| { + if (std.mem.eql(u8, it, "reasoning")) { + if (io.get("encrypted_content") != null) { + ev.reasoning_item_json = try stringifyValue(allocator, item); + } + } + } +} + +fn stringifyValue(allocator: Allocator, value: std.json.Value) ![]const u8 { + var aw: Writer.Allocating = .init(allocator); + errdefer aw.deinit(); + var s: std.json.Stringify = .{ .writer = &aw.writer }; + try s.write(value); + return try aw.toOwnedSlice(); +} + +fn readUsage(obj: std.json.ObjectMap, ev: *StreamEvent) void { + const resp = obj.get("response") orelse return; + if (resp != .object) return; + const u = resp.object.get("usage") orelse return; + if (u != .object) return; + var usage: StreamEvent.Usage = .{}; + usage.input_tokens = u64Field(u.object, "input_tokens"); + usage.output_tokens = u64Field(u.object, "output_tokens"); + usage.reasoning_tokens = blk: { + const otd = u.object.get("output_tokens_details") orelse break :blk 0; + if (otd != .object) break :blk 0; + break :blk u64Field(otd.object, "reasoning_tokens"); + }; + usage.cached_tokens = blk: { + const itd = u.object.get("input_tokens_details") orelse break :blk 0; + if (itd != .object) break :blk 0; + break :blk u64Field(itd.object, "cached_tokens"); + }; + ev.usage = usage; +} + +fn readCompletedOutput(allocator: Allocator, obj: std.json.ObjectMap, ev: *StreamEvent) !void { + const resp = obj.get("response") orelse return; + if (resp != .object) return; + const output = resp.object.get("output") orelse return; + if (output != .array) return; + + var items: std.ArrayList(StreamEvent.OutputItem) = .empty; + errdefer items.deinit(allocator); + for (output.array.items, 0..) |v, i| { + if (v != .object) continue; + const item_type = strField(v.object, "type") orelse continue; + if (!std.mem.eql(u8, item_type, "function_call")) continue; + try items.append(allocator, .{ + .output_index = i, + .call_id = strField(v.object, "call_id"), + .name = strField(v.object, "name"), + .arguments = strField(v.object, "arguments"), + }); + } + if (items.items.len == 0) return; + const buf = try items.toOwnedSlice(allocator); + ev.completed_items = buf; +} + +fn readResponseError(obj: std.json.ObjectMap) ?[]const u8 { + const resp = obj.get("response") orelse return null; + if (resp != .object) return null; + if (resp.object.get("error")) |e| { + if (e == .object) return strField(e.object, "message"); + } + if (resp.object.get("incomplete_details")) |d| { + if (d == .object) return strField(d.object, "reason"); + } + return null; +} + +fn strField(obj: std.json.ObjectMap, name: []const u8) ?[]const u8 { + const v = obj.get(name) orelse return null; + return if (v == .string) v.string else null; +} + +fn u64Field(obj: std.json.ObjectMap, name: []const u8) u64 { + const v = obj.get(name) orelse return 0; + if (v != .integer or v.integer < 0) return 0; + return @intCast(v.integer); +} + +fn usizeField(obj: std.json.ObjectMap, name: []const u8) ?usize { + const v = obj.get(name) orelse return null; + if (v != .integer or v.integer < 0) return null; + return @intCast(v.integer); +} + +// =========================================================================== +// Tests +// =========================================================================== + +const testing = std.testing; + +fn testConfig(model: []const u8) config_mod.OpenAIResponsesConfig { + return .{ .api_key = "k", .base_url = "u", .model = model }; +} + +fn emptyTools() tool_registry_mod.ToolRegistry { + return tool_registry_mod.ToolRegistry.init(testing.allocator); +} + +fn addUserText(conv: *conversation.Conversation, text: []const u8) !void { + const tb = try conversation.textualBlockFromSlice(conv.allocator, text); + var block: conversation.ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + +test "responses serializeRequest - instructions, input, store/include" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addSystemMessage("You are Codex."); + try addUserText(&conv, "Hello!"); + + var cfg = testConfig("gpt-5.1-codex"); + cfg.reasoning = .high; + var tools = emptyTools(); + defer tools.deinit(); + const body = try serializeRequest(allocator, &cfg, &conv, &tools, .public); + defer allocator.free(body); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const root = parsed.value.object; + + try testing.expectEqualStrings("gpt-5.1-codex", root.get("model").?.string); + try testing.expect(root.get("stream").?.bool); + try testing.expect(!root.get("store").?.bool); + try testing.expectEqualStrings("You are Codex.", root.get("instructions").?.string); + try testing.expectEqualStrings("reasoning.encrypted_content", root.get("include").?.array.items[0].string); + try testing.expectEqualStrings("high", root.get("reasoning").?.object.get("effort").?.string); + + const input = root.get("input").?.array.items; + try testing.expectEqual(@as(usize, 1), input.len); + try testing.expectEqualStrings("user", input[0].object.get("role").?.string); + const part = input[0].object.get("content").?.array.items[0].object; + try testing.expectEqualStrings("input_text", part.get("type").?.string); + try testing.expectEqualStrings("Hello!", part.get("text").?.string); +} + +test "responses serializeRequest - tools are flat function items" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "go"); + + var tools = emptyTools(); + defer tools.deinit(); + try tools.register(.{ + .decl = .{ .name = "echo", .description = "Echo.", .schema_json = "{\"type\":\"object\"}" }, + .ctx = undefined, + .vtable = &NoopToolVT.v, + }); + + const cfg = testConfig("gpt-5.1-codex"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools, .public); + defer allocator.free(body); + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const tool0 = parsed.value.object.get("tools").?.array.items[0].object; + // Flat shape: name/description/parameters directly on the tool object. + try testing.expectEqualStrings("function", tool0.get("type").?.string); + try testing.expectEqualStrings("echo", tool0.get("name").?.string); + try testing.expectEqualStrings("Echo.", tool0.get("description").?.string); + try testing.expect(tool0.get("parameters").? == .object); +} + +test "responses serializeRequest - codex dialect omits max tokens and sends codex defaults" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "Hello!"); + + var tools = emptyTools(); + defer tools.deinit(); + const cfg = testConfig("gpt-5.5"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools, .codex); + defer allocator.free(body); + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + const root = parsed.value.object; + + try testing.expect(root.get("max_output_tokens") == null); + try testing.expectEqualStrings("low", root.get("text").?.object.get("verbosity").?.string); + try testing.expectEqualStrings("auto", root.get("tool_choice").?.string); + try testing.expect(root.get("parallel_tool_calls").?.bool); +} + +test "responses serializeRequest - assistant phase metadata and reasoning signature replay" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const sig = try allocator.dupe(u8, "{\"type\":\"reasoning\",\"id\":\"rs_1\",\"encrypted_content\":\"sealed\"}"); + const thinking = try conversation.textualBlockFromSlice(allocator, "thinking"); + const text = try conversation.textualBlockFromSlice(allocator, "answer"); + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ .text = thinking, .signature = sig } }, + .{ .Text = text }, + }, null); + try conversation.setThinkingOrigins(allocator, conv.messages.items[0].content.items, .openai_codex_responses, "u", "gpt-5.5"); + conv.messages.items[0].metadata = try allocator.dupe(u8, "{\"openai_responses_phase\":\"final_answer\"}"); + + var tools = emptyTools(); + defer tools.deinit(); + const cfg = testConfig("gpt-5.5"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools, .codex); + defer allocator.free(body); + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const input = parsed.value.object.get("input").?.array.items; + try testing.expectEqual(@as(usize, 2), input.len); + try testing.expectEqualStrings("reasoning", input[0].object.get("type").?.string); + try testing.expectEqualStrings("sealed", input[0].object.get("encrypted_content").?.string); + try testing.expectEqualStrings("assistant", input[1].object.get("role").?.string); + try testing.expectEqualStrings("final_answer", input[1].object.get("phase").?.string); +} + +test "responses serializeRequest - mismatched signature origin skips reasoning replay" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const sig = try allocator.dupe(u8, "{\"type\":\"reasoning\",\"id\":\"rs_1\",\"encrypted_content\":\"sealed\"}"); + try conv.addAssistantMessage(&.{ + .{ .Thinking = .{ .text = try conversation.textualBlockFromSlice(allocator, "thinking"), .signature = sig } }, + .{ .Text = try conversation.textualBlockFromSlice(allocator, "answer") }, + }, null); + try conversation.setThinkingOrigins(allocator, conv.messages.items[0].content.items, .openai_responses, "https://api.individual.githubcopilot.com", "gpt-5.4-mini"); + + var tools = emptyTools(); + defer tools.deinit(); + const cfg = testConfig("gpt-5.5"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools, .codex); + defer allocator.free(body); + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const input = parsed.value.object.get("input").?.array.items; + try testing.expectEqual(@as(usize, 1), input.len); + try testing.expectEqualStrings("assistant", input[0].object.get("role").?.string); +} + +test "responses serializeRequest - assistant tool_use + tool result round-trip" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const id = try allocator.dupe(u8, "call_1"); + const name = try allocator.dupe(u8, "echo"); + var args: conversation.TextualBlock = .empty; + try args.appendSlice(allocator, "{\"m\":\"hi\"}"); + try conv.addAssistantMessage(&.{ + .{ .Text = try conversation.textualBlockFromSlice(allocator, "calling") }, + .{ .ToolUse = .{ .id = id, .name = name, .input = args } }, + }, null); + + const rid = try allocator.dupe(u8, "call_1"); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "42") }); + var content: std.ArrayList(conversation.ContentBlock) = .empty; + try content.append(allocator, .{ .ToolResult = .{ .tool_use_id = rid, .parts = parts } }); + try conv.messages.append(allocator, .{ .role = .user, .content = content }); + + var tools = emptyTools(); + defer tools.deinit(); + const cfg = testConfig("gpt-5.1-codex"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools, .public); + defer allocator.free(body); + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const input = parsed.value.object.get("input").?.array.items; + // assistant message (text) + function_call + function_call_output = 3. + try testing.expectEqual(@as(usize, 3), input.len); + try testing.expectEqualStrings("assistant", input[0].object.get("role").?.string); + try testing.expectEqualStrings("function_call", input[1].object.get("type").?.string); + try testing.expectEqualStrings("call_1", input[1].object.get("call_id").?.string); + try testing.expectEqualStrings("echo", input[1].object.get("name").?.string); + try testing.expectEqualStrings("function_call_output", input[2].object.get("type").?.string); + try testing.expectEqualStrings("call_1", input[2].object.get("call_id").?.string); + try testing.expectEqualStrings("42", input[2].object.get("output").?.string); +} + +test "responses serializeRequest - tool result media splits into output note + synthetic user" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + const rid = try allocator.dupe(u8, "call_img"); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + try parts.append(allocator, .{ .text = try conversation.textualBlockFromSlice(allocator, "the file:") }); + try parts.append(allocator, .{ .media = .{ + .media_type = try allocator.dupe(u8, "image/png"), + .data = try conversation.textualBlockFromSlice(allocator, "iVBOR=="), + } }); + try parts.append(allocator, .{ .media = .{ + .media_type = try allocator.dupe(u8, "application/pdf"), + .data = try conversation.textualBlockFromSlice(allocator, "JVBERg=="), + } }); + var content: std.ArrayList(conversation.ContentBlock) = .empty; + try content.append(allocator, .{ .ToolResult = .{ .tool_use_id = rid, .parts = parts } }); + try conv.messages.append(allocator, .{ .role = .user, .content = content }); + + var tools = emptyTools(); + defer tools.deinit(); + const cfg = testConfig("gpt-5.1-codex"); + const body = try serializeRequest(allocator, &cfg, &conv, &tools, .public); + defer allocator.free(body); + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + defer parsed.deinit(); + + const input = parsed.value.object.get("input").?.array.items; + // function_call_output + synthetic user message = 2. + try testing.expectEqual(@as(usize, 2), input.len); + + // The output carries the text plus the attachment note. + try testing.expectEqualStrings("function_call_output", input[0].object.get("type").?.string); + const out_text = input[0].object.get("output").?.string; + try testing.expect(std.mem.indexOf(u8, out_text, "the file:") != null); + try testing.expect(std.mem.indexOf(u8, out_text, "[attachment(s) provided in the following user message]") != null); + + // The synthetic user message carries the image and the PDF. + try testing.expectEqualStrings("user", input[1].object.get("role").?.string); + const uc = input[1].object.get("content").?.array.items; + try testing.expectEqual(@as(usize, 2), uc.len); + try testing.expectEqualStrings("input_image", uc[0].object.get("type").?.string); + try testing.expectEqualStrings("data:image/png;base64,iVBOR==", uc[0].object.get("image_url").?.string); + try testing.expectEqualStrings("input_file", uc[1].object.get("type").?.string); + try testing.expectEqualStrings("attachment.pdf", uc[1].object.get("filename").?.string); + try testing.expectEqualStrings("data:application/pdf;base64,JVBERg==", uc[1].object.get("file_data").?.string); +} + +test "responses parseStreamEvent - output_text delta" { + const allocator = testing.allocator; + var ev = try parseStreamEvent(allocator, + \\{"type":"response.output_text.delta","item_id":"msg_1","delta":"Hi"} + ); + defer ev.deinit(); + try testing.expectEqual(EventKind.output_text_delta, ev.kind); + try testing.expectEqualStrings("Hi", ev.delta.?); +} + +test "responses parseStreamEvent - function_call item added/done + args delta" { + const allocator = testing.allocator; + var added = try parseStreamEvent(allocator, + \\{"type":"response.output_item.added","item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"echo"}} + ); + defer added.deinit(); + try testing.expectEqual(EventKind.output_item_added, added.kind); + try testing.expectEqualStrings("function_call", added.item_type.?); + try testing.expectEqualStrings("call_9", added.call_id.?); + try testing.expectEqualStrings("echo", added.name.?); + + var d = try parseStreamEvent(allocator, + \\{"type":"response.function_call_arguments.delta","item_id":"fc_1","delta":"{\"x\":1}"} + ); + defer d.deinit(); + try testing.expectEqual(EventKind.function_call_arguments_delta, d.kind); + try testing.expectEqualStrings("{\"x\":1}", d.delta.?); + + var alt = try parseStreamEvent(allocator, + \\{"type":"response.function_call_arguments.delta","item_id":"fc_1","arguments_delta":"{\"x\":1}"} + ); + defer alt.deinit(); + try testing.expectEqual(EventKind.function_call_arguments_delta, alt.kind); + try testing.expectEqualStrings("{\"x\":1}", alt.delta.?); + + var by_index = try parseStreamEvent(allocator, + \\{"type":"response.function_call_arguments.delta","output_index":0,"delta":"{\"x\":1}"} + ); + defer by_index.deinit(); + try testing.expectEqual(EventKind.function_call_arguments_delta, by_index.kind); + try testing.expectEqual(@as(usize, 0), by_index.output_index.?); + try testing.expectEqualStrings("{\"x\":1}", by_index.delta.?); + + var done = try parseStreamEvent(allocator, + \\{"type":"response.function_call_arguments.done","output_index":0,"arguments":"{\"x\":1}"} + ); + defer done.deinit(); + try testing.expectEqual(EventKind.function_call_arguments_done, done.kind); + try testing.expectEqual(@as(usize, 0), done.output_index.?); + try testing.expectEqualStrings("{\"x\":1}", done.arguments.?); +} + +test "responses parseStreamEvent - completed usage" { + const allocator = testing.allocator; + var ev = try parseStreamEvent(allocator, + \\{"type":"response.completed","response":{"usage":{"input_tokens":100,"output_tokens":20,"input_tokens_details":{"cached_tokens":80},"output_tokens_details":{"reasoning_tokens":8}}}} + ); + defer ev.deinit(); + try testing.expectEqual(EventKind.completed, ev.kind); + try testing.expectEqual(@as(u64, 100), ev.usage.?.input_tokens); + try testing.expectEqual(@as(u64, 20), ev.usage.?.output_tokens); + try testing.expectEqual(@as(u64, 80), ev.usage.?.cached_tokens); + try testing.expectEqual(@as(u64, 8), ev.usage.?.reasoning_tokens); +} + +test "responses parseStreamEvent - done alias, phase, encrypted reasoning item" { + const allocator = testing.allocator; + var done = try parseStreamEvent(allocator, + \\{"type":"response.done","response":{"usage":{"input_tokens":1,"output_tokens":2}}} + ); + defer done.deinit(); + try testing.expectEqual(EventKind.completed, done.kind); + try testing.expectEqual(@as(u64, 1), done.usage.?.input_tokens); + + var msg = try parseStreamEvent(allocator, + \\{"type":"response.output_item.done","item":{"type":"message","id":"msg_1","phase":"commentary"}} + ); + defer msg.deinit(); + try testing.expectEqual(EventKind.output_item_done, msg.kind); + try testing.expectEqualStrings("commentary", msg.item_phase.?); + + var reasoning = try parseStreamEvent(allocator, + \\{"type":"response.output_item.done","item":{"type":"reasoning","id":"rs_1","encrypted_content":"sealed"}} + ); + defer reasoning.deinit(); + try testing.expectEqual(EventKind.output_item_done, reasoning.kind); + try testing.expect(reasoning.reasoning_item_json != null); +} + +test "responses parseStreamEvent - completed output function calls" { + const allocator = testing.allocator; + var ev = try parseStreamEvent(allocator, + \\{"type":"response.completed","response":{"output":[{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read","arguments":"{\"path\":\"a\"}","status":"completed"}],"usage":{"input_tokens":100,"output_tokens":20}}} + ); + defer ev.deinit(); + try testing.expectEqual(EventKind.completed, ev.kind); + try testing.expectEqual(@as(usize, 1), ev.completed_items.len); + try testing.expectEqual(@as(usize, 0), ev.completed_items[0].output_index); + try testing.expectEqualStrings("call_9", ev.completed_items[0].call_id.?); + try testing.expectEqualStrings("std__read", ev.completed_items[0].name.?); + try testing.expectEqualStrings("{\"path\":\"a\"}", ev.completed_items[0].arguments.?); +} + +test "responses parseStreamEvent - error + failed" { + const allocator = testing.allocator; + var e = try parseStreamEvent(allocator, + \\{"type":"error","message":"boom"} + ); + defer e.deinit(); + try testing.expectEqual(EventKind.err, e.kind); + try testing.expectEqualStrings("boom", e.error_message.?); + + var f = try parseStreamEvent(allocator, + \\{"type":"response.failed","response":{"error":{"message":"bad"}}} + ); + defer f.deinit(); + try testing.expectEqual(EventKind.failed, f.kind); + try testing.expectEqualStrings("bad", f.error_message.?); +} + +const tool_mod = @import("tool.zig"); +const NoopToolVT = struct { + fn invoke(_: *anyopaque, _: []const u8, _: Allocator) anyerror!tool_mod.ResultParts { + return error.NotImplementedInTest; + } + fn deinit_(_: *anyopaque, _: Allocator) void {} + const v: tool_mod.Tool.VTable = .{ .invoke = invoke, .deinit = deinit_ }; +}; diff --git a/src/pricing.zig b/src/pricing.zig new file mode 100644 index 0000000..0565593 --- /dev/null +++ b/src/pricing.zig @@ -0,0 +1,383 @@ +//! Per-(provider, model) token pricing and cost calculation. +//! +//! Prices are stored as integers — specifically, *micro-cents per token* +//! (1/1,000,000 of a cent), equivalently picodollars (10^-12 USD) per +//! token. Reasons: +//! +//! - All commonly-quoted "USD per million tokens" prices land on round +//! integers under the conversion: $3.00 / 1M tokens = 300 +//! micro-cents per token. +//! - Per-message token counts (10^2 to 10^5) multiplied by per-token +//! prices (~10^2) stay well inside `u64` for entire long-running +//! sessions — a session worth ~$1 (10^14 micro-cents) is nowhere +//! near `u64.max` (~1.8 × 10^19). +//! - Summing per-turn costs across a session is exact: no +//! floating-point drift. +//! +//! The TOML on-disk format lets users write `input = 3.0` for $3/Mtok, +//! which is the natural unit for humans. The loader multiplies by 100 +//! and rounds to the nearest integer; the rounding handles parse-time +//! float-precision wobble. +//! +//! `costMicroCents` does the integer arithmetic. Each `Pricing` field +//! is `?u64`: `null` means "we don't know the price for this token +//! category," which is distinct from a price of zero (e.g. OpenAI does +//! charge nothing for cache writes — that's a known 0, not unknown). +//! `costMicroCents` returns `?u64`: if any usage category with nonzero +//! count maps to a `null` price, the whole turn cost goes to `null` +//! ("unknown"), and any session-level sum that includes that turn must +//! likewise degenerate to `null`. This prevents silently treating +//! unknown costs as free. +//! +//! The display layer lives in the CLI; libpanto only computes. + +const std = @import("std"); + +const session_mod = @import("session.zig"); +pub const Usage = session_mod.Usage; + +// ============================================================================= +// Pricing struct +// ============================================================================= + +/// Per-token pricing for a single (provider, model) pair. +/// +/// Units: micro-cents per token (1/1,000,000 of a cent per token), aka +/// picodollars (10^-12 USD) per token. +/// +/// Conversion from "USD per million tokens": +/// +/// micro_cents_per_token = round(USD_per_Mtok * 100) +/// +/// So $3.00/Mtok → 300 micro-cents/token. +pub const Pricing = struct { + /// Per fresh (uncached, non-cache-written) input token. `null` = + /// unknown (e.g. field omitted from `models.toml`). + input: ?u64 = null, + /// Per output token. `null` = unknown. + output: ?u64 = null, + /// Per cache-read input token. Typically a fraction of `input` + /// (0.1× on Anthropic, 0.5× on OpenAI for cached prompt tokens). + /// `null` = unknown. + cache_read: ?u64 = null, + /// Per cache-write input token. Anthropic charges a premium + /// (1.25× `input`); OpenAI doesn't bill a cache-write rate (a + /// known 0, which should be written `cache_write = 0` rather than + /// omitted). `null` = unknown. + cache_write: ?u64 = null, + + /// Convert a USD-per-million-tokens float (the human-friendly unit) + /// to the internal integer representation. Rounds to nearest. + /// + /// `dollars_per_mtok * 1_000_000 cents/dollar / 1_000_000 tokens` = + /// cents-per-token, then * 1_000_000 micro-cents/cent = + /// micro-cents-per-token. The 1_000_000s cancel, leaving + /// `dollars_per_mtok * 100`. + /// + /// Non-finite inputs round to 0. Negative inputs are clamped to 0 + /// (treated as a known free price, not unknown). + pub fn fromDollarsPerMtok(dollars_per_mtok: f64) u64 { + if (!std.math.isFinite(dollars_per_mtok) or dollars_per_mtok <= 0) return 0; + const scaled = dollars_per_mtok * 100.0; + const r = @round(scaled); + if (r >= @as(f64, @floatFromInt(std.math.maxInt(u64)))) return std.math.maxInt(u64); + return @intFromFloat(r); + } +}; + +// ============================================================================= +// Cost calculation +// ============================================================================= + +/// Compute the cost of a single turn's `usage` under the given `pricing`, +/// in micro-cents. Returns `null` if any usage category with a nonzero +/// token count maps to a `null` price — i.e. "we used some cache reads +/// but we don't know the cache-read price" poisons the whole turn cost +/// to "unknown". Categories with zero tokens are ignored regardless of +/// whether their price is known, so e.g. a model with `cache_write = +/// null` still produces a known cost on turns that never write to +/// cache. +/// +/// `reasoning` does NOT contribute separately — it's already counted +/// inside `output`. +pub fn costMicroCents(usage: Usage, pricing: Pricing) ?u64 { + var total: u64 = 0; + total +%= component(usage.input, pricing.input) orelse return null; + total +%= component(usage.output, pricing.output) orelse return null; + total +%= component(usage.cache_read, pricing.cache_read) orelse return null; + total +%= component(usage.cache_write, pricing.cache_write) orelse return null; + return total; +} + +/// Cost contribution (in micro-cents) from a single (tokens, price) +/// pair. Returns `0` when `tokens == 0` regardless of whether `price` +/// is known — so unknown prices don't poison turns that never used +/// that category. Returns `null` when tokens are nonzero but the +/// price is unknown; callers convert that to a `null` total. +fn component(tokens: u64, price: ?u64) ?u64 { + if (tokens == 0) return 0; + const p = price orelse return null; + return tokens *% p; +} + +/// Add a turn's cost to a running session total, in micro-cents. +/// Saturating `+%` on the total (a session-bucket overflow is +/// catastrophic for a `u64` value, so we pin to max rather than wrap). +/// The poison rule matches `costMicroCents`: any individual turn +/// whose cost is unknown poisons the session total to `null`. +/// +/// This is the single accumulation point for session-level cost +/// display, so the model-switch tolerance lives here: a switch in the +/// middle of a session just means successive `costMicroCents` calls +/// run against different `Pricing` structs (one per `(provider, +/// model)`). The TUI footer's cost pass holds the registry and looks +/// up the right pricing for each turn at the time the turn lands. +pub fn addCost(total: ?u64, turn_cost: ?u64) ?u64 { + const t = total orelse return null; + const c = turn_cost orelse return null; + return t +% c; +} + +test "addCost: accumulates known costs across many turns" { + var s: ?u64 = 0; + s = addCost(s, 1_000_000); // $0.01 + s = addCost(s, 5_000_000); // $0.05 + s = addCost(s, 2_000_000); // $0.02 + try testing.expectEqual(@as(?u64, 8_000_000), s); +} + +test "addCost: a known + unknown + known sequence poisons the total" { + // The poison rule is one-way: once any priced component of any + // turn is unknown, the whole session cost is unknown forever. + var s: ?u64 = 0; + s = addCost(s, 1_000_000); // $0.01 (known) + try testing.expectEqual(@as(?u64, 1_000_000), s); + s = addCost(s, null); // poison! + try testing.expect(s == null); + s = addCost(s, 5_000_000); // still null + try testing.expect(s == null); +} + +test "addCost: tolerates a model switch mid-session (each turn's cost is per-model)" { + // A model switch in the middle of a session: each turn's + // cost is computed against the active model's pricing + // upstream of `addCost`, so the function itself just sees a + // sequence of independent turn costs. The TUI's session-cost + // display sums them all; this is the tolerance: a switch is + // invisible to the accumulator as long as both models have + // pricing entries. + var s: ?u64 = 0; + s = addCost(s, costMicroCents( + .{ .input = 100, .output = 50 }, + .{ .input = 300, .output = 1500 }, + ).?); + // Switch to a different-priced model. + s = addCost(s, costMicroCents( + .{ .input = 200, .output = 100 }, + .{ .input = 100, .output = 500 }, + ).?); + // 100*300 + 50*1500 = 105_000 + // 200*100 + 100*500 = 70_000 + // total = 175_000 micro-cents = $0.00175 -> $0.00 (rounded) + try testing.expectEqual(@as(?u64, 175_000), s); +} + +test "addCost: a switch from a priced model to an unpriced model poisons" { + // The new model has no pricing entry: `costMicroCents` returns + // null (every priced component is null), and `addCost` then + // poisons the session total to null. The cost UP TO the + // switch is "known"; after it the total is "unknown". + var s: ?u64 = 0; + s = addCost(s, costMicroCents( + .{ .input = 100, .output = 50 }, + .{ .input = 300, .output = 1500 }, + ).?); + // Switch to an unpriced model. + s = addCost(s, costMicroCents( + .{ .input = 200, .output = 100 }, + .{}, // no pricing at all + )); + try testing.expect(s == null); +} + +// ============================================================================= +// Registry +// ============================================================================= + +/// In-memory registry of `(provider, model) -> Pricing`. Lookups are by +/// exact match of both fields. +/// +/// The registry owns the (provider, model) key strings; entries are +/// pushed via `set` (which duplicates the inputs). +pub const Registry = struct { + allocator: std.mem.Allocator, + entries: std.ArrayList(Entry), + + pub const Entry = struct { + provider: []u8, + model: []u8, + pricing: Pricing, + }; + + pub fn init(allocator: std.mem.Allocator) Registry { + return .{ .allocator = allocator, .entries = .empty }; + } + + pub fn deinit(self: *Registry) void { + for (self.entries.items) |e| { + self.allocator.free(e.provider); + self.allocator.free(e.model); + } + self.entries.deinit(self.allocator); + } + + /// Look up pricing for (provider, model). Returns null if no entry + /// matches — distinct from "price is zero," which is a valid entry. + pub fn get(self: *const Registry, provider: []const u8, model: []const u8) ?Pricing { + for (self.entries.items) |e| { + if (std.mem.eql(u8, e.provider, provider) and std.mem.eql(u8, e.model, model)) { + return e.pricing; + } + } + return null; + } + + /// Insert or replace pricing for (provider, model). Duplicates the + /// key strings. + pub fn set( + self: *Registry, + provider: []const u8, + model: []const u8, + pricing: Pricing, + ) !void { + for (self.entries.items) |*e| { + if (std.mem.eql(u8, e.provider, provider) and std.mem.eql(u8, e.model, model)) { + e.pricing = pricing; + return; + } + } + const provider_copy = try self.allocator.dupe(u8, provider); + errdefer self.allocator.free(provider_copy); + const model_copy = try self.allocator.dupe(u8, model); + errdefer self.allocator.free(model_copy); + try self.entries.append(self.allocator, .{ + .provider = provider_copy, + .model = model_copy, + .pricing = pricing, + }); + } + + pub fn count(self: *const Registry) usize { + return self.entries.items.len; + } +}; + +// ============================================================================= +// Tests +// ============================================================================= + +const testing = std.testing; + +test "Pricing.fromDollarsPerMtok: $3.00/Mtok -> 300 micro-cents/token" { + try testing.expectEqual(@as(u64, 300), Pricing.fromDollarsPerMtok(3.0)); + try testing.expectEqual(@as(u64, 1500), Pricing.fromDollarsPerMtok(15.0)); + try testing.expectEqual(@as(u64, 30), Pricing.fromDollarsPerMtok(0.3)); + try testing.expectEqual(@as(u64, 0), Pricing.fromDollarsPerMtok(0)); + try testing.expectEqual(@as(u64, 0), Pricing.fromDollarsPerMtok(-1)); +} + +test "Pricing.fromDollarsPerMtok: rounds float noise to clean integer" { + // 0.1 * 3 = 0.30000000000000004 in IEEE 754. Verify rounding + // recovers the clean integer. + const v = 0.1 * 3.0; + try testing.expectEqual(@as(u64, 30), Pricing.fromDollarsPerMtok(v)); +} + +test "costMicroCents: standard mixed input/output" { + const pricing: Pricing = .{ + .input = 300, // $3/Mtok + .output = 1500, // $15/Mtok + }; + const usage: Usage = .{ .input = 1000, .output = 200 }; + // 1000*300 + 200*1500 = 300_000 + 300_000 = 600_000 micro-cents. + // = 6 cents = $0.06. + try testing.expectEqual(@as(?u64, 600_000), costMicroCents(usage, pricing)); +} + +test "costMicroCents: cache_read and cache_write discounts are honored" { + const pricing: Pricing = .{ + .input = 300, + .output = 1500, + .cache_read = 30, // 0.1x input + .cache_write = 375, // 1.25x input + }; + const usage: Usage = .{ + .input = 1000, + .output = 200, + .cache_read = 5000, + .cache_write = 500, + }; + // 1000*300 + 200*1500 + 5000*30 + 500*375 + // = 300_000 + 300_000 + 150_000 + 187_500 = 937_500. + try testing.expectEqual(@as(?u64, 937_500), costMicroCents(usage, pricing)); +} + +test "costMicroCents: reasoning tokens do not double-count" { + const pricing: Pricing = .{ .input = 0, .output = 1500 }; + const usage: Usage = .{ .output = 100, .reasoning = 60 }; + // Cost is from `output` alone; reasoning is a subset of output. + try testing.expectEqual(@as(?u64, 150_000), costMicroCents(usage, pricing)); +} + +test "costMicroCents: unknown price + nonzero usage poisons to null" { + // gpt-4o written with only input/output set; cache fields default + // to null (unknown). A turn that uses any cache reads should + // surface as unknown cost, not silently free. + const pricing: Pricing = .{ + .input = 250, + .output = 1000, + // cache_read, cache_write left null. + }; + const usage: Usage = .{ .input = 1000, .output = 200, .cache_read = 500 }; + try testing.expectEqual(@as(?u64, null), costMicroCents(usage, pricing)); +} + +test "costMicroCents: unknown price + zero usage stays known" { + // Same partially-specified pricing, but the turn never touched + // cache. Cost should remain known. + const pricing: Pricing = .{ .input = 250, .output = 1000 }; + const usage: Usage = .{ .input = 1000, .output = 200 }; + try testing.expectEqual(@as(?u64, 450_000), costMicroCents(usage, pricing)); +} + +test "Registry: set, get, replace" { + var reg = Registry.init(testing.allocator); + defer reg.deinit(); + + try testing.expect(reg.get("anthropic", "claude-sonnet-4") == null); + + try reg.set("anthropic", "claude-sonnet-4", .{ .input = 300, .output = 1500 }); + try testing.expectEqual(@as(usize, 1), reg.count()); + + const p = reg.get("anthropic", "claude-sonnet-4").?; + try testing.expectEqual(@as(?u64, 300), p.input); + try testing.expectEqual(@as(?u64, 1500), p.output); + + // Replace existing. + try reg.set("anthropic", "claude-sonnet-4", .{ .input = 400, .output = 1600 }); + try testing.expectEqual(@as(usize, 1), reg.count()); + const p2 = reg.get("anthropic", "claude-sonnet-4").?; + try testing.expectEqual(@as(?u64, 400), p2.input); +} + +test "Registry: distinct (provider, model) pairs" { + var reg = Registry.init(testing.allocator); + defer reg.deinit(); + try reg.set("openai", "gpt-4o", .{ .input = 250 }); + try reg.set("openai", "gpt-4o-mini", .{ .input = 15 }); + try reg.set("anthropic", "claude-sonnet-4", .{ .input = 300 }); + try testing.expectEqual(@as(usize, 3), reg.count()); + try testing.expectEqual(@as(?u64, 250), reg.get("openai", "gpt-4o").?.input); + try testing.expectEqual(@as(?u64, 15), reg.get("openai", "gpt-4o-mini").?.input); + try testing.expectEqual(@as(?u64, 300), reg.get("anthropic", "claude-sonnet-4").?.input); +} diff --git a/src/provider.zig b/src/provider.zig new file mode 100644 index 0000000..e12ff59 --- /dev/null +++ b/src/provider.zig @@ -0,0 +1,554 @@ +const std = @import("std"); +const http = std.http; +const Uri = std.Uri; + +const config_mod = @import("config.zig"); +const conversation = @import("conversation.zig"); +const tool_registry_mod = @import("tool_registry.zig"); +const session_mod = @import("session.zig"); +const stream_mod = @import("stream.zig"); +pub const ToolRegistry = tool_registry_mod.ToolRegistry; +pub const Usage = session_mod.Usage; + +const EventQueue = stream_mod.EventQueue; + +/// Open a streaming `POST` to `uri` carrying `body`, with the shared transport +/// options every provider uses (identity encoding so gzip can't buffer SSE +/// frames, no keep-alive, no redirects). `req` must point at pinned storage — +/// the body writer and `receiveHead` borrow it, and the caller keeps it for +/// the response's lifetime. On success the request is left open (the caller +/// owns it) and the received response head is returned; on failure the request +/// is cleaned up before returning the error. +pub fn sendRequest( + client: *http.Client, + uri: Uri, + extra_headers: []const http.Header, + body: []const u8, + req: *http.Client.Request, +) !http.Client.Response { + req.* = try client.request(.POST, uri, .{ + .extra_headers = extra_headers, + // Disable compression: gzip buffers small SSE frames, defeating the + // streaming property we paid for `stream: true` to get. + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .keep_alive = false, + .redirect_behavior = .not_allowed, + }); + errdefer req.deinit(); + + req.transfer_encoding = .{ .content_length = body.len }; + var send_buf: [4096]u8 = undefined; + var bw = try req.sendBodyUnflushed(&send_buf); + try bw.writer.writeAll(body); + try bw.end(); + try req.connection.?.flush(); + + var redirect_buf: [1024]u8 = undefined; + return try req.receiveHead(&redirect_buf); +} + +/// Handle a >=400 provider response: capture `Retry-After`, drain the body +/// (capped at 16 KiB) for diagnostics, classify the status, log it (demoting +/// recoverable auth failures to `.debug`), stash status + retry into `diag`, +/// and return the classified error for the caller to propagate. `transfer_buf` +/// backs the drain reader; `name` is the provider's log prefix. +pub fn classifyErrorResponse( + allocator: std.mem.Allocator, + response: *http.Client.Response, + transfer_buf: []u8, + diag: ?*ProviderDiagnostic, + name: []const u8, +) ProviderError { + // `head.bytes` (which `iterateHeaders` walks) points into the connection + // read buffer and is invalidated the moment the body stream is + // initialized below. Capture Retry-After first. + const retry_after_ms = retryAfterFromHead(response.head); + const body_reader = response.reader(transfer_buf); + var err_buf: std.ArrayList(u8) = .empty; + defer err_buf.deinit(allocator); + var tmp: [1024]u8 = undefined; + while (true) { + const n = body_reader.readSliceShort(&tmp) catch break; + if (n == 0) break; + err_buf.appendSlice(allocator, tmp[0..n]) catch break; + if (err_buf.items.len > 16 * 1024) break; + } + const status: u16 = @intFromEnum(response.head.status); + const classified = classifyHttpStatus(status, err_buf.items); + // 401/403 is routinely recovered by the turn-runner's forced token + // refresh + reopen; demote it to `.debug` (still in the debug log) so a + // transparent refresh doesn't surface a scary error line. The retry layer + // raises a hard error only if recovery ultimately fails. + if (classified == error.ProviderAuthFailed) { + std.log.debug("{s} HTTP {d} (recoverable auth): {s}", .{ name, status, err_buf.items }); + } else { + std.log.err("{s} HTTP {d}: {s}", .{ name, status, err_buf.items }); + } + if (diag) |d| { + d.status_code = status; + d.retry_after_ms = retry_after_ms; + } + return classified; +} + +/// Decode a wire tool name (`__` -> `.`) in place within an assembled name +/// buffer. Decoding only ever shrinks the buffer (reads stay ahead of writes), +/// so aliasing src/dst is safe; we then truncate to the decoded length. +/// Unambiguous because internal names never contain a literal `__`. +pub fn decodeNameInPlace(name_buf: *conversation.TextualBlock) void { + const decoded = tool_registry_mod.decodeName(name_buf.items, name_buf.items); + name_buf.items.len = decoded.len; +} + +/// Combine a stream error's `kind` and `message` into one owned, human-readable +/// string (either may be absent). Returns null when both are absent. Caller +/// owns the result. +pub fn formatStreamError( + allocator: std.mem.Allocator, + kind: ?[]const u8, + message: ?[]const u8, +) std.mem.Allocator.Error!?[]u8 { + if (kind != null and message != null) + return try std.fmt.allocPrint(allocator, "{s}: {s}", .{ kind.?, message.? }); + if (kind) |k| return try allocator.dupe(u8, k); + if (message) |m| return try allocator.dupe(u8, m); + return null; +} + +pub const ContentBlockType = enum { + Text, + Thinking, + ToolUse, + ToolResult, +}; + +/// Heuristic detector for provider context-overflow rejections, applied to +/// an HTTP 400 error response body. Both OpenAI-compatible and Anthropic +/// APIs reject oversized requests on the input side with HTTP 400 and a +/// recognizable message; matching it lets the agent compact and retry +/// instead of surfacing a hard error. +/// +/// Markers (case-sensitive substrings, as the wire emits them): +/// - OpenAI: `context_length_exceeded`, `maximum context length` +/// - Anthropic: `prompt is too long` +/// Merge a provider's built-in request headers with caller-supplied +/// `extra_headers` (config `Header`s) into one `[]std.http.Header`, allocated +/// with `alloc`. The caller owns the result and must free it once the request +/// has been sent. `base` comes first; `extra` is appended in order, so an +/// `extra` header with the same name as a base header is sent as a second +/// occurrence (provider-identity headers in practice never collide with the +/// fixed content-type/accept/authorization set). +pub fn mergeHeaders( + alloc: std.mem.Allocator, + base: []const std.http.Header, + extra: []const config_mod.Header, +) ![]std.http.Header { + const out = try alloc.alloc(std.http.Header, base.len + extra.len); + @memcpy(out[0..base.len], base); + for (extra, 0..) |h, i| out[base.len + i] = .{ .name = h.name, .value = h.value }; + return out; +} + +/// Splice a pre-encoded JSON value into the current stringifier position. +/// Used to embed a tool's `input_schema` (and a replayed tool_use `input`) +/// verbatim into a request body. On parse failure, emit `{}` so we never +/// produce invalid wire JSON — an empty object is the correct degenerate +/// value on the wire for both uses. +pub fn writeRawJson(s: *std.json.Stringify, raw: []const u8) !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const parsed = std.json.parseFromSlice(std.json.Value, arena.allocator(), raw, .{}) catch { + try s.beginObject(); + try s.endObject(); + return; + }; + try s.write(parsed.value); +} + +pub fn isContextOverflowBody(body: []const u8) bool { + const markers = [_][]const u8{ + "context_length_exceeded", + "maximum context length", + "prompt is too long", + "context window", + }; + for (markers) |m| { + if (std.mem.indexOf(u8, body, m) != null) return true; + } + return false; +} + +/// Distinct provider/API failure classes the agent needs in order to decide +/// between retrying, compacting, and hard-failing. The transport/stream layer +/// maps HTTP status codes and connection failures onto these so the agent's +/// retry policy can switch on a stable, provider-agnostic name rather than a +/// broad `error.HttpError`. +/// +/// Zig errors cannot carry payloads, so status code, `Retry-After`, and the +/// provider's diagnostic message ride alongside via `ProviderDiagnostic` (an +/// out-parameter the provider fills before returning the error). +pub const ProviderError = error{ + /// HTTP 429. Retryable; honor `Retry-After` when present. + ProviderRateLimited, + /// HTTP 503 or a server explicitly signalling unavailability. Retryable. + ProviderUnavailable, + /// HTTP 500/502/504. Retryable. + ProviderServerError, + /// Connection reset, DNS/connect failure, TLS failure, request timeout + /// (HTTP 408), or other transport-level failure before a response. Also + /// covers retryable conflict/not-ready statuses (409, 425). Retryable. + ProviderTransport, + /// The provider stream ended or was malformed before a complete + /// assistant message was committed. Retryable (subject to policy). + ProviderStreamMalformed, + /// HTTP 401/403. Not retryable — a credentials/permissions problem. + ProviderAuthFailed, + /// HTTP 400 (other than context overflow). Not retryable — the request + /// itself is malformed. + ProviderBadRequest, + /// HTTP 404 shaped as an unknown-model error. Not retryable. + ProviderModelNotFound, + /// The input context exceeds the model's window (HTTP 400 + a recognized + /// context marker). Handled by one-shot compaction, not ordinary retry. + ContextOverflow, +}; + +/// Side-channel for the payload a `ProviderError` cannot carry. The provider +/// fills the relevant fields immediately before returning a classified error; +/// the agent reads them to drive backoff and retry notifications. Reset to +/// `.{}` before each provider attempt. +pub const ProviderDiagnostic = struct { + /// The HTTP status code, when the failure carried one. + status_code: ?u16 = null, + /// Parsed `Retry-After` delay in milliseconds, when the provider sent it. + retry_after_ms: ?u64 = null, + /// The provider's diagnostic message (borrowed/owned per caller; the + /// agent treats it as borrowed for the lifetime of the failed attempt). + message: ?[]const u8 = null, + + pub fn reset(self: *ProviderDiagnostic) void { + self.* = .{}; + } +}; + +/// True for the provider errors the agent's retry policy may retry. Context +/// overflow is deliberately excluded — it has a separate one-shot compaction +/// path. Auth, bad-request, and model-not-found are terminal. +pub fn isRetryableProviderError(err: anyerror) bool { + return switch (err) { + error.ProviderRateLimited, + error.ProviderUnavailable, + error.ProviderServerError, + error.ProviderTransport, + error.ProviderStreamMalformed, + error.ProviderOverloaded, + => true, + else => false, + }; +} + +/// Map an HTTP status code from a provider response onto a `ProviderError`. +/// The `body` is inspected only for the 400 case, to separate context +/// overflow (compact-and-retry) from an ordinary bad request (hard-fail). +/// +/// Caller is responsible for stashing the status code (and any `Retry-After`) +/// into a `ProviderDiagnostic`; this function only chooses the error name. +pub fn classifyHttpStatus(status: u16, body: []const u8) ProviderError { + return switch (status) { + 400 => if (isContextOverflowBody(body)) error.ContextOverflow else error.ProviderBadRequest, + 401, 403 => error.ProviderAuthFailed, + 404 => error.ProviderModelNotFound, + 408 => error.ProviderTransport, + 409, 425 => error.ProviderTransport, + 429 => error.ProviderRateLimited, + 503 => error.ProviderUnavailable, + 500, 502, 504 => error.ProviderServerError, + else => if (status >= 500) error.ProviderServerError else error.ProviderBadRequest, + }; +} + +/// Parse an HTTP `Retry-After` header value into milliseconds. Supports the +/// delta-seconds form (`"120"`); the HTTP-date form is not parsed and returns +/// null (the agent then falls back to its computed backoff). Returns null for +/// empty or unparseable values. +pub fn parseRetryAfterMs(value: []const u8) ?u64 { + const trimmed = std.mem.trim(u8, value, " \t\r\n"); + if (trimmed.len == 0) return null; + const secs = std.fmt.parseInt(u64, trimmed, 10) catch return null; + return secs *| std.time.ms_per_s; +} + +/// Find a `Retry-After` header (case-insensitive) in an HTTP response head +/// and parse it into milliseconds. `head` is a `std.http.Client.Response.Head` +/// (taken as `anytype` to avoid importing the http types here). Returns null +/// when absent or unparseable. +pub fn retryAfterFromHead(head: anytype) ?u64 { + var it = head.iterateHeaders(); + while (it.next()) |h| { + if (std.ascii.eqlIgnoreCase(h.name, "retry-after")) { + return parseRetryAfterMs(h.value); + } + } + return null; +} + +/// Details handed to the receiver before the agent sleeps for a provider +/// retry. Purely a UI/runtime event — never persisted to the session. +pub const ProviderRetryInfo = struct { + /// The attempt that just failed, 1-based (1 = the initial attempt). + attempt: usize, + /// Total attempts the policy will make, including the first. + max_attempts: usize, + /// How long the agent will sleep before the next attempt, in ms. + delay_ms: u64, + /// The classified error that triggered the retry. + err: anyerror, + /// HTTP status code, when known. + status_code: ?u16 = null, + /// Provider-sent `Retry-After`, when present, in ms. + retry_after_ms: ?u64 = null, + /// Provider diagnostic message, when known. + message: ?[]const u8 = null, + /// True when the retry is a context-overflow compaction attempt rather + /// than an ordinary backoff retry. Compaction retries report + /// `delay_ms == 0`. + compaction: bool = false, +}; + +/// A resumable provider streaming response: the pull-side projection of one +/// provider HTTP turn. It wraps the per-provider `ResumableResponse` (which +/// owns the pinned HTTP request/response, body reader, `SSEParser`, and +/// decode state) behind a tag so the agent loop can pump any provider +/// uniformly. +/// +/// `produce(out)` reads just enough bytes to append one or more `Event`s to +/// `out` (or reach response-complete), so the `Stream` drains the queue +/// before pumping again. On response completion the assistant message has +/// been committed to the conversation and a terminal `message_complete` +/// pushed. +/// +/// Tool-use identity (`id`, `name`) is delivered via a `tool_details` event, +/// pushed once per ToolUse block at the earliest moment both fields are +/// known. For Anthropic this is immediately after `block_start`, before any +/// deltas. For OpenAI Chat Completions this may be partway through the +/// arg-deltas, since the wire protocol can split `id` and `name` across +/// multiple streaming chunks. The guarantees: it fires strictly after the +/// block's `block_start`, strictly before its `block_complete`, and at most +/// once per ToolUse block. It never fires for non-ToolUse blocks. If a +/// tool_use block is dropped because identity never fully arrived, neither +/// `tool_details` nor `block_complete` fire for it. +/// +/// The terminal `message_complete`'s `usage` carries the wire-reported token +/// counts for the just-finished assistant message. It is `null` only when the +/// wire genuinely delivered no usage — chiefly OpenAI-compatible proxies +/// (OpenRouter, vLLM, some self-hosted backends) that ignore +/// `stream_options.include_usage`. Consumers that compute cost should record +/// the null case explicitly ("unknown") rather than treating it as zero. +pub const ProviderStream = struct { + ptr: *anyopaque, + vtable: *const VTable, + + pub const ProduceStatus = enum { more, response_complete }; + + pub const VTable = struct { + /// Pump the response, appending decoded events to `out`. Returns + /// `.more` (pump again) or `.response_complete` (the assistant + /// message is committed; a terminal `message_complete` was pushed). + produce: *const fn (*anyopaque, *EventQueue) anyerror!ProduceStatus, + /// Free the response and any owned state. + deinit: *const fn (*anyopaque) void, + /// Optional: after a failed `produce`, return the provider's + /// diagnostic message for the failure (e.g. an Anthropic + /// `overloaded_error` message), borrowed for the lifetime of the + /// response. Null when the provider has nothing to add beyond the + /// classified error name. + last_error: ?*const fn (*anyopaque) ?[]const u8 = null, + }; + + /// Pump the response, appending decoded events to `out`. Errors are + /// genuine failures (transport/parse/provider). + pub fn produce(self: ProviderStream, out: *EventQueue) anyerror!ProduceStatus { + return self.vtable.produce(self.ptr, out); + } + + pub fn deinit(self: ProviderStream) void { + self.vtable.deinit(self.ptr); + } + + /// The provider's diagnostic message for the most recent `produce` + /// failure, if any. Borrowed for the lifetime of the response. + pub fn lastError(self: ProviderStream) ?[]const u8 { + const f = self.vtable.last_error orelse return null; + return f(self.ptr); + } +}; + +/// Open one streaming provider turn against the active config snapshot, +/// returning a resumable `ProviderStream`. Performs the POST and reads +/// response headers (classifying any >=400 status into a provider error), +/// but does not pump the body — that happens lazily via +/// `ProviderStream.produce`. +/// +/// This is the single dispatch point: it switches on `cfg.provider`'s +/// `APIStyle` tag, builds a transient per-request object bound to the +/// process-global HTTP client, and opens it. There is no persistent provider +/// object — every turn re-reads `cfg`, so swapping the agent's +/// `*const Config` between turns changes provider, model, base_url, and the +/// visible tool set with no transport teardown. +/// +/// The tool registry is supplied by the caller (the `Agent` owns it now, +/// not `cfg`); the serializers receive it directly. On success the caller +/// owns the returned `ProviderStream` and must `deinit` it. +pub fn openStream( + allocator: std.mem.Allocator, + io: std.Io, + cfg: *const config_mod.Config, + registry: *const ToolRegistry, + conv: *conversation.Conversation, + diag: ?*ProviderDiagnostic, +) anyerror!ProviderStream { + // Imported lazily to break the circular module graph: + // provider.zig <- provider_openai_chat.zig <- provider.zig. + const provider_openai_chat = @import("provider_openai_chat.zig"); + const provider_anthropic_messages = @import("provider_anthropic_messages.zig"); + const provider_openai_responses = @import("provider_openai_responses.zig"); + const client = config_mod.httpClient(); + switch (cfg.provider) { + .openai_chat => |*c| { + var req: provider_openai_chat.OpenAIChatRequest = .{ + .allocator = allocator, + .io = io, + .config = c, + .http_client = client, + .diag = diag, + }; + const rr = try req.open(conv, registry); + return rr.providerStream(); + }, + .anthropic_messages => |*c| { + var req: provider_anthropic_messages.AnthropicMessagesRequest = .{ + .allocator = allocator, + .io = io, + .config = c, + .http_client = client, + .diag = diag, + }; + const rr = try req.open(conv, registry); + return rr.providerStream(); + }, + .openai_responses => |*c| { + var req: provider_openai_responses.OpenAIResponsesRequest = .{ + .allocator = allocator, + .io = io, + .config = c, + .http_client = client, + .diag = diag, + }; + const rr = try req.open(conv, registry); + return rr.providerStream(); + }, + .openai_codex_responses => |*c| { + var req: provider_openai_responses.OpenAIResponsesRequest = .{ + .allocator = allocator, + .io = io, + .config = c, + .dialect = .codex, + .http_client = client, + .diag = diag, + }; + const rr = try req.open(conv, registry); + return rr.providerStream(); + }, + } +} + +/// The shape of `openStream`, exposed as a function-pointer type so the agent +/// can carry an injectable seam (real dispatch in production, a stub in +/// tests) without resurrecting a per-provider vtable. +pub const OpenStreamFn = *const fn ( + allocator: std.mem.Allocator, + io: std.Io, + cfg: *const config_mod.Config, + registry: *const ToolRegistry, + conv: *conversation.Conversation, + diag: ?*ProviderDiagnostic, +) anyerror!ProviderStream; + +test "mergeHeaders - base first, extra appended, converted to http.Header" { + const t2 = std.testing; + const base = [_]std.http.Header{ + .{ .name = "content-type", .value = "application/json" }, + .{ .name = "authorization", .value = "Bearer x" }, + }; + const extra = [_]config_mod.Header{ + .{ .name = "Copilot-Integration-Id", .value = "vscode-chat" }, + .{ .name = "X-Initiator", .value = "user" }, + }; + const merged = try mergeHeaders(t2.allocator, &base, &extra); + defer t2.allocator.free(merged); + try t2.expectEqual(@as(usize, 4), merged.len); + try t2.expectEqualStrings("content-type", merged[0].name); + try t2.expectEqualStrings("Copilot-Integration-Id", merged[2].name); + try t2.expectEqualStrings("user", merged[3].value); +} + +test "mergeHeaders - empty extra yields a copy of base" { + const t2 = std.testing; + const base = [_]std.http.Header{.{ .name = "accept", .value = "text/event-stream" }}; + const merged = try mergeHeaders(t2.allocator, &base, &.{}); + defer t2.allocator.free(merged); + try t2.expectEqual(@as(usize, 1), merged.len); + try t2.expectEqualStrings("accept", merged[0].name); +} + +test "isContextOverflowBody - matches known markers, rejects others" { + const t2 = std.testing; + try t2.expect(isContextOverflowBody("{\"error\":{\"code\":\"context_length_exceeded\"}}")); + try t2.expect(isContextOverflowBody("This model's maximum context length is 8192 tokens")); + try t2.expect(isContextOverflowBody("prompt is too long: 250000 tokens > 200000 maximum")); + try t2.expect(!isContextOverflowBody("{\"error\":{\"code\":\"invalid_api_key\"}}")); + try t2.expect(!isContextOverflowBody("rate limit exceeded")); +} + +test "classifyHttpStatus - maps statuses to provider errors" { + const t2 = std.testing; + try t2.expectEqual(error.ContextOverflow, classifyHttpStatus(400, "prompt is too long")); + try t2.expectEqual(error.ProviderBadRequest, classifyHttpStatus(400, "bad json")); + try t2.expectEqual(error.ProviderAuthFailed, classifyHttpStatus(401, "")); + try t2.expectEqual(error.ProviderAuthFailed, classifyHttpStatus(403, "")); + try t2.expectEqual(error.ProviderModelNotFound, classifyHttpStatus(404, "")); + try t2.expectEqual(error.ProviderTransport, classifyHttpStatus(408, "")); + try t2.expectEqual(error.ProviderTransport, classifyHttpStatus(409, "")); + try t2.expectEqual(error.ProviderTransport, classifyHttpStatus(425, "")); + try t2.expectEqual(error.ProviderRateLimited, classifyHttpStatus(429, "")); + try t2.expectEqual(error.ProviderServerError, classifyHttpStatus(500, "")); + try t2.expectEqual(error.ProviderServerError, classifyHttpStatus(502, "")); + try t2.expectEqual(error.ProviderUnavailable, classifyHttpStatus(503, "")); + try t2.expectEqual(error.ProviderServerError, classifyHttpStatus(504, "")); + try t2.expectEqual(error.ProviderServerError, classifyHttpStatus(599, "")); + try t2.expectEqual(error.ProviderBadRequest, classifyHttpStatus(418, "")); +} + +test "isRetryableProviderError - retryable vs terminal" { + const t2 = std.testing; + try t2.expect(isRetryableProviderError(error.ProviderRateLimited)); + try t2.expect(isRetryableProviderError(error.ProviderUnavailable)); + try t2.expect(isRetryableProviderError(error.ProviderServerError)); + try t2.expect(isRetryableProviderError(error.ProviderTransport)); + try t2.expect(isRetryableProviderError(error.ProviderStreamMalformed)); + try t2.expect(!isRetryableProviderError(error.ProviderAuthFailed)); + try t2.expect(!isRetryableProviderError(error.ProviderBadRequest)); + try t2.expect(!isRetryableProviderError(error.ProviderModelNotFound)); + try t2.expect(!isRetryableProviderError(error.ContextOverflow)); + try t2.expect(!isRetryableProviderError(error.Canceled)); +} + +test "parseRetryAfterMs - delta-seconds and rejects" { + const t2 = std.testing; + try t2.expectEqual(@as(?u64, 120_000), parseRetryAfterMs("120")); + try t2.expectEqual(@as(?u64, 0), parseRetryAfterMs("0")); + try t2.expectEqual(@as(?u64, 2_000), parseRetryAfterMs(" 2 ")); + try t2.expectEqual(@as(?u64, null), parseRetryAfterMs("")); + // HTTP-date form is not parsed. + try t2.expectEqual(@as(?u64, null), parseRetryAfterMs("Wed, 21 Oct 2015 07:28:00 GMT")); +} diff --git a/src/provider_anthropic_messages.zig b/src/provider_anthropic_messages.zig new file mode 100644 index 0000000..955991a --- /dev/null +++ b/src/provider_anthropic_messages.zig @@ -0,0 +1,1267 @@ +//! Anthropic Messages API streaming provider. +//! +//! Wire format reference: +//! https://platform.claude.com/docs/en/build-with-claude/streaming +//! +//! Responsibilities: +//! - Convert `Conversation` → request JSON (delegated to anthropic_messages_json.zig) +//! - POST to `{base_url}/v1/messages` with `stream: true` +//! (the provider owns the current `/v1` suffix; a future wire revision +//! would add a new `anthropic_messages_v2` API style rather than guessing +//! from the configured base URL) +//! - Read the chunked body, feed bytes through SSEParser +//! - Parse each event payload, drive a thin assembly loop, and emit Receiver +//! callbacks. Anthropic gives us explicit block boundaries, so no +//! state-machine inference is needed. +//! - Assemble the final Message and emit onMessageComplete. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Io = std.Io; +const http = std.http; +const Uri = std.Uri; + +const conversation = @import("conversation.zig"); +const provider_mod = @import("provider.zig"); +const stream_mod = @import("stream.zig"); +const sse_mod = @import("sse.zig"); +const json_mod = @import("anthropic_messages_json.zig"); +const config_mod = @import("config.zig"); +const tool_registry_mod = @import("tool_registry.zig"); + +const Event = stream_mod.Event; +const EventQueue = stream_mod.EventQueue; + +/// A single Anthropic Messages streaming request. Transient: constructed +/// per `streamStep`, holds only borrowed state (allocator, io, the global +/// HTTP client, and the active config). Carries nothing across requests. +pub const AnthropicMessagesRequest = struct { + allocator: Allocator, + io: Io, + config: *const config_mod.AnthropicMessagesConfig, + http_client: *http.Client, + /// Optional diagnostic side-channel; see `OpenAIChatRequest.diag`. + diag: ?*provider_mod.ProviderDiagnostic = null, + + /// Open the streaming HTTP request and return a heap-allocated resumable + /// response. Reads response headers (classifying any >=400 status) but + /// does not pump the body — that happens lazily in + /// `ResumableResponse.produce`. On success the caller owns the returned + /// `*ResumableResponse` and must `deinit` it. + pub fn open( + self: *AnthropicMessagesRequest, + conv: *conversation.Conversation, + tools: *const provider_mod.ToolRegistry, + ) !*ResumableResponse { + const rr = try self.allocator.create(ResumableResponse); + errdefer self.allocator.destroy(rr); + rr.* = .{ + .allocator = self.allocator, + .conv = conv, + .parser = sse_mod.SSEParser.init(self.allocator), + .state = .init(self.allocator), + }; + rr.state.signature_origin = try conversation.SignatureOrigin.init( + self.allocator, + .anthropic_messages, + self.config.base_url, + self.config.model, + ); + errdefer { + rr.parser.deinit(); + rr.state.deinit(); + } + + const trimmed_base = std.mem.trim(u8, self.config.base_url, "/"); + const url = try std.fmt.allocPrint( + self.allocator, + "{s}/v1/messages", + .{trimmed_base}, + ); + defer self.allocator.free(url); + + const uri = try Uri.parse(url); + + const body = try json_mod.serializeRequest(self.allocator, self.config, conv, tools); + defer self.allocator.free(body); + std.log.debug("anthropic_messages => {s}", .{body}); + + // Build headers. Standard Anthropic-compatible backends use + // `x-api-key`. OAuth-backed Anthropic-compatible providers (e.g. + // Copilot) opt into `Authorization: Bearer ...` via + // `config.use_bearer_auth`, which is derived from the configured auth + // family rather than guessed from the URL/headers. The four base + // headers are always present; the interleaved-thinking beta header is + // added only when the config explicitly requests manual extended + // thinking with interleaving. It is intentionally NOT sent for + // `.adaptive` (interleaving is automatic there and the header causes + // 400s on some backends) or `.disabled`. + const use_bearer_auth = self.config.use_bearer_auth; + const auth_value = if (use_bearer_auth) + try std.fmt.allocPrint( + self.allocator, + "Bearer {s}", + .{self.config.api_key}, + ) + else + ""; + defer if (use_bearer_auth) self.allocator.free(auth_value); + const auth_header: http.Header = if (use_bearer_auth) + .{ .name = "authorization", .value = auth_value } + else + .{ .name = "x-api-key", .value = self.config.api_key }; + var headers_buf: [5]http.Header = .{ + .{ .name = "content-type", .value = "application/json" }, + .{ .name = "accept", .value = "text/event-stream" }, + auth_header, + .{ .name = "anthropic-version", .value = self.config.api_version }, + undefined, // slot reserved for the optional beta header + }; + const send_interleaved = self.config.thinking == .enabled and + self.config.thinking_interleaved; + if (send_interleaved) { + headers_buf[4] = .{ + .name = "anthropic-beta", + .value = "interleaved-thinking-2025-05-14", + }; + } + const base_headers = headers_buf[0..if (send_interleaved) @as(usize, 5) else @as(usize, 4)]; + // Merge any provider `extra_headers` onto the base set. Freed at the + // end of `open` — after the request body has been flushed. + const extra_headers = try provider_mod.mergeHeaders( + self.allocator, + base_headers, + self.config.extra_headers, + ); + defer self.allocator.free(extra_headers); + + rr.response = try provider_mod.sendRequest(self.http_client, uri, extra_headers, body, &rr.req); + rr.req_open = true; + errdefer { + rr.req.deinit(); + rr.req_open = false; + } + + // A >=400 status maps to a retryable/terminal provider error. Anthropic + // rejects oversized requests with HTTP 400 + "prompt is too long", + // which `classifyErrorResponse` maps to ContextOverflow (compact+retry). + if (@intFromEnum(rr.response.head.status) >= 400) { + return provider_mod.classifyErrorResponse(self.allocator, &rr.response, &rr.transfer_buf, self.diag, "anthropic_messages"); + } + + rr.body_reader = rr.response.reader(&rr.transfer_buf); + return rr; + } +}; + +/// A resumable Anthropic Messages streaming response. Owns the pinned HTTP +/// request/response, the body reader's transfer buffer, the `SSEParser`, and +/// the block-assembly `StreamState`. Must be heap-allocated and never moved: +/// `body_reader` borrows `&self.response`. +pub const ResumableResponse = struct { + allocator: Allocator, + conv: *conversation.Conversation, + parser: sse_mod.SSEParser, + state: StreamState, + + req: http.Client.Request = undefined, + response: http.Client.Response = undefined, + transfer_buf: [4096]u8 = undefined, + body_reader: *std.Io.Reader = undefined, + chunk: [4096]u8 = undefined, + + req_open: bool = false, + done: bool = false, + + pub const ProduceStatus = provider_mod.ProviderStream.ProduceStatus; + + /// Wrap this response in the provider-agnostic `ProviderStream` the agent + /// loop drives. + pub fn providerStream(self: *ResumableResponse) provider_mod.ProviderStream { + return .{ .ptr = self, .vtable = &vtable }; + } + + const vtable: provider_mod.ProviderStream.VTable = .{ + .produce = produceVT, + .deinit = deinitVT, + .last_error = lastErrorVT, + }; + + fn produceVT(ptr: *anyopaque, out: *EventQueue) anyerror!ProduceStatus { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + return self.produce(out); + } + + fn lastErrorVT(ptr: *anyopaque) ?[]const u8 { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + return self.state.stream_error_message; + } + + fn deinitVT(ptr: *anyopaque) void { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + self.deinit(); + } + + pub fn deinit(self: *ResumableResponse) void { + if (self.req_open) self.req.deinit(); + self.parser.deinit(); + self.state.deinit(); + self.allocator.destroy(self); + } + + /// Pump the response: read one chunk, feed it through the SSE parser, and + /// decode each SSE event into zero or more `Event`s appended to `out`. + /// Returns `.more` if the caller should pump again, or + /// `.response_complete` once `message_stop` (or EOF) is reached and the + /// assistant message has been committed + a final `message_complete` + /// pushed. + pub fn produce(self: *ResumableResponse, out: *EventQueue) !ProduceStatus { + if (self.done) return .response_complete; + + var vecs: [1][]u8 = .{&self.chunk}; + const n = self.body_reader.readVec(&vecs) catch |err| switch (err) { + // Stream ended without an explicit message_stop. Finalize anyway. + error.EndOfStream => { + try self.finishStream(out); + return .response_complete; + }, + // Transport failure before the message completed: retryable. + else => return error.ProviderStreamMalformed, + }; + if (n == 0) return .more; + + const events = try self.parser.feed(self.chunk[0..n]); + defer self.parser.freeEvents(events); + + for (events) |ev_payload| { + std.log.debug("anthropic_messages <= {s}", .{ev_payload}); + try handleEvent(self.allocator, ev_payload, &self.state, out); + if (self.state.end_of_stream) { + try self.finishStream(out); + return .response_complete; + } + } + return .more; + } + + fn finishStream(self: *ResumableResponse, out: *EventQueue) !void { + if (self.done) return; + self.done = true; + try self.state.finalize(out, self.conv); + } +}; + +/// State maintained across the streaming response. +/// +/// Anthropic gives us explicit block boundaries (`content_block_start` / +/// `content_block_stop`), so we don't need to infer transitions like +/// `provider_openai_chat` does. We just track the currently-open block. +const StreamState = struct { + allocator: Allocator, + started: bool = false, + end_of_stream: bool = false, + finalized: bool = false, + + /// The block currently being assembled (if any). + active: ?ActiveBlock = null, + + /// Assembled blocks for the final message, in stream order. + blocks: std.ArrayList(conversation.ContentBlock) = .empty, + + /// Accumulated token counts. Anthropic reports the input-side counts + /// on `message_start.usage` and the final `output_tokens` (plus + /// possibly updated input-side counts) on `message_delta.usage`. + /// `usage_seen` distinguishes "genuinely all zero" from "never + /// reported" — the former stamps a real `Usage` on + /// `onMessageComplete`, the latter stamps `null`. + usage: provider_mod.Usage = .{}, + usage_seen: bool = false, + signature_origin: ?conversation.SignatureOrigin = null, + stop_reason: ?[]u8 = null, + /// Owned, human-readable description of a mid-stream `error` event + /// (e.g. `"overloaded_error: Overloaded"`), surfaced to the agent via + /// `ProviderStream.lastError` so the retry notice can show *why*. + stream_error_message: ?[]u8 = null, + + const ActiveBlock = struct { + /// Index reported on the wire (Anthropic's content-array index). + wire_index: usize, + kind: BlockKind, + text_buf: conversation.TextualBlock = .empty, + signature: ?[]const u8 = null, + /// Populated for `.tool_use` blocks. Owned by this state until the + /// block closes, at which point ownership transfers to the + /// ToolUseBlock. + tool_id: ?[]u8 = null, + tool_name: ?[]u8 = null, + }; + + const BlockKind = enum { text, thinking, tool_use, unsupported }; + + fn init(allocator: Allocator) StreamState { + return .{ .allocator = allocator }; + } + + fn deinit(self: *StreamState) void { + if (self.active) |*a| { + a.text_buf.deinit(self.allocator); + if (a.signature) |sig| self.allocator.free(sig); + if (a.tool_id) |s| self.allocator.free(s); + if (a.tool_name) |s| self.allocator.free(s); + } + for (self.blocks.items) |*b| b.deinit(self.allocator); + self.blocks.deinit(self.allocator); + if (self.signature_origin) |*o| o.deinit(self.allocator); + if (self.stop_reason) |s| self.allocator.free(s); + if (self.stream_error_message) |s| self.allocator.free(s); + } + + fn ensureStarted(self: *StreamState, out: *EventQueue) !void { + if (self.started) return; + self.started = true; + try out.push(.{ .message_start = .assistant }); + } + + /// Merge a wire-level usage snapshot into the accumulated counts. + /// Missing fields mean "unchanged," not "reset to zero." Marks + /// `usage_seen` so `finalize` delivers a non-null `Usage` to + /// `onMessageComplete`. + fn mergeUsage(self: *StreamState, partial: json_mod.StreamUsage) void { + if (partial.input_tokens) |v| self.usage.input = v; + if (partial.output_tokens) |v| self.usage.output = v; + if (partial.cache_creation_input_tokens) |v| self.usage.cache_write = v; + if (partial.cache_read_input_tokens) |v| self.usage.cache_read = v; + if (partial.input_tokens != null or partial.output_tokens != null or + partial.cache_creation_input_tokens != null or partial.cache_read_input_tokens != null) + { + self.usage_seen = true; + } + } + + fn openBlock( + self: *StreamState, + out: *EventQueue, + wire_index: usize, + kind: BlockKind, + tool_id: ?[]const u8, + tool_name: ?[]const u8, + ) !void { + // Defensive: if a prior block didn't get an explicit stop, drop it. + if (self.active != null) { + self.discardActive(); + } + var ab: ActiveBlock = .{ + .wire_index = wire_index, + .kind = kind, + }; + // For tool_use blocks, capture the identity fields. Anthropic + // delivers both whole on content_block_start. The wire name is + // encoded (`__` for `.`); decode it here so everything downstream + // — onToolDetails, the stored ContentBlock, session logs, and + // dispatch — sees the internal (dotted) name. The decoded form is + // never longer than the wire form. + if (kind == .tool_use) { + if (tool_id) |id| ab.tool_id = try self.allocator.dupe(u8, id); + if (tool_name) |n| { + // Decode `__` -> `.` into an exact-size owned buffer so the + // stored slice is freeable as a whole allocation. + const owned = try self.allocator.alloc(u8, n.len); + errdefer self.allocator.free(owned); + const decoded = tool_registry_mod.decodeName(owned, n); + if (decoded.len == n.len) { + ab.tool_name = owned; + } else { + ab.tool_name = try self.allocator.realloc(owned, decoded.len); + } + } + } + self.active = ab; + const block_type: ?provider_mod.ContentBlockType = switch (kind) { + .text => .Text, + .thinking => .Thinking, + .tool_use => .ToolUse, + .unsupported => null, + }; + if (block_type) |bt| { + try out.push(.{ .block_start = .{ .block_type = bt, .index = wire_index } }); + // Anthropic delivers tool id+name whole on content_block_start, + // so we can fire tool_details immediately — before any arg + // deltas. If the wire was malformed and either field is + // missing, skip: closeBlock will drop the block defensively. + // id/name are owned by `ab` (stable) but we dupe into the queue + // arena for a uniform borrow lifetime. + if (kind == .tool_use) { + if (ab.tool_id != null and ab.tool_name != null) { + try out.push(.{ .tool_details = .{ + .index = wire_index, + .id = try out.dupeBytes(ab.tool_id.?), + .name = try out.dupeBytes(ab.tool_name.?), + } }); + } + } + } + } + + fn appendTextDelta( + self: *StreamState, + out: *EventQueue, + delta: []const u8, + ) !void { + const a = &(self.active orelse return); + if (a.kind == .unsupported) return; + try a.text_buf.appendSlice(self.allocator, delta); + // Dupe into the queue arena: `delta` borrows the transient SSE/JSON + // payload that `produce` frees before `next()` reads the queue. + try out.push(.{ .content_delta = .{ + .index = a.wire_index, + .delta = try out.dupeBytes(delta), + } }); + } + + /// Append a chunk of the streamed JSON arguments for the active + /// tool_use block. No-op if the active block isn't a tool_use. + fn appendInputJsonDelta( + self: *StreamState, + out: *EventQueue, + delta: []const u8, + ) !void { + const a = &(self.active orelse return); + if (a.kind != .tool_use) return; + try a.text_buf.appendSlice(self.allocator, delta); + try out.push(.{ .content_delta = .{ + .index = a.wire_index, + .delta = try out.dupeBytes(delta), + } }); + } + + fn setSignature(self: *StreamState, sig: []const u8) !void { + const a = &(self.active orelse return); + if (a.signature) |old| self.allocator.free(old); + a.signature = try self.allocator.dupe(u8, sig); + } + + fn setStopReason(self: *StreamState, reason: ?[]const u8) !void { + if (self.stop_reason) |old| self.allocator.free(old); + self.stop_reason = if (reason) |r| try self.allocator.dupe(u8, r) else null; + } + + /// Record a readable description of a mid-stream `error` event, combining + /// the error `kind` and `message` into one owned string (either may be + /// absent). Replaces any previous value. + fn setStreamErrorMessage(self: *StreamState, kind: ?[]const u8, message: ?[]const u8) !void { + if (self.stream_error_message) |old| self.allocator.free(old); + self.stream_error_message = try provider_mod.formatStreamError(self.allocator, kind, message); + } + + /// Close the active block: append it to `blocks` and emit block_complete. + fn closeBlock( + self: *StreamState, + out: *EventQueue, + ) !void { + var a = self.active orelse return; + self.active = null; + + if (a.kind == .unsupported) { + a.text_buf.deinit(self.allocator); + if (a.signature) |sig| self.allocator.free(sig); + if (a.tool_id) |s| self.allocator.free(s); + if (a.tool_name) |s| self.allocator.free(s); + return; + } + // tool_use blocks require both id and name. If either is missing + // (malformed stream), drop the block defensively. + if (a.kind == .tool_use and (a.tool_id == null or a.tool_name == null)) { + a.text_buf.deinit(self.allocator); + if (a.tool_id) |s| self.allocator.free(s); + if (a.tool_name) |s| self.allocator.free(s); + return; + } + + const block: conversation.ContentBlock = switch (a.kind) { + .text => blk: { + if (a.signature) |sig| self.allocator.free(sig); + break :blk .{ .Text = a.text_buf }; + }, + .thinking => .{ .Thinking = .{ + .text = a.text_buf, + .signature = a.signature, + } }, + // An interrupted/malformed tool_use (incomplete or non-object + // input JSON) is preserved as-is. The agent's dispatch path + // detects invalid input and answers it with a synthetic error + // ToolResult in the *following user message* — emitting a + // ToolResult here would wrongly place it in this assistant + // message, which Anthropic rejects. + .tool_use => blk: { + break :blk .{ .ToolUse = .{ + .id = a.tool_id.?, + .name = a.tool_name.?, + .input = a.text_buf, + } }; + }, + .unsupported => unreachable, + }; + + try self.blocks.append(self.allocator, block); + try out.push(.{ .block_complete = .{ + .index = a.wire_index, + .block = self.blocks.items[self.blocks.items.len - 1], + } }); + } + + /// Drop the active block without emitting a completion callback. + /// Used when an unexpected `content_block_start` arrives before the + /// previous block closed. + fn discardActive(self: *StreamState) void { + if (self.active) |*a| { + a.text_buf.deinit(self.allocator); + if (a.signature) |sig| self.allocator.free(sig); + if (a.tool_id) |s| self.allocator.free(s); + if (a.tool_name) |s| self.allocator.free(s); + self.active = null; + } + } + + fn finalize( + self: *StreamState, + out: *EventQueue, + conv: *conversation.Conversation, + ) !void { + if (self.finalized) return; + self.finalized = true; + + if (self.active != null) { + // Preserve an interrupted tool call so the agent can answer it + // with a synthetic error ToolResult instead of invoking it. + try self.closeBlock(out); + } + + const moved_blocks = try self.blocks.toOwnedSlice(self.allocator); + defer self.allocator.free(moved_blocks); + + const usage: ?provider_mod.Usage = if (self.usage_seen) self.usage else null; + if (self.signature_origin) |origin| { + try conversation.setThinkingOrigins( + self.allocator, + moved_blocks, + origin.api_style, + origin.base_url, + origin.model, + ); + } + try conv.addAssistantMessage(moved_blocks, usage); + + const msg = conv.messages.items[conv.messages.items.len - 1]; + try out.push(.{ .message_complete = .{ .message = msg, .usage = usage } }); + } +}; + +fn handleEvent( + allocator: Allocator, + payload: []const u8, + state: *StreamState, + out: *EventQueue, +) !void { + var parsed = try json_mod.parseStreamEvent(allocator, payload); + defer parsed.deinit(); + + switch (parsed.event) { + .message_start => |s| { + try state.ensureStarted(out); + state.mergeUsage(s.usage); + }, + .content_block_start => |s| { + try state.ensureStarted(out); + const kind: StreamState.BlockKind = switch (s.kind) { + .text => .text, + .thinking => .thinking, + .tool_use => .tool_use, + .unknown => .unsupported, + }; + try state.openBlock(out, s.index, kind, s.tool_id, s.tool_name); + }, + .content_block_delta => |d| { + if (d.text_delta) |t| try state.appendTextDelta(out, t); + if (d.thinking_delta) |t| try state.appendTextDelta(out, t); + if (d.signature_delta) |sig| try state.setSignature(sig); + if (d.input_json_delta) |j| try state.appendInputJsonDelta(out, j); + }, + .content_block_stop => |s| { + if (state.active) |a| { + if (a.wire_index == s.index) try state.closeBlock(out); + } + }, + .message_delta => |d| { + try state.setStopReason(d.stop_reason); + state.mergeUsage(d.usage); + }, + .message_stop => { + state.end_of_stream = true; + }, + .ping => {}, + .@"error" => |e| { + if (!@import("builtin").is_test) { + std.log.err("anthropic stream error: {?s}: {?s}", .{ e.kind, e.message }); + } + // Stash a readable description so the agent's retry notice can + // explain *why* the stream failed instead of only showing the + // bare error name. Owned by `state`; freed in `deinit`. + state.setStreamErrorMessage(e.kind, e.message) catch {}; + // Mid-stream error event before the message was committed. Map + // the common overload case to a dedicated retryable error so the + // UI can say "overloaded" rather than "malformed stream"; other + // kinds stay as the generic retryable malformed-stream error. + if (e.kind) |k| { + if (std.mem.eql(u8, k, "overloaded_error")) return error.ProviderOverloaded; + } + return error.ProviderStreamMalformed; + }, + .unknown => { + // Forward-compatible: ignore unknown event types per Anthropic's + // versioning policy. + }, + } +} + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const testing = std.testing; + +/// Records the decoded pull `Event`s for assertions. Keeps the same typed +/// schema the old RecordingReceiver exposed (message_start / block_start / +/// delta / block_complete / message_complete), so the existing assertions +/// are preserved verbatim. The recorder owns copies of all byte payloads +/// (the queue arena is reset on full drain). +const EventRecorder = struct { + allocator: Allocator, + events: std.ArrayList(Rec) = .empty, + + const Rec = union(enum) { + message_start: conversation.MessageRole, + block_start: struct { + kind: provider_mod.ContentBlockType, + index: usize, + }, + delta: struct { + index: usize, + bytes: []const u8, // owned copy + }, + block_complete: struct { + index: usize, + kind: provider_mod.ContentBlockType, + text: []const u8, // owned copy + signature: ?[]const u8 = null, // owned copy when present + }, + message_complete: ?provider_mod.Usage, + }; + + fn init(allocator: Allocator) EventRecorder { + return .{ .allocator = allocator }; + } + + fn deinit(self: *EventRecorder) void { + for (self.events.items) |ev| { + switch (ev) { + .delta => |d| self.allocator.free(d.bytes), + .block_complete => |b| { + self.allocator.free(b.text); + if (b.signature) |s| self.allocator.free(s); + }, + else => {}, + } + } + self.events.deinit(self.allocator); + } + + /// Translate one pull `Event` into the recorder's schema. Tool identity + /// (`tool_details`) is dropped: the anthropic tests assert tool-use + /// blocks via the ContentBlock in conv after finalize, not via the + /// event stream. Tool-arg `content_delta`s are also dropped here because + /// the old recorder only recorded text/thinking deltas (it routed tool + /// args through a separate path that didn't call onContentDelta in a way + /// these tests observe) — we preserve that by only recording deltas for + /// the currently text/thinking block. Since the recorder can't see block + /// kind from a bare delta, we record every delta; the existing tests + /// only assert delta bytes for text/thinking turns, so this is + /// equivalent for them. + fn record(self: *EventRecorder, ev: Event) !void { + switch (ev) { + .message_start => |role| try self.events.append(self.allocator, .{ .message_start = role }), + .block_start => |b| try self.events.append(self.allocator, .{ .block_start = .{ + .kind = b.block_type, + .index = b.index, + } }), + .content_delta => |d| { + const copy = try self.allocator.dupe(u8, d.delta); + try self.events.append(self.allocator, .{ .delta = .{ .index = d.index, .bytes = copy } }); + }, + .block_complete => |bc| switch (bc.block) { + .Text => |tb| { + const txt = try self.allocator.dupe(u8, tb.items); + try self.events.append(self.allocator, .{ .block_complete = .{ + .kind = .Text, + .index = bc.index, + .text = txt, + } }); + }, + .Thinking => |tb| { + const txt = try self.allocator.dupe(u8, tb.text.items); + const sig = if (tb.signature) |s| try self.allocator.dupe(u8, s) else null; + try self.events.append(self.allocator, .{ .block_complete = .{ + .kind = .Thinking, + .index = bc.index, + .text = txt, + .signature = sig, + } }); + }, + else => {}, + }, + .message_complete => |m| try self.events.append(self.allocator, .{ .message_complete = m.usage }), + else => {}, + } + } +}; + +fn runStreamedTurn( + allocator: Allocator, + conv: *conversation.Conversation, + rec: ?*EventRecorder, + events: []const []const u8, +) !void { + var state: StreamState = .init(allocator); + defer state.deinit(); + + var queue = EventQueue.init(allocator); + defer queue.deinit(); + + for (events) |payload| { + try handleEvent(allocator, payload, &state, &queue); + if (state.end_of_stream) break; + } + try state.finalize(&queue, conv); + + while (queue.pop()) |ev| { + if (rec) |r| try r.record(ev); + } +} + +/// Test helper: append a single-text user message. `addUserMessage` now +/// takes a block slice (symmetric with `addAssistantMessage`); this wraps +/// the common plain-text case the tests below use. +fn addUserText(conv: *conversation.Conversation, text: []const u8) !void { + const tb = try conversation.textualBlockFromSlice(conv.allocator, text); + var block: conversation.ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + +test "streams a text-only turn end-to-end" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hello"); + + var rec = EventRecorder.init(allocator); + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","content":[],"model":"claude"}} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"!"}} + , + \\{"type":"content_block_stop","index":0} + , + \\{"type":"message_delta","delta":{"stop_reason":"end_turn"}} + , + \\{"type":"message_stop"} + , + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + // Conversation now holds the assistant reply. + try testing.expectEqual(@as(usize, 2), conv.messages.items.len); + try testing.expectEqual(conversation.MessageRole.assistant, conv.messages.items[1].role); + try testing.expectEqual(@as(usize, 1), conv.messages.items[1].content.items.len); + try testing.expectEqualStrings( + "Hello!", + conv.messages.items[1].content.items[0].Text.items, + ); + + // Callback sequence: msg_start, block_start, delta, delta, block_complete, msg_complete. + try testing.expectEqual(@as(usize, 6), rec.events.items.len); + try testing.expectEqual(conversation.MessageRole.assistant, rec.events.items[0].message_start); + try testing.expectEqual(provider_mod.ContentBlockType.Text, rec.events.items[1].block_start.kind); + try testing.expectEqualStrings("Hello", rec.events.items[2].delta.bytes); + try testing.expectEqualStrings("!", rec.events.items[3].delta.bytes); + try testing.expectEqualStrings("Hello!", rec.events.items[4].block_complete.text); + try testing.expect(rec.events.items[5] == .message_complete); + // No usage on the wire — the assertion is structural. + try testing.expect(rec.events.items[5].message_complete == null); +} + +test "anthropic: captures usage from message_start and message_delta on message_complete" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var rec = EventRecorder.init(allocator); + defer rec.deinit(); + + const events = [_][]const u8{ + // Initial input-side counts on message_start. + \\{"type":"message_start","message":{"id":"m","type":"message","role":"assistant","content":[],"model":"claude","usage":{"input_tokens":100,"cache_creation_input_tokens":50,"cache_read_input_tokens":200}}} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}} + , + \\{"type":"content_block_stop","index":0} + , + // Final output count on message_delta. + \\{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":42}} + , + \\{"type":"message_stop"} + , + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + // Find the message_complete event and check its usage payload. + var found: ?provider_mod.Usage = null; + for (rec.events.items) |ev| { + if (ev == .message_complete) found = ev.message_complete; + } + try testing.expect(found != null); + const u = found.?; + try testing.expectEqual(@as(u64, 100), u.input); + try testing.expectEqual(@as(u64, 42), u.output); + try testing.expectEqual(@as(u64, 200), u.cache_read); + try testing.expectEqual(@as(u64, 50), u.cache_write); + try testing.expectEqual(@as(u64, 0), u.reasoning); // Anthropic doesn't split reasoning separately. +} + +test "anthropic: message_complete carries null usage when wire omits it" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var rec = EventRecorder.init(allocator); + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"type":"message_start","message":{"id":"m","role":"assistant","content":[],"model":"claude"}} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}} + , + \\{"type":"content_block_stop","index":0} + , + \\{"type":"message_delta","delta":{"stop_reason":"end_turn"}} + , + \\{"type":"message_stop"} + , + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + var saw_complete = false; + for (rec.events.items) |ev| { + if (ev == .message_complete) { + saw_complete = true; + try testing.expect(ev.message_complete == null); + } + } + try testing.expect(saw_complete); +} + +test "captures thinking signature for round-trip" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "solve"); + + var rec = EventRecorder.init(allocator); + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"type":"message_start","message":{"role":"assistant"}} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"step one"}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" step two"}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"EqQBabc"}} + , + \\{"type":"content_block_stop","index":0} + , + \\{"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}} + , + \\{"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"answer"}} + , + \\{"type":"content_block_stop","index":1} + , + \\{"type":"message_stop"} + , + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + // The assistant message has Thinking + Text, with signature on the Thinking. + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 2), asst.content.items.len); + try testing.expectEqualStrings("step one step two", asst.content.items[0].Thinking.text.items); + try testing.expectEqualStrings("EqQBabc", asst.content.items[0].Thinking.signature.?); + try testing.expectEqualStrings("answer", asst.content.items[1].Text.items); +} + +test "signature-only thinking block (display omitted)" { + // Anthropic emits a thinking block with only a signature_delta when + // `display: "omitted"` is configured. Verify we still capture the + // signature with empty thinking text. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var rec = EventRecorder.init(allocator); + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"type":"message_start","message":{"role":"assistant"}} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"sig123"}} + , + \\{"type":"content_block_stop","index":0} + , + \\{"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}} + , + \\{"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"hi back"}} + , + \\{"type":"content_block_stop","index":1} + , + \\{"type":"message_stop"} + , + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 2), asst.content.items.len); + try testing.expectEqualStrings("", asst.content.items[0].Thinking.text.items); + try testing.expectEqualStrings("sig123", asst.content.items[0].Thinking.signature.?); +} + +test "ping and unknown events are ignored" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var rec = EventRecorder.init(allocator); + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"type":"message_start","message":{"role":"assistant"}} + , + \\{"type":"ping"} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + , + \\{"type":"ping"} + , + \\{"type":"future_event_type","whatever":true} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}} + , + \\{"type":"content_block_stop","index":0} + , + \\{"type":"message_stop"} + , + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + try testing.expectEqualStrings( + "ok", + conv.messages.items[1].content.items[0].Text.items, + ); +} + +test "tool_use blocks are captured with id, name, and assembled input" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "use a tool"); + + var rec = EventRecorder.init(allocator); + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"type":"message_start","message":{"role":"assistant"}} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"tu_1","name":"calc","input":{}}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"x\":"}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"1}"}} + , + \\{"type":"content_block_stop","index":0} + , + \\{"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}} + , + \\{"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"done"}} + , + \\{"type":"content_block_stop","index":1} + , + \\{"type":"message_stop"} + , + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 2), asst.content.items.len); + + const tu = asst.content.items[0].ToolUse; + try testing.expectEqualStrings("tu_1", tu.id); + try testing.expectEqualStrings("calc", tu.name); + try testing.expectEqualStrings("{\"x\":1}", tu.input.items); + + try testing.expectEqualStrings("done", asst.content.items[1].Text.items); +} + +test "inbound wire tool name is decoded to dotted form" { + // Anthropic delivers the (wire-encoded) name whole at + // content_block_start; it is decoded to the internal dotted form for + // the conversation, session logs, and dispatch. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "use a tool"); + + var rec = EventRecorder.init(allocator); + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"type":"message_start","message":{"role":"assistant"}} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"t1","name":"calc__sum","input":{}}} + , + \\{"type":"content_block_stop","index":0} + , + \\{"type":"message_stop"} + , + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("calc.sum", tu.name); +} + +test "error event propagates as Zig error" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var queue = EventQueue.init(allocator); + defer queue.deinit(); + + var state: StreamState = .init(allocator); + defer state.deinit(); + + try handleEvent( + allocator, + \\{"type":"message_start","message":{"role":"assistant"}} + , + &state, + &queue, + ); + + const result = handleEvent( + allocator, + \\{"type":"error","error":{"type":"overloaded_error","message":"too busy"}} + , + &state, + &queue, + ); + // `overloaded_error` maps to the dedicated retryable error, and the + // provider's diagnostic is stashed for the agent's retry notice. + try testing.expectError(error.ProviderOverloaded, result); + try testing.expectEqualStrings("overloaded_error: too busy", state.stream_error_message.?); +} + +test "non-overloaded error event stays malformed and stashes message" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var queue = EventQueue.init(allocator); + defer queue.deinit(); + + var state: StreamState = .init(allocator); + defer state.deinit(); + + const result = handleEvent( + allocator, + \\{"type":"error","error":{"type":"api_error","message":"boom"}} + , + &state, + &queue, + ); + try testing.expectError(error.ProviderStreamMalformed, result); + try testing.expectEqualStrings("api_error: boom", state.stream_error_message.?); +} + +test "two streamed turns persist assistant replies in the conversation" { + // Same regression scenario as the openai_chat test, adapted to Anthropic. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try conv.addSystemMessage("Be brief."); + try addUserText(&conv, "hi"); + + var rec = EventRecorder.init(allocator); + defer rec.deinit(); + + const turn1 = [_][]const u8{ + \\{"type":"message_start","message":{"role":"assistant"}} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi!"}} + , + \\{"type":"content_block_stop","index":0} + , + \\{"type":"message_stop"} + , + }; + try runStreamedTurn(allocator, &conv, &rec, &turn1); + + try addUserText(&conv, "what did you say?"); + + const turn2 = [_][]const u8{ + \\{"type":"message_start","message":{"role":"assistant"}} + , + \\{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + , + \\{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I said: Hi!"}} + , + \\{"type":"content_block_stop","index":0} + , + \\{"type":"message_stop"} + , + }; + try runStreamedTurn(allocator, &conv, &rec, &turn2); + + // system + user + assistant + user + assistant = 5 + try testing.expectEqual(@as(usize, 5), conv.messages.items.len); + try testing.expectEqualStrings( + "I said: Hi!", + conv.messages.items[4].content.items[0].Text.items, + ); +} + +/// Helper: whether this config sends Bearer auth instead of `x-api-key`. +fn headerSliceUsesBearerAuth(cfg: *const config_mod.AnthropicMessagesConfig) bool { + return cfg.use_bearer_auth; +} + +/// Helper: build the header slice exactly as `open` does, given a config, +/// and return whether the interleaved beta header is present. +/// This lets us test the header-selection logic without a live HTTP connection. +fn headerSliceIncludesInterleaved(cfg: *const config_mod.AnthropicMessagesConfig) bool { + const send_interleaved = cfg.thinking == .enabled and cfg.thinking_interleaved; + return send_interleaved; +} + +test "oauth-backed anthropic auth uses bearer auth" { + const cfg: config_mod.AnthropicMessagesConfig = .{ + .api_key = "k", + .base_url = "https://api.individual.githubcopilot.com", + .model = "claude-sonnet-4-5", + .use_bearer_auth = true, + }; + try testing.expect(headerSliceUsesBearerAuth(&cfg)); +} + +test "plain anthropic auth uses x-api-key" { + const cfg: config_mod.AnthropicMessagesConfig = .{ + .api_key = "k", + .base_url = "https://api.anthropic.com", + .model = "claude-sonnet-4-5", + }; + try testing.expect(!headerSliceUsesBearerAuth(&cfg)); +} + +test "interleaved beta header: enabled when thinking=.enabled and interleaved=true" { + const cfg: config_mod.AnthropicMessagesConfig = .{ + .api_key = "k", + .base_url = "u", + .model = "m", + .thinking = .enabled, + .thinking_interleaved = true, + }; + try testing.expect(headerSliceIncludesInterleaved(&cfg)); +} + +test "interleaved beta header: absent when thinking=.enabled and interleaved=false" { + const cfg: config_mod.AnthropicMessagesConfig = .{ + .api_key = "k", + .base_url = "u", + .model = "m", + .thinking = .enabled, + .thinking_interleaved = false, + }; + try testing.expect(!headerSliceIncludesInterleaved(&cfg)); +} + +test "interleaved beta header: absent when thinking=.adaptive even if interleaved=true" { + const cfg: config_mod.AnthropicMessagesConfig = .{ + .api_key = "k", + .base_url = "u", + .model = "m", + .thinking = .adaptive, + .thinking_interleaved = true, + }; + // .adaptive does not send the header; interleaving is automatic there. + try testing.expect(!headerSliceIncludesInterleaved(&cfg)); +} + +test "interleaved beta header: absent when thinking=.disabled" { + const cfg: config_mod.AnthropicMessagesConfig = .{ + .api_key = "k", + .base_url = "u", + .model = "m", + .thinking = .disabled, + .thinking_interleaved = true, + }; + try testing.expect(!headerSliceIncludesInterleaved(&cfg)); +} diff --git a/src/provider_openai_chat.zig b/src/provider_openai_chat.zig new file mode 100644 index 0000000..4ac1e11 --- /dev/null +++ b/src/provider_openai_chat.zig @@ -0,0 +1,1306 @@ +//! OpenAI Chat Completions streaming provider. +//! +//! Wire format reference: https://platform.openai.com/docs/api-reference/chat/streaming +//! +//! Responsibilities: +//! - Convert `Conversation` → request JSON (delegated to openai_chat_json.zig) +//! - POST to `{base_url}/chat/completions` with `stream: true` +//! - Read the chunked body, feed bytes through SSEParser +//! - Parse each event payload, drive the block boundary state machine, +//! and emit Receiver callbacks +//! - Assemble the final Message and emit onMessageComplete + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Io = std.Io; +const http = std.http; +const Uri = std.Uri; + +const conversation = @import("conversation.zig"); +const provider_mod = @import("provider.zig"); +const stream_mod = @import("stream.zig"); +const sse_mod = @import("sse.zig"); +const json_mod = @import("openai_chat_json.zig"); +const config_mod = @import("config.zig"); + +const Event = stream_mod.Event; +const EventQueue = stream_mod.EventQueue; + +const decodeNameInPlace = provider_mod.decodeNameInPlace; + +/// Active streaming block type tracked by the state machine. Mirrors the +/// `ContentBlock` union variants but adds `.none` for "no block open yet". +const ActiveBlock = enum { none, text, thinking, tool_use }; + +/// A single OpenAI Chat streaming request. Transient: constructed per +/// `streamStep`, holds only borrowed state (allocator, io, the global HTTP +/// client, and the active config). Carries nothing across requests, so it +/// is created inline by the free `streamStep` entry point below. +pub const OpenAIChatRequest = struct { + allocator: Allocator, + io: Io, + config: *const config_mod.OpenAIChatConfig, + http_client: *http.Client, + /// Optional diagnostic side-channel. When non-null, classified failures + /// stash the HTTP status code and any `Retry-After` here for the agent's + /// retry policy. Strings written here are not owned by the diagnostic; + /// they live only as long as this request object. + diag: ?*provider_mod.ProviderDiagnostic = null, + + /// Open the streaming HTTP request and return a heap-allocated + /// resumable response. Performs the POST and reads response headers + /// (classifying any >=400 status into a provider error), but does NOT + /// pump the body — that happens lazily in `ResumableResponse.produce`. + /// + /// On success the caller owns the returned `*ResumableResponse` and must + /// `deinit` it. On failure nothing is allocated. + pub fn open( + self: *OpenAIChatRequest, + conv: *conversation.Conversation, + tools: *const provider_mod.ToolRegistry, + ) !*ResumableResponse { + const rr = try self.allocator.create(ResumableResponse); + errdefer self.allocator.destroy(rr); + rr.* = .{ + .allocator = self.allocator, + .conv = conv, + .parser = sse_mod.SSEParser.init(self.allocator), + .state = .init(self.allocator), + }; + errdefer { + rr.parser.deinit(); + rr.state.deinit(); + } + + // Build URL: "{base_url}/chat/completions" + const url = try std.fmt.allocPrint( + self.allocator, + "{s}/chat/completions", + .{self.config.base_url}, + ); + defer self.allocator.free(url); + + const uri = try Uri.parse(url); + + // Build the request body. + const body = try json_mod.serializeRequest(self.allocator, self.config, conv, tools); + defer self.allocator.free(body); + std.log.debug("openai_chat => {s}", .{body}); + + // Auth header + const auth_value = try std.fmt.allocPrint( + self.allocator, + "Bearer {s}", + .{self.config.api_key}, + ); + defer self.allocator.free(auth_value); + + const base_headers = [_]http.Header{ + .{ .name = "content-type", .value = "application/json" }, + .{ .name = "accept", .value = "text/event-stream" }, + .{ .name = "authorization", .value = auth_value }, + }; + // Merge provider `extra_headers` (e.g. Copilot editor identity, or + // auth-exchange-derived headers) onto the base set. Freed at the end + // of `open` — after the request body has been flushed. + const extra_headers = try provider_mod.mergeHeaders( + self.allocator, + &base_headers, + self.config.extra_headers, + ); + defer self.allocator.free(extra_headers); + + // Open the request. We can't use `fetch()` because it buffers the + // response; we want to stream the body as it arrives. The request + // is moved into the heap struct so the body reader (which borrows + // `&rr.response`) stays valid across `produce` calls. + rr.response = try provider_mod.sendRequest(self.http_client, uri, extra_headers, body, &rr.req); + rr.req_open = true; + errdefer { + rr.req.deinit(); + rr.req_open = false; + } + + // A >=400 status classifies into a retryable/terminal provider error. + // HTTP 400 with a context marker becomes `ContextOverflow` so the + // caller can compact and retry rather than hard-fail. + if (@intFromEnum(rr.response.head.status) >= 400) { + return provider_mod.classifyErrorResponse(self.allocator, &rr.response, &rr.transfer_buf, self.diag, "openai_chat"); + } + + // Bind the streaming body reader. Valid for the lifetime of `rr` + // (it borrows `&rr.response` and `&rr.transfer_buf`, both pinned). + rr.body_reader = rr.response.reader(&rr.transfer_buf); + return rr; + } +}; + +/// A resumable OpenAI Chat streaming response. Owns the pinned HTTP +/// request/response, the body reader's transfer buffer, the `SSEParser`, +/// and the block-assembly `StreamState`. `produce` pumps just enough bytes +/// to emit one or more events into the queue, or reports the response is +/// complete (its assistant message committed to the conversation). +/// +/// Must be heap-allocated and never moved: `body_reader` borrows +/// `&self.response`. +pub const ResumableResponse = struct { + allocator: Allocator, + conv: *conversation.Conversation, + parser: sse_mod.SSEParser, + state: StreamState, + + req: http.Client.Request = undefined, + response: http.Client.Response = undefined, + /// Transfer buffer backing `body_reader`. Pinned in the heap struct. + transfer_buf: [4096]u8 = undefined, + /// The streaming body reader, bound in `open` after a 2xx response. + body_reader: *std.Io.Reader = undefined, + /// Chunk scratch for `readVec`. + chunk: [4096]u8 = undefined, + + /// True once `req` has been initialized (so `deinit` knows to free it). + req_open: bool = false, + /// Set once the response is fully decoded and finalized. + done: bool = false, + + pub const ProduceStatus = provider_mod.ProviderStream.ProduceStatus; + + /// Wrap this response in the provider-agnostic `ProviderStream` the agent + /// loop drives. + pub fn providerStream(self: *ResumableResponse) provider_mod.ProviderStream { + return .{ .ptr = self, .vtable = &vtable }; + } + + const vtable: provider_mod.ProviderStream.VTable = .{ + .produce = produceVT, + .deinit = deinitVT, + .last_error = lastErrorVT, + }; + + fn lastErrorVT(ptr: *anyopaque) ?[]const u8 { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + return self.state.stream_error_message; + } + + fn produceVT(ptr: *anyopaque, out: *EventQueue) anyerror!ProduceStatus { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + return self.produce(out); + } + + fn deinitVT(ptr: *anyopaque) void { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + self.deinit(); + } + + pub fn deinit(self: *ResumableResponse) void { + if (self.req_open) self.req.deinit(); + self.parser.deinit(); + self.state.deinit(); + self.allocator.destroy(self); + } + + /// Pump the response: read one chunk, feed it through the SSE parser, + /// and decode each SSE event into zero or more `Event`s appended to + /// `out`. Returns `.more` if the caller should pump again, or + /// `.response_complete` once the terminal (`[DONE]` or EOF) has been + /// reached and the assistant message has been committed + a final + /// `message_complete` pushed. + /// + /// Reading and finalizing here means a single `produce` call may push + /// several events; the `Stream` drains the queue before pumping again. + pub fn produce(self: *ResumableResponse, out: *EventQueue) !ProduceStatus { + if (self.done) return .response_complete; + + var vecs: [1][]u8 = .{&self.chunk}; + const n = self.body_reader.readVec(&vecs) catch |err| switch (err) { + // Stream ended without [DONE]. Some servers and proxies omit it + // (or drop the trailing usage chunk). Finalize with whatever + // we've got — usage will be null in that case, which is fine. + error.EndOfStream => { + try self.finishStream(out); + return .response_complete; + }, + // A transport read failure mid-stream (reset, TLS, timeout) + // before `[DONE]` means no assistant message was committed. + // Surface it as a retryable malformed-stream error. + else => return error.ProviderStreamMalformed, + }; + if (n == 0) return .more; + + const events = try self.parser.feed(self.chunk[0..n]); + defer self.parser.freeEvents(events); + + for (events) |ev_payload| { + std.log.debug("openai_chat <= {s}", .{ev_payload}); + if (std.mem.eql(u8, ev_payload, "[DONE]")) { + try self.finishStream(out); + return .response_complete; + } + try handleEvent(self.allocator, ev_payload, &self.state, out); + // Note: we do NOT bail when state.end_of_stream is set. + // OpenAI emits the terminating `usage` chunk *after* the + // chunk carrying finish_reason, then sends `[DONE]`. If + // we returned on finish_reason we'd never capture usage. + // `[DONE]` is the authoritative end-of-stream marker. + } + return .more; + } + + fn finishStream(self: *ResumableResponse, out: *EventQueue) !void { + if (self.done) return; + self.done = true; + try self.state.finalize(out, self.conv); + } +}; + +/// State maintained across the streaming response: which block is currently +/// being assembled, accumulated content, and the assistant message being +/// built up for the final `onMessageComplete` callback. +/// +/// We model the assistant message as a sequence of blocks, exactly one of +/// which is active at a time. Text/thinking transitions are inferred from +/// which field a delta carries. Tool_use blocks arrive as a per-call wire +/// `index`; the OpenAI Chat Completions streaming spec does not formally +/// promise that all fragments for a given index arrive contiguously, but in +/// practice every well-behaved backend (and the official Node SDK's own +/// reassembly logic) treats a delta for a new index as the implicit close +/// of the previous one. We do the same: seeing a delta for an index that +/// differs from `current_tool_index` closes the prior tool_use and opens a +/// new one. `finish_reason` closes the last still-open tool_use. A delta +/// arriving for an index that has already been closed is a degenerate +/// backend behavior (e.g. vLLM with speculative decoding under some +/// configurations) — we log an error and drop the fragment. +const StreamState = struct { + allocator: Allocator, + started: bool = false, + /// Set when the wire stream signals end-of-turn (finish_reason or [DONE]). + /// Tells the outer read loop to stop pulling more events. + end_of_stream: bool = false, + /// Set once `finalize` has run, to make it idempotent. + finalized: bool = false, + active: ActiveBlock = .none, + /// Block index reported to the receiver. Increments per block boundary. + block_index: usize = 0, + + /// Buffer for the currently-streaming text/thinking block. Owned by + /// this state until the block is completed, at which point ownership + /// transfers to the assembled Message. + current_buf: conversation.TextualBlock = .empty, + + /// Assembled blocks for the final message, in stream order. + blocks: std.ArrayList(conversation.ContentBlock) = .empty, + + /// The currently-streaming tool_use, if any. Closed when a delta for + /// a different wire index arrives, or at finalize. + active_tool: ?ToolUseInProgress = null, + /// Wire index of `active_tool` (when non-null). + current_tool_index: ?usize = null, + /// Wire indices that have already been closed. Used solely to detect + /// (and report) the degenerate case of a delta arriving for an index + /// whose block we've already emitted. + closed_tool_indices: std.AutoHashMap(usize, void), + + /// Token counts from the terminating chunk's `usage` block. Only + /// populated when the server sent `usage` (i.e. the request used + /// `stream_options.include_usage: true` AND the server honored it). + usage: ?provider_mod.Usage = null, + + /// Owned, human-readable description of a mid-stream error embedded in an + /// HTTP-200 SSE body, surfaced to the agent via `ProviderStream.lastError` + /// so the retry notice can explain *why*. + stream_error_message: ?[]u8 = null, + + const ToolUseInProgress = struct { + /// Block index emitted to the receiver for this tool call's + /// onBlockStart / onContentDelta / onBlockComplete callbacks. + block_index: usize, + /// id/name are buffered as TextualBlocks because lenient providers + /// (OpenRouter passthroughs, some self-hosted backends) may stream + /// either field as fragments across multiple deltas. OpenAI itself + /// sends them whole on the first delta, but the structural cost of + /// supporting fragments is small and worth the robustness. + id_buf: conversation.TextualBlock = .empty, + name_buf: conversation.TextualBlock = .empty, + arguments: conversation.TextualBlock = .empty, + /// Set once we've emitted `onBlockStart(.ToolUse, ...)` for this + /// block. We defer until either the first argument fragment + /// arrives or the block is closed — not for identity reasons + /// (identity is no longer passed at start) but to keep block + /// indices clean: a tool_call that turns out to lack id or name + /// is dropped silently rather than producing an empty + /// start/complete pair. + started: bool = false, + /// Set once we've emitted `onToolDetails` for this block. Fired + /// as soon as both id and name are non-empty, which may be on + /// the first delta (the common case) or partway through arg + /// deltas (fragmented-identity providers). + details_emitted: bool = false, + + fn deinit(self: *ToolUseInProgress, allocator: Allocator) void { + self.id_buf.deinit(allocator); + self.name_buf.deinit(allocator); + self.arguments.deinit(allocator); + } + }; + + fn init(allocator: Allocator) StreamState { + return .{ + .allocator = allocator, + .closed_tool_indices = std.AutoHashMap(usize, void).init(allocator), + }; + } + + fn deinit(self: *StreamState) void { + self.current_buf.deinit(self.allocator); + for (self.blocks.items) |*b| b.deinit(self.allocator); + self.blocks.deinit(self.allocator); + if (self.active_tool) |*tu| tu.deinit(self.allocator); + self.closed_tool_indices.deinit(); + if (self.stream_error_message) |s| self.allocator.free(s); + } + + /// Record a readable description of an embedded stream error, combining + /// the error `type` and `message` into one owned string (either may be + /// absent). Replaces any previous value. + fn setStreamErrorMessage(self: *StreamState, kind: ?[]const u8, message: ?[]const u8) !void { + if (self.stream_error_message) |old| self.allocator.free(old); + self.stream_error_message = try provider_mod.formatStreamError(self.allocator, kind, message); + } + + /// Close the active text/thinking block (if any) and emit + /// onBlockComplete. Ownership of `current_buf` transfers into the + /// appended block. + fn closeActive(self: *StreamState, out: *EventQueue) !void { + if (self.active == .none) return; + + const block: conversation.ContentBlock = switch (self.active) { + .text => .{ .Text = self.current_buf }, + .thinking => .{ .Thinking = .{ .text = self.current_buf } }, + .tool_use, .none => unreachable, + }; + self.current_buf = .empty; + + try self.blocks.append(self.allocator, block); + try out.push(.{ .block_complete = .{ + .index = self.block_index, + .block = self.blocks.items[self.blocks.items.len - 1], + } }); + + self.active = .none; + } + + /// Open a new text/thinking block, possibly closing a prior one. + fn openBlock( + self: *StreamState, + new_active: ActiveBlock, + out: *EventQueue, + ) !void { + std.debug.assert(new_active == .text or new_active == .thinking); + if (self.active == new_active) return; + if (self.active != .none) { + try self.closeActive(out); + self.block_index += 1; + } + self.active = new_active; + const block_type: provider_mod.ContentBlockType = switch (new_active) { + .text => .Text, + .thinking => .Thinking, + .tool_use, .none => unreachable, + }; + try out.push(.{ .block_start = .{ .block_type = block_type, .index = self.block_index } }); + } + + fn appendDelta( + self: *StreamState, + out: *EventQueue, + delta: []const u8, + ) !void { + try self.current_buf.appendSlice(self.allocator, delta); + // Dupe into the queue arena: the raw `delta` borrows the transient + // SSE payload that `produce` frees before `next()` reads the queue. + try out.push(.{ .content_delta = .{ + .index = self.block_index, + .delta = try out.dupeBytes(delta), + } }); + } + + /// Apply one streaming tool_call delta. Opens a new tool_use on the + /// first sight of a wire index, closing any prior tool_use (or active + /// text/thinking block) first. A delta for an already-closed index is + /// a malformed stream — we log and drop it. + fn applyToolCallDelta( + self: *StreamState, + out: *EventQueue, + d: json_mod.ToolCallDelta, + ) !void { + // Degenerate backend: a delta arrived for an index whose block we + // already finalized. Drop the fragment so we don't reopen a closed + // block, but log loudly enough to make this diagnosable. + if (self.closed_tool_indices.contains(d.index)) { + if (!@import("builtin").is_test) { + std.log.err( + "openai_chat: dropping tool_call delta for already-closed wire index {d} (non-contiguous tool_call stream); id={?s} name={?s} args={?s}", + .{ d.index, d.id, d.name, d.arguments }, + ); + } + return; + } + + // Wire-index change closes the previously-active tool_use. This is + // the only signal openai_chat gives us for mid-stream tool_use + // boundaries; see the StreamState doc-comment for the rationale. + if (self.current_tool_index) |cur| { + if (cur != d.index) try self.closeActiveTool(out); + } + + if (self.active_tool == null) { + // Opening a new tool_use. First close any open text/thinking + // block so the tool_use gets its own block_index. + if (self.active != .none) { + try self.closeActive(out); + self.block_index += 1; + } + self.active_tool = .{ .block_index = self.block_index }; + self.current_tool_index = d.index; + self.block_index += 1; + } + const tu = &self.active_tool.?; + + // Some OpenAI-compatible backends send identity as fragments + // (`call_` then `xyz`), while others resend the cumulative/full value + // on later argument chunks. Accept both without turning repeats into + // runaway ids like `call_xyzcall_xyz...`. + if (d.id) |s| try mergeStreamingField(self.allocator, &tu.id_buf, s); + if (d.name) |s| try mergeStreamingField(self.allocator, &tu.name_buf, s); + + // Defer `onBlockStart` until args begin. The first argument + // fragment is our signal that identity is likely settled enough + // to render. If the block closes before any args arrive (zero-arg + // tool), `closeActiveTool` emits the start there. + if (d.arguments) |a| { + try self.emitStartIfNeeded(out, tu); + // Fire `tool_details` as soon as both id and name are + // known. We can't know identity is *final* until the block + // closes (a later delta could append more bytes), but in + // practice OpenAI sends each whole on the first delta. A + // pathological backend that streams id/name across many + // chunks would have us emit a truncated value here. We + // accept that trade-off: consumers that need the canonical + // value can read it from the assembled ContentBlock at + // block_complete. + try self.emitDetailsIfReady(out, tu); + try tu.arguments.appendSlice(self.allocator, a); + // Dupe into the queue arena (the SSE payload is freed before + // `next()` reads the queue). + try out.push(.{ .content_delta = .{ + .index = tu.block_index, + .delta = try out.dupeBytes(a), + } }); + } else { + // Identity-only chunk (no args yet). Still try to emit + // details, in case both fields are now populated. + if (tu.started) try self.emitDetailsIfReady(out, tu); + } + } + + /// Fire `tool_details` once both id and name are non-empty. No-op if + /// already fired or if either field is still empty. Requires that + /// `block_start` has already been emitted. Slices are duped into the + /// queue arena because `id_buf`/`name_buf` may still grow (and realloc) + /// on later fragments. + fn emitDetailsIfReady( + self: *StreamState, + out: *EventQueue, + tu: *ToolUseInProgress, + ) !void { + _ = self; + if (tu.details_emitted) return; + if (!tu.started) return; + if (tu.id_buf.items.len == 0 or tu.name_buf.items.len == 0) return; + tu.details_emitted = true; + try out.push(.{ .tool_details = .{ + .index = tu.block_index, + .id = try out.dupeBytes(tu.id_buf.items), + .name = try out.dupeBytes(tu.name_buf.items), + } }); + } + + /// Close the currently-active tool_use (if any), emitting block_start + /// (if it wasn't already), block_complete, and recording the wire + /// index as closed. No-op if there's no active tool_use. + fn closeActiveTool(self: *StreamState, out: *EventQueue) !void { + var tu = self.active_tool orelse return; + self.active_tool = null; + const wire_index = self.current_tool_index.?; + self.current_tool_index = null; + try self.closed_tool_indices.put(wire_index, {}); + + // Drop entries lacking id or name. The stream closed the block + // before the provider sent enough to identify which tool was + // being called — there's nothing we can dispatch. + if (tu.id_buf.items.len == 0 or tu.name_buf.items.len == 0) { + if (!@import("builtin").is_test) { + std.log.err( + "openai_chat: dropping incomplete tool_use at wire index {d}: id={d} bytes, name=\"{s}\", args={d} bytes", + .{ + wire_index, + tu.id_buf.items.len, + tu.name_buf.items, + tu.arguments.items.len, + }, + ); + } + tu.deinit(self.allocator); + return; + } + + // The model echoes the wire-encoded tool name (`__` for `.`). + // Decode in place now that the full name is assembled, so the + // conversation, receiver callbacks, and dispatch all see the + // internal (dotted) name. Decoding never grows the buffer. + decodeNameInPlace(&tu.name_buf); + + // If no arguments ever arrived, we haven't emitted block_start + // yet — do it now so the consumer sees a balanced start/complete. + try self.emitStartIfNeeded(out, &tu); + // Last chance to fire details if a fragmented-identity provider + // only finished id/name accumulation at the very end. + try self.emitDetailsIfReady(out, &tu); + + const id_owned = try tu.id_buf.toOwnedSlice(self.allocator); + const name_owned = try tu.name_buf.toOwnedSlice(self.allocator); + const block: conversation.ContentBlock = .{ .ToolUse = .{ + .id = id_owned, + .name = name_owned, + .input = tu.arguments, + } }; + // Ownership has moved into `block`; clear the local before it + // goes out of scope so deinit doesn't double-free. + tu.arguments = .empty; + + try self.blocks.append(self.allocator, block); + try out.push(.{ .block_complete = .{ + .index = tu.block_index, + .block = self.blocks.items[self.blocks.items.len - 1], + } }); + } + + /// Emit `block_start(.ToolUse, ...)` once per in-progress tool use. + /// Callers must invoke this before the first `content_delta` or + /// `block_complete` for the block. Identity (id/name) is *not* passed at + /// start — consumers get identity from `tool_details` or the assembled + /// ContentBlock at block_complete time. + fn emitStartIfNeeded( + self: *StreamState, + out: *EventQueue, + tu: *ToolUseInProgress, + ) !void { + _ = self; + if (tu.started) return; + tu.started = true; + try out.push(.{ .block_start = .{ .block_type = .ToolUse, .index = tu.block_index } }); + } + + fn mergeStreamingField( + allocator: Allocator, + buf: *conversation.TextualBlock, + piece: []const u8, + ) !void { + if (piece.len == 0) return; + if (buf.items.len == 0) { + try buf.appendSlice(allocator, piece); + } else if (std.mem.eql(u8, buf.items, piece)) { + return; + } else if (std.mem.startsWith(u8, piece, buf.items)) { + buf.clearRetainingCapacity(); + try buf.appendSlice(allocator, piece); + } else { + try buf.appendSlice(allocator, piece); + } + } + + /// End the stream: close any open text/thinking block, close the still- + /// active tool_use (if any), then commit the assembled assistant + /// Message to the conversation and push the terminal `message_complete`. + fn finalize( + self: *StreamState, + out: *EventQueue, + conv: *conversation.Conversation, + ) !void { + if (self.finalized) return; + self.finalized = true; + + try self.closeActive(out); + try self.closeActiveTool(out); + + // Move blocks into a fresh conversation message. + const moved_blocks = try self.blocks.toOwnedSlice(self.allocator); + defer self.allocator.free(moved_blocks); + + try conv.addAssistantMessage(moved_blocks, self.usage); + + const msg = conv.messages.items[conv.messages.items.len - 1]; + try out.push(.{ .message_complete = .{ .message = msg, .usage = self.usage } }); + } +}; + +fn handleEvent( + allocator: Allocator, + payload: []const u8, + state: *StreamState, + out: *EventQueue, +) !void { + var parsed = try json_mod.parseStreamEvent(allocator, payload); + defer parsed.deinit(); + const d = parsed.delta; + + // Usage block arrives in the terminating chunk (after finish_reason, + // with an empty `choices` array). Capture it; `finalize` delivers it + // as part of `onMessageComplete`. OpenAI bills `prompt_tokens` as + // the *total* input including cached tokens; we split them so + // callers don't have to. + if (d.usage) |u| { + const prompt: u64 = u.prompt_tokens orelse 0; + const cached: u64 = u.cached_prompt_tokens orelse 0; + const fresh: u64 = if (cached > prompt) 0 else prompt - cached; + state.usage = .{ + .input = fresh, + .output = u.completion_tokens orelse 0, + .cache_read = cached, + .cache_write = 0, // OpenAI doesn't bill a cache-write premium. + .reasoning = u.reasoning_tokens orelse 0, + }; + } + + // Mid-stream provider error: some OpenAI-compatible endpoints (and + // OpenAI itself on rare transient failures) return HTTP 200 with an + // error embedded in the SSE stream. Treat the turn as failed. + if (d.error_message != null or d.error_type != null) { + if (!@import("builtin").is_test) { + std.log.err("openai_chat stream error: {?s}: {?s}", .{ + d.error_type, d.error_message, + }); + } + // Stash a readable description so the agent's retry notice can + // explain *why* the stream failed. Owned by `state`. + state.setStreamErrorMessage(d.error_type, d.error_message) catch {}; + return error.ProviderStreamMalformed; + } + + if (!state.started and d.role != null) { + state.started = true; + try out.push(.{ .message_start = .assistant }); + } + + if (d.reasoning_content) |rc| if (rc.len > 0) { + if (!state.started) { + state.started = true; + try out.push(.{ .message_start = .assistant }); + } + try state.openBlock(.thinking, out); + try state.appendDelta(out, rc); + }; + + if (d.content) |c| if (c.len > 0) { + if (!state.started) { + state.started = true; + try out.push(.{ .message_start = .assistant }); + } + try state.openBlock(.text, out); + try state.appendDelta(out, c); + }; + + if (d.tool_calls.len > 0) { + if (!state.started) { + state.started = true; + try out.push(.{ .message_start = .assistant }); + } + for (d.tool_calls) |tc| try state.applyToolCallDelta(out, tc); + } + + if (d.finish_reason) |_| { + state.end_of_stream = true; + } +} + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const testing = std.testing; + +/// Feed a sequence of SSE event payloads through the state machine as if +/// they had been delivered by the wire, finalizing into `conv`. The decoded +/// `Event`s are recorded as compact strings (the same schema the old +/// RecordingReceiver used) so callback-ordering assertions are preserved. +fn runStreamedTurn( + allocator: Allocator, + conv: *conversation.Conversation, + rec: ?*EventRecorder, + events: []const []const u8, +) !void { + var state: StreamState = .init(allocator); + defer state.deinit(); + + var queue = EventQueue.init(allocator); + defer queue.deinit(); + + for (events) |payload| { + if (std.mem.eql(u8, payload, "[DONE]")) break; + // Process every chunk through to [DONE], including the + // post-finish_reason usage chunk. Mirrors the production pump in + // ResumableResponse.produce. + try handleEvent(allocator, payload, &state, &queue); + } + try state.finalize(&queue, conv); + + // Drain into the recorder before the arena resets. The queue holds all + // events from this turn; popping records each, and the final null-pop + // resets the arena. + while (queue.pop()) |ev| { + if (rec) |r| try r.record(ev); + } +} + +/// Records decoded `Event`s as compact strings for ordering assertions. +const EventRecorder = struct { + allocator: Allocator, + events: std.ArrayList([]const u8) = .empty, + + fn deinit(self: *EventRecorder) void { + for (self.events.items) |e| self.allocator.free(e); + self.events.deinit(self.allocator); + } + + fn push(self: *EventRecorder, comptime fmt: []const u8, args: anytype) !void { + const owned = try std.fmt.allocPrint(self.allocator, fmt, args); + try self.events.append(self.allocator, owned); + } + + fn record(self: *EventRecorder, ev: Event) !void { + switch (ev) { + .message_start => try self.push("msg_start", .{}), + .block_start => |b| try self.push("block_start[{d}]:{s}", .{ b.index, @tagName(b.block_type) }), + .tool_details => |t| try self.push("tool_details[{d}]:{s}:{s}", .{ t.index, t.id, t.name }), + .content_delta => |d| try self.push("delta[{d}]:{s}", .{ d.index, d.delta }), + .block_complete => |b| try self.push("block_complete[{d}]", .{b.index}), + .message_complete => |m| { + if (m.usage) |u| { + try self.push( + "msg_complete[usage:in={d},out={d},cr={d},cw={d},rsn={d}]", + .{ u.input, u.output, u.cache_read, u.cache_write, u.reasoning }, + ); + } else { + try self.push("msg_complete[usage:null]", .{}); + } + }, + else => {}, + } + } +}; + +/// Test helper: append a single-text user message. `addUserMessage` now +/// takes a block slice (symmetric with `addAssistantMessage`); this wraps +/// the common plain-text case the tests below use. +fn addUserText(conv: *conversation.Conversation, text: []const u8) !void { + const tb = try conversation.textualBlockFromSlice(conv.allocator, text); + var block: conversation.ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + +test "two streamed turns persist assistant replies in the conversation" { + // Regression test for the bug where `finish_reason` arrived before + // `[DONE]` and `finalize` early-returned without appending the assistant + // message, so follow-up turns were sent without prior responses. + + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + + try conv.addSystemMessage("You are a helpful assistant."); + try addUserText(&conv, "hello!"); + + const turn1 = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"content":"Hello! "}}]} + , + \\{"choices":[{"delta":{"content":"How can I help you today?"}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"stop"}]} + , + "[DONE]", + }; + try runStreamedTurn(allocator, &conv, null, &turn1); + + try testing.expectEqual(@as(usize, 3), conv.messages.items.len); + try testing.expectEqual(conversation.MessageRole.assistant, conv.messages.items[2].role); + try testing.expectEqualStrings( + "Hello! How can I help you today?", + conv.messages.items[2].content.items[0].Text.items, + ); + + // Second user turn: the assistant must still see its prior response. + try addUserText(&conv, "how did you respond to my greeting just now?"); + + const turn2 = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"content":"I replied: \"Hello! How can I help you today?\""}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"stop"}]} + , + "[DONE]", + }; + try runStreamedTurn(allocator, &conv, null, &turn2); + + // System + user + assistant + user + assistant = 5 messages. + try testing.expectEqual(@as(usize, 5), conv.messages.items.len); + try testing.expectEqual(conversation.MessageRole.assistant, conv.messages.items[4].role); + try testing.expectEqualStrings( + "I replied: \"Hello! How can I help you today?\"", + conv.messages.items[4].content.items[0].Text.items, + ); +} + +test "openai_chat: terminating usage chunk lands on message_complete with split cache_read" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var rec = EventRecorder{ .allocator = allocator }; + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"content":"hi"}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"stop"}]} + , + // OpenAI's terminating chunk: empty choices, top-level usage. + \\{"choices":[],"usage":{"prompt_tokens":150,"completion_tokens":42,"prompt_tokens_details":{"cached_tokens":120},"completion_tokens_details":{"reasoning_tokens":18}}} + , + "[DONE]", + }; + try runStreamedTurn(allocator, &conv, &rec, &events); + + var found: ?[]const u8 = null; + for (rec.events.items) |s| { + if (std.mem.startsWith(u8, s, "msg_complete[")) found = s; + } + try testing.expect(found != null); + // 150 prompt - 120 cached = 30 fresh input. + try testing.expectEqualStrings("msg_complete[usage:in=30,out=42,cr=120,cw=0,rsn=18]", found.?); +} + +test "openai_chat: omitted stream usage yields null on message_complete" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var rec = EventRecorder{ .allocator = allocator }; + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"content":"hi"}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"stop"}]} + , + "[DONE]", + }; + try runStreamedTurn(allocator, &conv, &rec, &events); + + var found: ?[]const u8 = null; + for (rec.events.items) |s| { + if (std.mem.startsWith(u8, s, "msg_complete")) found = s; + } + try testing.expect(found != null); + try testing.expectEqualStrings("msg_complete[usage:null]", found.?); +} + +test "openai_chat: empty content alongside reasoning does not split thinking block" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "think"); + + var rec = EventRecorder{ .allocator = allocator }; + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"reasoning_content":"rea","content":""}}]} + , + \\{"choices":[{"delta":{"reasoning_content":"son","content":""}}]} + , + \\{"choices":[{"delta":{"reasoning_content":"ing","content":""}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"stop"}]} + , + "[DONE]", + }; + try runStreamedTurn(allocator, &conv, &rec, &events); + + const expected = [_][]const u8{ + "msg_start", + "block_start[0]:Thinking", + "delta[0]:rea", + "delta[0]:son", + "delta[0]:ing", + "block_complete[0]", + "msg_complete[usage:null]", + }; + try testing.expectEqual(expected.len, rec.events.items.len); + for (expected, rec.events.items) |want, got| { + try testing.expectEqualStrings(want, got); + } + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 1), asst.content.items.len); + try testing.expectEqualStrings("reasoning", asst.content.items[0].Thinking.text.items); +} + +test "fragmented tool_call id and name are reassembled" { + // Lenient OpenAI-compatible providers occasionally split `id` and + // `function.name` across multiple deltas instead of sending them whole + // on the first chunk. Verify the state machine appends both correctly + // and emits a complete identity to the receiver. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call something"); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_","type":"function","function":{"name":"pi"}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"xyz","function":{"name":"ng"}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"host\":\"a.com\"}"}}]}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]} + , + "[DONE]", + }; + + try runStreamedTurn(allocator, &conv, null, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 1), asst.content.items.len); + const tu = asst.content.items[0].ToolUse; + try testing.expectEqualStrings("call_xyz", tu.id); + try testing.expectEqualStrings("ping", tu.name); + try testing.expectEqualStrings("{\"host\":\"a.com\"}", tu.input.items); +} + +test "repeated full tool_call id and name do not accumulate" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "read it"); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"chatcmpl-tool-abc","type":"function","function":{"name":"std__read","arguments":"{\"path\""}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"chatcmpl-tool-abc","function":{"name":"std__read","arguments":":\"a\"}"}}]}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]} + , + "[DONE]", + }; + + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("chatcmpl-tool-abc", tu.id); + try testing.expectEqualStrings("std.read", tu.name); + try testing.expectEqualStrings("{\"path\":\"a\"}", tu.input.items); +} + +test "cumulative tool_call id and name replace their prefix" { + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "read it"); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"chatcmpl-tool-","type":"function","function":{"name":"std_","arguments":"{\"path\""}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"chatcmpl-tool-abc","function":{"name":"std__read","arguments":":\"a\"}"}}]}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]} + , + "[DONE]", + }; + + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("chatcmpl-tool-abc", tu.id); + try testing.expectEqualStrings("std.read", tu.name); + try testing.expectEqualStrings("{\"path\":\"a\"}", tu.input.items); +} + +test "inbound wire tool name is decoded to dotted form (even split across __)" { + // The model echoes the wire name it was given (`std__read`). It is + // decoded to the internal `std.read` for the conversation/session/ + // dispatch. The decode happens after full assembly, so a `__` split + // across two deltas (`std_` + `_read`) decodes correctly. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "read a file"); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c1","type":"function","function":{"name":"std_"}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"name":"_read"}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{}"}}]}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]} + , + "[DONE]", + }; + + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("std.read", tu.name); +} + +test "parallel tool_calls emit one complete start/delta/complete cycle per block" { + // Regression test: previously, the OpenAI provider deferred ALL + // tool_use onBlockComplete callbacks to finalize, so a four-tool + // parallel batch produced start/start/start/start/delta*/complete/ + // complete/complete/complete — the receiver couldn't render each tool + // as its own discrete block. With the new contiguity-driven close-on- + // next-index logic, each tool_use should produce a contiguous + // start → delta(s) → complete trio. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "ping four hosts"); + + var rec: EventRecorder = .{ .allocator = allocator }; + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c0","type":"function","function":{"name":"ping","arguments":"{\"host\":\"a\"}"}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":1,"id":"c1","type":"function","function":{"name":"ping","arguments":"{\"host\":\"b\"}"}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":2,"id":"c2","type":"function","function":{"name":"ping","arguments":"{\"host\":\"c\"}"}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":3,"id":"c3","type":"function","function":{"name":"ping","arguments":"{\"host\":\"d\"}"}}]}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]} + , + "[DONE]", + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + const expected = [_][]const u8{ + "msg_start", + "block_start[0]:ToolUse", + "tool_details[0]:c0:ping", + "delta[0]:{\"host\":\"a\"}", + "block_complete[0]", + "block_start[1]:ToolUse", + "tool_details[1]:c1:ping", + "delta[1]:{\"host\":\"b\"}", + "block_complete[1]", + "block_start[2]:ToolUse", + "tool_details[2]:c2:ping", + "delta[2]:{\"host\":\"c\"}", + "block_complete[2]", + "block_start[3]:ToolUse", + "tool_details[3]:c3:ping", + "delta[3]:{\"host\":\"d\"}", + "block_complete[3]", + // No usage chunk in this fixture (older test data) — record + // shows null. + "msg_complete[usage:null]", + }; + + // Identity arrives in the assembled ContentBlock at completion time. + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 4), asst.content.items.len); + for (asst.content.items) |b| { + try testing.expectEqualStrings("ping", b.ToolUse.name); + } + try testing.expectEqual(expected.len, rec.events.items.len); + for (expected, rec.events.items) |want, got| { + try testing.expectEqualStrings(want, got); + } +} + +test "non-contiguous tool_call deltas: re-emission of a closed index is dropped" { + // Degenerate backend behavior: a delta for an already-closed wire + // index. We must not reopen the block; instead drop the fragment and + // log. The successfully-closed prior blocks remain intact. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "go"); + + var rec: EventRecorder = .{ .allocator = allocator }; + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c0","type":"function","function":{"name":"ping","arguments":"{\"x\":1}"}}]}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":1,"id":"c1","type":"function","function":{"name":"ping","arguments":"{\"y\":2}"}}]}}]} + , + // Delta for already-closed index 0: must be dropped. + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":",extra"}}]}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]} + , + "[DONE]", + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + // Two well-formed tool_use blocks in the final message, args unaffected + // by the dropped fragment. + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 2), asst.content.items.len); + try testing.expectEqualStrings("{\"x\":1}", asst.content.items[0].ToolUse.input.items); + try testing.expectEqualStrings("{\"y\":2}", asst.content.items[1].ToolUse.input.items); + + // Callback sequence: index 0 closed cleanly before any stray delta. + // There must be exactly one block_complete[0] in the event log + // (i.e. the stray delta did not produce a second open/close cycle). + var n_complete_0: usize = 0; + for (rec.events.items) |e| { + if (std.mem.eql(u8, e, "block_complete[0]")) n_complete_0 += 1; + } + try testing.expectEqual(@as(usize, 1), n_complete_0); +} + +test "onToolDetails fires after id+name complete, even mid-arg-stream" { + // Fragmented-identity provider: id arrives split across two chunks, + // and an arg fragment appears between them. `onToolDetails` must + // wait until both id and name are non-empty (i.e. on the chunk that + // completes id), and must fire exactly once, before block_complete. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "go"); + + var rec: EventRecorder = .{ .allocator = allocator }; + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + // Identity-only chunk: name arrives whole, id starts. No args yet, + // so onBlockStart hasn't fired and details can't either. + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_","type":"function","function":{"name":"ping"}}]}}]} + , + // First arg chunk: onBlockStart fires. id is still "call_" — not + // empty — and name is non-empty, so onToolDetails fires here + // with whatever id we have so far (`call_`). + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"x\":"}}]}}]} + , + // Mid-stream id completion + second arg chunk. onToolDetails has + // already fired so it does NOT fire again, even though id grew. + // The final ContentBlock will carry the full "call_xyz" id. + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"xyz","function":{"arguments":"1}"}}]}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]} + , + "[DONE]", + }; + + try runStreamedTurn(allocator, &conv, &rec, &events); + + // Exactly one tool_details event, fired with the id-prefix that was + // current at first-args-arrival, and ordered between block_start and + // block_complete. + var n_details: usize = 0; + var details_pos: ?usize = null; + var block_start_pos: ?usize = null; + var block_complete_pos: ?usize = null; + for (rec.events.items, 0..) |e, i| { + if (std.mem.startsWith(u8, e, "tool_details[")) { + n_details += 1; + details_pos = i; + try testing.expectEqualStrings("tool_details[0]:call_:ping", e); + } else if (std.mem.eql(u8, e, "block_start[0]:ToolUse")) { + block_start_pos = i; + } else if (std.mem.eql(u8, e, "block_complete[0]")) { + block_complete_pos = i; + } + } + try testing.expectEqual(@as(usize, 1), n_details); + try testing.expect(block_start_pos.? < details_pos.?); + try testing.expect(details_pos.? < block_complete_pos.?); + + // Final ContentBlock has the full id assembled from both fragments. + const asst = conv.messages.items[1]; + try testing.expectEqualStrings("call_xyz", asst.content.items[0].ToolUse.id); + try testing.expectEqualStrings("ping", asst.content.items[0].ToolUse.name); + try testing.expectEqualStrings("{\"x\":1}", asst.content.items[0].ToolUse.input.items); +} + +test "tool_call with no arguments still finalizes a well-formed ToolUse" { + // Some providers may emit a tool call with no arguments at all (e.g. a + // zero-arg tool). The state machine should still emit onBlockStart + // exactly once at finalize time and produce a ToolUse with empty input. + const allocator = testing.allocator; + + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "ring it"); + + const events = [_][]const u8{ + \\{"choices":[{"delta":{"role":"assistant"}}]} + , + \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c1","type":"function","function":{"name":"ring"}}]}}]} + , + \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]} + , + "[DONE]", + }; + + try runStreamedTurn(allocator, &conv, null, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 1), asst.content.items.len); + const tu = asst.content.items[0].ToolUse; + try testing.expectEqualStrings("c1", tu.id); + try testing.expectEqualStrings("ring", tu.name); + try testing.expectEqual(@as(usize, 0), tu.input.items.len); +} diff --git a/src/provider_openai_responses.zig b/src/provider_openai_responses.zig new file mode 100644 index 0000000..d764e6f --- /dev/null +++ b/src/provider_openai_responses.zig @@ -0,0 +1,1139 @@ +//! OpenAI Responses API streaming provider (ChatGPT-subscription Codex). +//! +//! Mirrors `provider_openai_chat.zig` in shape — a transient request object +//! that opens the HTTP stream and a heap-pinned `ResumableResponse` that pumps +//! SSE bytes into `Event`s — but speaks the Responses streaming protocol +//! (typed `response.*` events) instead of Chat Completions `choices[].delta`. +//! +//! Event → block mapping: +//! - `response.output_text.delta` → Text block deltas +//! - `response.reasoning_summary_text.delta` → Thinking block deltas +//! - `response.output_item.added` (function_call) → opens a ToolUse block +//! - `response.function_call_arguments.delta` → ToolUse input deltas +//! - `response.output_item.done` (function_call) → closes the ToolUse +//! - `response.completed` → usage + finalize +//! - `error` / `response.failed` → malformed-stream error +//! +//! NOTE: the Responses-backed Codex path could not be verified against live +//! ChatGPT-subscription credentials; the request/stream shapes follow the +//! OpenAI Responses API docs and the open-source Codex client. Fixture tests +//! exercise the state machine; live verification is still required. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Io = std.Io; +const http = std.http; +const Uri = std.Uri; + +const conversation = @import("conversation.zig"); +const provider_mod = @import("provider.zig"); +const stream_mod = @import("stream.zig"); +const sse_mod = @import("sse.zig"); +const json_mod = @import("openai_responses_json.zig"); +const config_mod = @import("config.zig"); + +const Event = stream_mod.Event; +const EventQueue = stream_mod.EventQueue; + +pub const OpenAIResponsesDialect = enum { + public, + codex, +}; + +const decodeNameInPlace = provider_mod.decodeNameInPlace; + +pub const OpenAIResponsesRequest = struct { + allocator: Allocator, + io: Io, + config: *const config_mod.OpenAIResponsesConfig, + dialect: OpenAIResponsesDialect = .public, + http_client: *http.Client, + diag: ?*provider_mod.ProviderDiagnostic = null, + + pub fn open( + self: *OpenAIResponsesRequest, + conv: *conversation.Conversation, + tools: *const provider_mod.ToolRegistry, + ) !*ResumableResponse { + const rr = try self.allocator.create(ResumableResponse); + errdefer self.allocator.destroy(rr); + rr.* = .{ + .allocator = self.allocator, + .conv = conv, + .parser = sse_mod.SSEParser.init(self.allocator), + .state = .init(self.allocator), + }; + rr.state.signature_origin = try conversation.SignatureOrigin.init( + self.allocator, + if (self.dialect == .codex) .openai_codex_responses else .openai_responses, + self.config.base_url, + self.config.model, + ); + errdefer { + rr.parser.deinit(); + rr.state.deinit(); + } + + const url = try responsesURL(self.allocator, self.config.base_url); + defer self.allocator.free(url); + const uri = try Uri.parse(url); + + const body = try json_mod.serializeRequest(self.allocator, self.config, conv, tools, switch (self.dialect) { + .public => .public, + .codex => .codex, + }); + defer self.allocator.free(body); + std.log.debug("openai_responses => {s}", .{body}); + + const auth_value = try std.fmt.allocPrint(self.allocator, "Bearer {s}", .{self.config.api_key}); + defer self.allocator.free(auth_value); + + const base_headers = [_]http.Header{ + .{ .name = "content-type", .value = "application/json" }, + .{ .name = "accept", .value = "text/event-stream" }, + .{ .name = "authorization", .value = auth_value }, + }; + const extra_headers = try provider_mod.mergeHeaders( + self.allocator, + &base_headers, + self.config.extra_headers, + ); + defer self.allocator.free(extra_headers); + + rr.response = try provider_mod.sendRequest(self.http_client, uri, extra_headers, body, &rr.req); + rr.req_open = true; + errdefer { + rr.req.deinit(); + rr.req_open = false; + } + + if (@intFromEnum(rr.response.head.status) >= 400) { + return provider_mod.classifyErrorResponse(self.allocator, &rr.response, &rr.transfer_buf, self.diag, "openai_responses"); + } + + rr.body_reader = rr.response.reader(&rr.transfer_buf); + return rr; + } +}; + +// Appends `/responses` to `base_url` (with trailing slashes trimmed). The +// caller is responsible for any path segment preceding `/responses` — for +// the Codex ChatGPT-subscription endpoint, that means putting `/codex` +// (or any other prefix) in `base_url` directly. No `endsWith` guessing: the +// contract is the same regardless of `dialect`. +fn responsesURL(allocator: Allocator, base_url: []const u8) ![]u8 { + const trimmed = std.mem.trim(u8, base_url, "/"); + return std.fmt.allocPrint(allocator, "{s}/responses", .{trimmed}); +} + +pub const ResumableResponse = struct { + allocator: Allocator, + conv: *conversation.Conversation, + parser: sse_mod.SSEParser, + state: StreamState, + + req: http.Client.Request = undefined, + response: http.Client.Response = undefined, + transfer_buf: [4096]u8 = undefined, + body_reader: *std.Io.Reader = undefined, + chunk: [4096]u8 = undefined, + + req_open: bool = false, + done: bool = false, + + pub const ProduceStatus = provider_mod.ProviderStream.ProduceStatus; + + pub fn providerStream(self: *ResumableResponse) provider_mod.ProviderStream { + return .{ .ptr = self, .vtable = &vtable }; + } + + const vtable: provider_mod.ProviderStream.VTable = .{ + .produce = produceVT, + .deinit = deinitVT, + .last_error = lastErrorVT, + }; + + fn lastErrorVT(ptr: *anyopaque) ?[]const u8 { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + return self.state.stream_error_message; + } + fn produceVT(ptr: *anyopaque, out: *EventQueue) anyerror!ProduceStatus { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + return self.produce(out); + } + fn deinitVT(ptr: *anyopaque) void { + const self: *ResumableResponse = @ptrCast(@alignCast(ptr)); + self.deinit(); + } + + pub fn deinit(self: *ResumableResponse) void { + if (self.req_open) self.req.deinit(); + self.parser.deinit(); + self.state.deinit(); + self.allocator.destroy(self); + } + + pub fn produce(self: *ResumableResponse, out: *EventQueue) !ProduceStatus { + if (self.done) return .response_complete; + + var vecs: [1][]u8 = .{&self.chunk}; + const n = self.body_reader.readVec(&vecs) catch |err| switch (err) { + error.EndOfStream => { + try self.finishStream(out); + return .response_complete; + }, + else => return error.ProviderStreamMalformed, + }; + if (n == 0) return .more; + + const events = try self.parser.feed(self.chunk[0..n]); + defer self.parser.freeEvents(events); + + for (events) |ev_payload| { + std.log.debug("openai_responses <= {s}", .{ev_payload}); + // The Responses stream has no `[DONE]` sentinel; `response.completed` + // is the terminal event. + if (std.mem.eql(u8, ev_payload, "[DONE]")) { + try self.finishStream(out); + return .response_complete; + } + const terminal = try handleEvent(self.allocator, ev_payload, &self.state, out); + if (terminal) { + try self.finishStream(out); + return .response_complete; + } + } + return .more; + } + + fn finishStream(self: *ResumableResponse, out: *EventQueue) !void { + if (self.done) return; + self.done = true; + try self.state.finalize(out, self.conv); + } +}; + +const ActiveBlock = enum { none, text, thinking }; + +/// Streaming-response assembly state. +/// +/// Modeled on `provider_openai_chat.zig`'s `StreamState`: one active +/// text/thinking block at a time, plus a set of in-progress tool calls. The +/// Responses protocol is friendlier than Chat Completions here — every +/// function-call event carries an explicit `item_id` (and `output_index`), +/// and the lifecycle is spelled out by `output_item.added` → +/// `function_call_arguments.delta`* → `function_call_arguments.done`/ +/// `output_item.done` → `response.completed`. So tool calls are keyed by +/// `item_id` (the stable identity) and need none of Chat Completions' +/// contiguity inference. +/// +/// Argument-accumulation rule (the crux of the tool-input correctness): the +/// concatenation of `function_call_arguments.delta` fragments is the reliable +/// source of the tool input. A terminal `function_call_arguments.done` / +/// `output_item.done` / `response.completed` event also restates the full +/// `arguments`, but we apply it only as a *non-empty* override: a restated +/// value can improve the accumulation (e.g. if it is more complete) but never +/// wipe it. This matters because these events are observed to restate +/// `arguments` as `""` once the value has already streamed via deltas, and an +/// unconditional overwrite there destroys the real input — the original +/// empty-tool-input bug. Empty arguments normalize to `"{}"` so a tool never +/// receives an unparseable empty string. +const StreamState = struct { + allocator: Allocator, + started: bool = false, + finalized: bool = false, + active: ActiveBlock = .none, + block_index: usize = 0, + current_buf: conversation.TextualBlock = .empty, + current_thinking_signature: ?[]const u8 = null, + signature_origin: ?conversation.SignatureOrigin = null, + assistant_phase: ?AssistantPhase = null, + blocks: std.ArrayList(conversation.ContentBlock) = .empty, + /// In-progress and completed tool calls, keyed by `output_index`, in + /// first-seen order. Completed calls are retained (flagged `closed`) so + /// the terminal `response.completed`, which restates every output item, + /// does not re-emit a call that `output_item.done` already closed. + /// + /// We key by `output_index`, NOT the per-item `item_id`. The OpenAI spec + /// promises a stable `item.id` per output item, but the GitHub Copilot + /// Responses proxy emits a *fresh, opaque `item.id` on every event* for + /// the same call (`output_item.added`, each `…arguments.delta`, + /// `output_item.done`, and the `response.completed` restatement all + /// differ). Keying by `item_id` there made dedup miss, so one call + /// fanned out into three identical ToolUse blocks sharing one `call_id`. + /// `output_index` is stable across all of a call's events on both + /// backends — and is what `provider_openai_chat` keys on, too. + tools: std.AutoArrayHashMapUnmanaged(usize, ToolUseInProgress) = .empty, + usage: ?provider_mod.Usage = null, + stream_error_message: ?[]u8 = null, + + const ToolUseInProgress = struct { + block_index: usize, + /// Set once `block_complete` has been emitted for this call. + closed: bool = false, + id_buf: conversation.TextualBlock = .empty, // call_id + name_buf: conversation.TextualBlock = .empty, + arguments: conversation.TextualBlock = .empty, + + fn deinit(self: *ToolUseInProgress, allocator: Allocator) void { + self.id_buf.deinit(allocator); + self.name_buf.deinit(allocator); + self.arguments.deinit(allocator); + } + }; + + fn init(allocator: Allocator) StreamState { + return .{ .allocator = allocator }; + } + + fn deinit(self: *StreamState) void { + self.current_buf.deinit(self.allocator); + if (self.current_thinking_signature) |s| self.allocator.free(s); + if (self.signature_origin) |*o| o.deinit(self.allocator); + for (self.blocks.items) |*b| b.deinit(self.allocator); + self.blocks.deinit(self.allocator); + var it = self.tools.iterator(); + while (it.next()) |e| { + e.value_ptr.deinit(self.allocator); + } + self.tools.deinit(self.allocator); + if (self.stream_error_message) |s| self.allocator.free(s); + } + + fn setStreamErrorMessage(self: *StreamState, message: []const u8) void { + if (self.stream_error_message) |old| self.allocator.free(old); + self.stream_error_message = self.allocator.dupe(u8, message) catch null; + } + + fn ensureStarted(self: *StreamState, out: *EventQueue) !void { + if (self.started) return; + self.started = true; + try out.push(.{ .message_start = .assistant }); + } + + fn closeActive(self: *StreamState, out: *EventQueue) !void { + if (self.active == .none) return; + const block: conversation.ContentBlock = switch (self.active) { + .text => .{ .Text = self.current_buf }, + .thinking => .{ .Thinking = .{ .text = self.current_buf, .signature = self.current_thinking_signature } }, + .none => unreachable, + }; + self.current_buf = .empty; + self.current_thinking_signature = null; + try self.blocks.append(self.allocator, block); + try out.push(.{ .block_complete = .{ + .index = self.block_index, + .block = self.blocks.items[self.blocks.items.len - 1], + } }); + self.active = .none; + } + + fn setThinkingSignature(self: *StreamState, signature: []const u8) !void { + if (self.current_thinking_signature) |old| self.allocator.free(old); + self.current_thinking_signature = try self.allocator.dupe(u8, signature); + } + + fn setAssistantPhase(self: *StreamState, phase: []const u8) void { + if (std.mem.eql(u8, phase, "commentary")) { + self.assistant_phase = .commentary; + } else if (std.mem.eql(u8, phase, "final_answer")) { + self.assistant_phase = .final_answer; + } + } + + fn openBlock(self: *StreamState, new_active: ActiveBlock, out: *EventQueue) !void { + std.debug.assert(new_active == .text or new_active == .thinking); + if (self.active == new_active) return; + if (self.active != .none) { + try self.closeActive(out); + self.block_index += 1; + } + self.active = new_active; + const block_type: provider_mod.ContentBlockType = switch (new_active) { + .text => .Text, + .thinking => .Thinking, + .none => unreachable, + }; + try out.push(.{ .block_start = .{ .block_type = block_type, .index = self.block_index } }); + } + + fn appendDelta(self: *StreamState, out: *EventQueue, delta: []const u8) !void { + try self.current_buf.appendSlice(self.allocator, delta); + try out.push(.{ .content_delta = .{ + .index = self.block_index, + .delta = try out.dupeBytes(delta), + } }); + } + + /// Normalize a final/seed arguments string: an empty value becomes `"{}"` + /// so a tool never sees an unparseable empty input. + fn normalizedArguments(arguments: []const u8) []const u8 { + return if (arguments.len == 0) "{}" else arguments; + } + + /// Resolve an in-progress (not-yet-closed) tool call by `output_index`. + /// Returns null for an unknown index or an already-closed call. + fn lookupTool(self: *StreamState, output_index: ?usize) ?*ToolUseInProgress { + const idx = output_index orelse return null; + const tu = self.tools.getPtr(idx) orelse return null; + if (tu.closed) return null; + return tu; + } + + /// Open a ToolUse block for a `function_call` output item. No-op if a + /// call at this `output_index` already exists (open or closed): the call + /// is announced once by `output_item.added`, then restated by + /// `output_item.done` and again by `response.completed` — and on the + /// Copilot proxy each restatement carries a different `item_id`, so + /// `output_index` is the only reliable dedup key. + fn openToolUse( + self: *StreamState, + out: *EventQueue, + output_index: ?usize, + call_id: ?[]const u8, + name: ?[]const u8, + ) !void { + const idx = output_index orelse return; + if (self.tools.contains(idx)) return; + // Close any active text/thinking block so the tool gets its own index. + if (self.active != .none) { + try self.closeActive(out); + self.block_index += 1; + } + var tu: ToolUseInProgress = .{ .block_index = self.block_index }; + errdefer tu.deinit(self.allocator); + if (call_id) |c| try tu.id_buf.appendSlice(self.allocator, c); + if (name) |nm| try tu.name_buf.appendSlice(self.allocator, nm); + try self.tools.put(self.allocator, idx, tu); + + try out.push(.{ .block_start = .{ .block_type = .ToolUse, .index = self.block_index } }); + if (call_id != null and name != null) { + try out.push(.{ .tool_details = .{ + .index = self.block_index, + .id = try out.dupeBytes(call_id.?), + .name = try out.dupeBytes(name.?), + } }); + } + self.block_index += 1; + } + + /// Append a streaming argument fragment to the matching call. Fragments + /// for an unknown or already-closed call are dropped (the protocol always + /// opens a call before streaming its arguments). + fn appendToolArgs(self: *StreamState, out: *EventQueue, output_index: ?usize, delta: []const u8) !void { + const tu = self.lookupTool(output_index) orelse return; + try tu.arguments.appendSlice(self.allocator, delta); + try out.push(.{ .content_delta = .{ + .index = tu.block_index, + .delta = try out.dupeBytes(delta), + } }); + } + + /// Override the matching call's arguments with a restated full value, but + /// only when non-empty — terminal events frequently restate already- + /// streamed arguments as `""`, which must not wipe the accumulated input. + fn setToolArgs(self: *StreamState, output_index: ?usize, arguments: []const u8) !void { + if (arguments.len == 0) return; + const tu = self.lookupTool(output_index) orelse return; + tu.arguments.clearRetainingCapacity(); + try tu.arguments.appendSlice(self.allocator, arguments); + } + + /// Close a ToolUse block. `final_args` (when a `done`/`completed` event + /// restates the full arguments) overrides the accumulated value only when + /// non-empty. A call whose identity never resolved (missing id or name) + /// is dropped. Idempotent: closing an already-closed call is a no-op. + fn closeToolUse( + self: *StreamState, + out: *EventQueue, + output_index: ?usize, + final_args: ?[]const u8, + ) !void { + const tu = self.lookupTool(output_index) orelse return; + if (final_args) |fa| { + if (fa.len > 0) { + tu.arguments.clearRetainingCapacity(); + try tu.arguments.appendSlice(self.allocator, fa); + } + } + tu.closed = true; + + if (tu.id_buf.items.len == 0 or tu.name_buf.items.len == 0) { + // Identity never resolved — nothing dispatchable. Free the buffers + // now; the (empty) entry stays in the map for dedup + final free. + tu.id_buf.clearAndFree(self.allocator); + tu.name_buf.clearAndFree(self.allocator); + tu.arguments.clearAndFree(self.allocator); + return; + } + + const input = try conversation.textualBlockFromSlice(self.allocator, normalizedArguments(tu.arguments.items)); + decodeNameInPlace(&tu.name_buf); + const block: conversation.ContentBlock = .{ .ToolUse = .{ + .id = try self.allocator.dupe(u8, tu.id_buf.items), + .name = try self.allocator.dupe(u8, tu.name_buf.items), + .input = input, + } }; + try self.blocks.append(self.allocator, block); + try out.push(.{ .block_complete = .{ + .index = tu.block_index, + .block = self.blocks.items[self.blocks.items.len - 1], + } }); + } + + fn finalize(self: *StreamState, out: *EventQueue, conv: *conversation.Conversation) !void { + if (self.finalized) return; + self.finalized = true; + try self.closeActive(out); + // Close any tool calls that never received an explicit done event. + var it = self.tools.iterator(); + while (it.next()) |e| { + if (!e.value_ptr.closed) try self.closeToolUse(out, e.key_ptr.*, null); + } + + const moved_blocks = try self.blocks.toOwnedSlice(self.allocator); + defer self.allocator.free(moved_blocks); + if (self.signature_origin) |origin| { + try conversation.setThinkingOrigins( + self.allocator, + moved_blocks, + origin.api_style, + origin.base_url, + origin.model, + ); + } + try conv.addAssistantMessage(moved_blocks, self.usage); + if (self.assistant_phase) |phase| { + const md = switch (phase) { + .commentary => openai_phase_commentary_metadata, + .final_answer => openai_phase_final_answer_metadata, + }; + conv.messages.items[conv.messages.items.len - 1].metadata = try conv.allocator.dupe(u8, md); + } + const msg = conv.messages.items[conv.messages.items.len - 1]; + try out.push(.{ .message_complete = .{ .message = msg, .usage = self.usage } }); + } +}; + +const AssistantPhase = enum { commentary, final_answer }; +const openai_phase_commentary_metadata = "{\"openai_responses_phase\":\"commentary\"}"; +const openai_phase_final_answer_metadata = "{\"openai_responses_phase\":\"final_answer\"}"; + +/// Handle one parsed event. Returns true when the stream is terminal +/// (`response.completed`) so the caller can finalize. +fn handleEvent( + allocator: Allocator, + payload: []const u8, + state: *StreamState, + out: *EventQueue, +) !bool { + var ev = try json_mod.parseStreamEvent(allocator, payload); + defer ev.deinit(); + + switch (ev.kind) { + .output_text_delta => { + if (ev.delta) |d| { + try state.ensureStarted(out); + try state.openBlock(.text, out); + try state.appendDelta(out, d); + } + }, + .reasoning_summary_delta => { + if (ev.delta) |d| { + try state.ensureStarted(out); + try state.openBlock(.thinking, out); + try state.appendDelta(out, d); + } + }, + .output_item_added => { + if (ev.item_type) |it| { + if (std.mem.eql(u8, it, "function_call")) { + try state.ensureStarted(out); + try state.openToolUse(out, ev.output_index, ev.call_id, ev.name); + // `output_item.added` sometimes seeds the full args. + if (ev.arguments) |args| try state.setToolArgs(ev.output_index, args); + } + } + }, + .function_call_arguments_delta => { + if (ev.delta) |d| try state.appendToolArgs(out, ev.output_index, d); + }, + .function_call_arguments_done => { + if (ev.arguments) |args| try state.setToolArgs(ev.output_index, args); + }, + .output_item_done => { + if (ev.item_type) |it| { + if (std.mem.eql(u8, it, "function_call")) { + try state.ensureStarted(out); + try state.openToolUse(out, ev.output_index, ev.call_id, ev.name); + try state.closeToolUse(out, ev.output_index, ev.arguments); + } else if (std.mem.eql(u8, it, "reasoning")) { + if (ev.reasoning_item_json) |sig| { + try state.ensureStarted(out); + try state.openBlock(.thinking, out); + try state.setThinkingSignature(sig); + try state.closeActive(out); + state.block_index += 1; + } + } else if (std.mem.eql(u8, it, "message")) { + if (ev.item_phase) |phase| state.setAssistantPhase(phase); + } + } + }, + .completed => { + // `response.completed` restates every output item; open+close any + // function call not already emitted via `output_item.done`. + for (ev.completed_items) |item| { + try state.ensureStarted(out); + try state.openToolUse(out, item.output_index, item.call_id, item.name); + try state.closeToolUse(out, item.output_index, item.arguments); + } + if (ev.usage) |u| { + const cached = u.cached_tokens; + const total_in = u.input_tokens; + const fresh = if (cached > total_in) 0 else total_in - cached; + state.usage = .{ + .input = fresh, + .output = u.output_tokens, + .cache_read = cached, + .cache_write = 0, + .reasoning = u.reasoning_tokens, + }; + } + return true; + }, + .failed, .err => { + if (ev.error_message) |m| { + std.log.err("openai_responses stream error: {s}", .{m}); + state.setStreamErrorMessage(m); + } + return error.ProviderStreamMalformed; + }, + .other => {}, + } + return false; +} + +// =========================================================================== +// Tests +// =========================================================================== + +const testing = std.testing; + +const EventRecorder = struct { + allocator: Allocator, + events: std.ArrayList([]const u8) = .empty, + + fn deinit(self: *EventRecorder) void { + for (self.events.items) |e| self.allocator.free(e); + self.events.deinit(self.allocator); + } + fn push(self: *EventRecorder, comptime fmt: []const u8, args: anytype) !void { + try self.events.append(self.allocator, try std.fmt.allocPrint(self.allocator, fmt, args)); + } + fn record(self: *EventRecorder, ev: Event) !void { + switch (ev) { + .message_start => try self.push("msg_start", .{}), + .block_start => |b| try self.push("block_start[{d}]:{s}", .{ b.index, @tagName(b.block_type) }), + .tool_details => |t| try self.push("tool_details[{d}]:{s}:{s}", .{ t.index, t.id, t.name }), + .content_delta => |d| try self.push("delta[{d}]:{s}", .{ d.index, d.delta }), + .block_complete => |b| try self.push("block_complete[{d}]", .{b.index}), + .message_complete => |m| { + if (m.usage) |u| { + try self.push("msg_complete[in={d},out={d},cr={d},rsn={d}]", .{ u.input, u.output, u.cache_read, u.reasoning }); + } else try self.push("msg_complete[null]", .{}); + }, + else => {}, + } + } +}; + +fn runStreamedTurn( + allocator: Allocator, + conv: *conversation.Conversation, + rec: ?*EventRecorder, + events: []const []const u8, +) !void { + var state: StreamState = .init(allocator); + defer state.deinit(); + var queue = EventQueue.init(allocator); + defer queue.deinit(); + + var terminal = false; + for (events) |payload| { + terminal = try handleEvent(allocator, payload, &state, &queue); + if (terminal) break; + } + try state.finalize(&queue, conv); + while (queue.pop()) |ev| { + if (rec) |r| try r.record(ev); + } +} + +fn addUserText(conv: *conversation.Conversation, text: []const u8) !void { + const tb = try conversation.textualBlockFromSlice(conv.allocator, text); + var block: conversation.ContentBlock = .{ .Text = tb }; + errdefer block.deinit(conv.allocator); + try conv.addUserMessage(&.{block}); +} + +test "responses stream: reasoning then text then completed" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "hi"); + + var rec = EventRecorder{ .allocator = allocator }; + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"type":"response.reasoning_summary_text.delta","item_id":"rs_1","delta":"thinking…"} + , + \\{"type":"response.output_text.delta","item_id":"msg_1","delta":"Hello"} + , + \\{"type":"response.output_text.delta","item_id":"msg_1","delta":" there"} + , + \\{"type":"response.completed","response":{"usage":{"input_tokens":10,"output_tokens":5,"output_tokens_details":{"reasoning_tokens":2}}}} + , + }; + try runStreamedTurn(allocator, &conv, &rec, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 2), asst.content.items.len); + try testing.expectEqualStrings("thinking…", asst.content.items[0].Thinking.text.items); + try testing.expectEqualStrings("Hello there", asst.content.items[1].Text.items); + + // Usage stamped. + try testing.expect(asst.usage != null); + try testing.expectEqual(@as(u64, 5), asst.usage.?.output); + try testing.expectEqual(@as(u64, 2), asst.usage.?.reasoning); +} + +test "responses stream: function call assembles a ToolUse" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call it"); + + var rec = EventRecorder{ .allocator = allocator }; + defer rec.deinit(); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read"}} + , + \\{"type":"response.function_call_arguments.delta","output_index":0,"item_id":"fc_1","delta":"{\"path\":"} + , + \\{"type":"response.function_call_arguments.delta","output_index":0,"item_id":"fc_1","delta":"\"a\"}"} + , + \\{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read","arguments":"{\"path\":\"a\"}"}} + , + \\{"type":"response.completed","response":{"usage":{"input_tokens":3,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, &rec, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 1), asst.content.items.len); + const tu = asst.content.items[0].ToolUse; + try testing.expectEqualStrings("call_9", tu.id); + // Wire name `std__read` decoded to internal dotted form. + try testing.expectEqualStrings("std.read", tu.name); + try testing.expectEqualStrings("{\"path\":\"a\"}", tu.input.items); + + // Callback order: start → details → deltas → complete. + const expect = [_][]const u8{ + "msg_start", + "block_start[0]:ToolUse", + "tool_details[0]:call_9:std__read", + "delta[0]:{\"path\":", + "delta[0]:\"a\"}", + "block_complete[0]", + "msg_complete[in=3,out=1,cr=0,rsn=0]", + }; + try testing.expectEqual(expect.len, rec.events.items.len); + for (expect, rec.events.items) |w, g| try testing.expectEqualStrings(w, g); +} + +test "responses stream: function call arguments can be keyed by output_index" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call it"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read"}} + , + \\{"type":"response.function_call_arguments.delta","output_index":0,"delta":"{\"path\":"} + , + \\{"type":"response.function_call_arguments.delta","output_index":0,"delta":"\"a\"}"} + , + \\{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read","arguments":""}} + , + \\{"type":"response.completed","response":{"usage":{"input_tokens":3,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("call_9", tu.id); + try testing.expectEqualStrings("std.read", tu.name); + try testing.expectEqualStrings("{\"path\":\"a\"}", tu.input.items); +} + +test "responses stream: function call arguments on item added are retained" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call it"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__bash","arguments":"{\"command\":\"ls\"}"}} + , + \\{"type":"response.completed","response":{"usage":{"input_tokens":3,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("call_9", tu.id); + try testing.expectEqualStrings("std.bash", tu.name); + try testing.expectEqualStrings("{\"command\":\"ls\"}", tu.input.items); +} + +test "responses stream: function call arguments done supplies final input" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call it"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read"}} + , + \\{"type":"response.function_call_arguments.done","output_index":0,"arguments":"{\"path\":\"a\"}"} + , + \\{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read","arguments":""}} + , + \\{"type":"response.completed","response":{"usage":{"input_tokens":3,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("call_9", tu.id); + try testing.expectEqualStrings("std.read", tu.name); + try testing.expectEqualStrings("{\"path\":\"a\"}", tu.input.items); +} + +test "responses stream: completed output supplies final function call input" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call it"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read","arguments":""}} + , + \\{"type":"response.completed","response":{"output":[{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read","arguments":"{\"path\":\"a\"}","status":"completed"}],"usage":{"input_tokens":3,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("call_9", tu.id); + try testing.expectEqualStrings("std.read", tu.name); + try testing.expectEqualStrings("{\"path\":\"a\"}", tu.input.items); +} + +test "responses stream: completed output does not duplicate closed function call" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call it"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read","arguments":""}} + , + \\{"type":"response.function_call_arguments.delta","output_index":0,"delta":"{\"path\":\"a\"}"} + , + \\{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read","arguments":""}} + , + \\{"type":"response.completed","response":{"output":[{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__read","arguments":"{\"path\":\"a\"}","status":"completed"}],"usage":{"input_tokens":3,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 1), asst.content.items.len); + try testing.expectEqualStrings("{\"path\":\"a\"}", asst.content.items[0].ToolUse.input.items); +} + +test "responses stream: empty function call arguments normalize to object" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call it"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"ping","arguments":""}} + , + \\{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"ping","arguments":""}} + , + \\{"type":"response.completed","response":{"usage":{"input_tokens":3,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("call_9", tu.id); + try testing.expectEqualStrings("ping", tu.name); + try testing.expectEqualStrings("{}", tu.input.items); +} + +test "responses stream: finalization closes an open function call" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call it"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","call_id":"call_9","name":"std__bash"}} + , + \\{"type":"response.function_call_arguments.delta","output_index":0,"item_id":"fc_1","delta":"{\"command\":\"ls\"}"} + , + \\{"type":"response.completed","response":{"usage":{"input_tokens":3,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("call_9", tu.id); + try testing.expectEqualStrings("std.bash", tu.name); + try testing.expectEqualStrings("{\"command\":\"ls\"}", tu.input.items); +} + +test "responses stream: streamed args survive an empty arguments.done" { + // Codex streams the arguments as `function_call_arguments.delta` + // fragments and then emits a terminal `function_call_arguments.done` + // whose `arguments` field is empty (the value already arrived via the + // deltas). The accumulated input must NOT be wiped by that empty done. + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "call it"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_1","status":"in_progress","arguments":"","call_id":"call_9","name":"std__read"}} + , + \\{"type":"response.function_call_arguments.delta","item_id":"fc_1","output_index":0,"delta":"{\"path\":"} + , + \\{"type":"response.function_call_arguments.delta","item_id":"fc_1","output_index":0,"delta":"\"a\"}"} + , + \\{"type":"response.function_call_arguments.done","item_id":"fc_1","output_index":0,"arguments":""} + , + \\{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_1","status":"completed","arguments":"","call_id":"call_9","name":"std__read"}} + , + \\{"type":"response.completed","response":{"usage":{"input_tokens":3,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const tu = conv.messages.items[1].content.items[0].ToolUse; + try testing.expectEqualStrings("call_9", tu.id); + try testing.expectEqualStrings("std.read", tu.name); + try testing.expectEqualStrings("{\"path\":\"a\"}", tu.input.items); +} + +test "responses stream: text then tool call keeps block order" { + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "go"); + + const events = [_][]const u8{ + \\{"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"delta":"working"} + , + \\{"type":"response.output_item.added","output_index":1,"item":{"type":"function_call","id":"fc_1","call_id":"c1","name":"ping"}} + , + \\{"type":"response.function_call_arguments.delta","output_index":1,"item_id":"fc_1","delta":"{}"} + , + \\{"type":"response.output_item.done","output_index":1,"item":{"type":"function_call","id":"fc_1","call_id":"c1","name":"ping","arguments":"{}"}} + , + \\{"type":"response.completed","response":{"usage":{"input_tokens":1,"output_tokens":1}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 2), asst.content.items.len); + try testing.expectEqualStrings("working", asst.content.items[0].Text.items); + try testing.expectEqualStrings("ping", asst.content.items[1].ToolUse.name); + try testing.expectEqualStrings("{}", asst.content.items[1].ToolUse.input.items); +} + +/// Drive raw SSE bytes through the same path `produce` uses live: feed +/// arbitrary byte chunks to the `SSEParser`, decode each event, and drain the +/// `EventQueue` after every chunk (which resets its arena). This exercises +/// payload lifetimes the all-at-once `runStreamedTurn` helper does not. +fn runRawStream( + allocator: Allocator, + conv: *conversation.Conversation, + raw: []const u8, + chunk_len: usize, +) !void { + var parser = sse_mod.SSEParser.init(allocator); + defer parser.deinit(); + var state: StreamState = .init(allocator); + defer state.deinit(); + var queue = EventQueue.init(allocator); + defer queue.deinit(); + + var off: usize = 0; + var done = false; + while (off < raw.len and !done) { + const end = @min(off + chunk_len, raw.len); + const events = try parser.feed(raw[off..end]); + defer parser.freeEvents(events); + off = end; + for (events) |payload| { + if (try handleEvent(allocator, payload, &state, &queue)) { + done = true; + break; + } + } + // Drain (and reset the arena) between chunks, as the agent loop does. + while (queue.pop()) |_| {} + } + try state.finalize(&queue, conv); + while (queue.pop()) |_| {} +} + +test "responses stream: realistic codex SSE assembles tool input across chunks" { + // A representative ChatGPT-Codex function-call stream: explicit `event:` + // lines, args streamed as `function_call_arguments.delta` fragments, and + // terminal `done`/`completed` events that restate `arguments` as "". + // Sliced into small byte chunks so events straddle reads and the queue + // arena resets mid-stream. + const allocator = testing.allocator; + + const raw = + "event: response.created\n" ++ + "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_1\"}}\n\n" ++ + "event: response.output_item.added\n" ++ + "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"id\":\"fc_1\",\"type\":\"function_call\",\"status\":\"in_progress\",\"arguments\":\"\",\"call_id\":\"call_9\",\"name\":\"std__read\"}}\n\n" ++ + "event: response.function_call_arguments.delta\n" ++ + "data: {\"type\":\"response.function_call_arguments.delta\",\"item_id\":\"fc_1\",\"output_index\":0,\"delta\":\"{\\\"path\\\":\"}\n\n" ++ + "event: response.function_call_arguments.delta\n" ++ + "data: {\"type\":\"response.function_call_arguments.delta\",\"item_id\":\"fc_1\",\"output_index\":0,\"delta\":\"\\\"/tmp/x\\\"}\"}\n\n" ++ + "event: response.function_call_arguments.done\n" ++ + "data: {\"type\":\"response.function_call_arguments.done\",\"item_id\":\"fc_1\",\"output_index\":0,\"arguments\":\"\"}\n\n" ++ + "event: response.output_item.done\n" ++ + "data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"id\":\"fc_1\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"\",\"call_id\":\"call_9\",\"name\":\"std__read\"}}\n\n" ++ + "event: response.completed\n" ++ + "data: {\"type\":\"response.completed\",\"response\":{\"output\":[{\"id\":\"fc_1\",\"type\":\"function_call\",\"status\":\"completed\",\"arguments\":\"{\\\"path\\\":\\\"/tmp/x\\\"}\",\"call_id\":\"call_9\",\"name\":\"std__read\"}],\"usage\":{\"input_tokens\":10,\"output_tokens\":5}}}\n\n"; + + // Try several chunk sizes so events land on different read boundaries. + for ([_]usize{ 1, 7, 64, raw.len }) |chunk_len| { + var c = conversation.Conversation.init(allocator); + defer c.deinit(); + try addUserText(&c, "read the file"); + try runRawStream(allocator, &c, raw, chunk_len); + + const asst = c.messages.items[1]; + try testing.expectEqual(@as(usize, 1), asst.content.items.len); + const tu = asst.content.items[0].ToolUse; + try testing.expectEqualStrings("call_9", tu.id); + try testing.expectEqualStrings("std.read", tu.name); + try testing.expectEqualStrings("{\"path\":\"/tmp/x\"}", tu.input.items); + } +} + +test "responses stream: parallel function calls assemble distinct ToolUse blocks" { + // The model emits several function calls in one turn, each with its own + // item_id / output_index / call_id, with their argument deltas + // interleaved. Each must become its own ToolUse block with the right + // input — no cross-talk between calls. + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "ls three dirs"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","id":"fc_a","call_id":"call_a","name":"std__shell","arguments":""}} + , + \\{"type":"response.output_item.added","output_index":1,"item":{"type":"function_call","id":"fc_b","call_id":"call_b","name":"std__shell","arguments":""}} + , + \\{"type":"response.function_call_arguments.delta","item_id":"fc_a","output_index":0,"delta":"{\"command\":\"ls a\"}"} + , + \\{"type":"response.function_call_arguments.delta","item_id":"fc_b","output_index":1,"delta":"{\"command\":"} + , + \\{"type":"response.output_item.added","output_index":2,"item":{"type":"function_call","id":"fc_c","call_id":"call_c","name":"std__shell","arguments":""}} + , + \\{"type":"response.function_call_arguments.delta","item_id":"fc_b","output_index":1,"delta":"\"ls b\"}"} + , + \\{"type":"response.function_call_arguments.delta","item_id":"fc_c","output_index":2,"delta":"{\"command\":\"ls c\"}"} + , + \\{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"fc_a","call_id":"call_a","name":"std__shell","arguments":""}} + , + \\{"type":"response.output_item.done","output_index":1,"item":{"type":"function_call","id":"fc_b","call_id":"call_b","name":"std__shell","arguments":""}} + , + \\{"type":"response.output_item.done","output_index":2,"item":{"type":"function_call","id":"fc_c","call_id":"call_c","name":"std__shell","arguments":""}} + , + \\{"type":"response.completed","response":{"output":[{"type":"function_call","id":"fc_a","call_id":"call_a","name":"std__shell","arguments":"{\"command\":\"ls a\"}"},{"type":"function_call","id":"fc_b","call_id":"call_b","name":"std__shell","arguments":"{\"command\":\"ls b\"}"},{"type":"function_call","id":"fc_c","call_id":"call_c","name":"std__shell","arguments":"{\"command\":\"ls c\"}"}],"usage":{"input_tokens":5,"output_tokens":9}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 3), asst.content.items.len); + try testing.expectEqualStrings("call_a", asst.content.items[0].ToolUse.id); + try testing.expectEqualStrings("{\"command\":\"ls a\"}", asst.content.items[0].ToolUse.input.items); + try testing.expectEqualStrings("call_b", asst.content.items[1].ToolUse.id); + try testing.expectEqualStrings("{\"command\":\"ls b\"}", asst.content.items[1].ToolUse.input.items); + try testing.expectEqualStrings("call_c", asst.content.items[2].ToolUse.id); + try testing.expectEqualStrings("{\"command\":\"ls c\"}", asst.content.items[2].ToolUse.input.items); + for (asst.content.items) |b| try testing.expectEqualStrings("std.shell", b.ToolUse.name); +} + +test "responses stream: one call with a mutating item_id stays a single block" { + // Regression for the GitHub Copilot Responses proxy: it emits a *fresh*, + // opaque `item_id` on every event for the SAME call — the + // `output_item.added`, each `…arguments.delta`, the `…arguments.done`, the + // `output_item.done`, and the `response.completed` restatement all carry + // different `item_id`s. Only `output_index` (and `call_id`) are stable. + // Keying tool calls by `item_id` made dedup miss, fanning this single call + // out into three identical ToolUse blocks sharing one `call_id` (which in + // turn stranded two UI result boxes at the `(…)` placeholder). Keying by + // `output_index` must collapse it back to exactly one block. A leading + // reasoning item (also with mutating ids) must not spawn a phantom tool. + const allocator = testing.allocator; + var conv = conversation.Conversation.init(allocator); + defer conv.deinit(); + try addUserText(&conv, "what's in the current directory?"); + + const events = [_][]const u8{ + \\{"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rsn_AAAA"}} + , + \\{"type":"response.output_item.done","output_index":0,"item":{"type":"reasoning","id":"rsn_BBBB"}} + , + \\{"type":"response.output_item.added","output_index":1,"item":{"type":"function_call","id":"itm_AAAA","call_id":"call_KOEB","name":"std__shell","arguments":""}} + , + \\{"type":"response.function_call_arguments.delta","output_index":1,"item_id":"itm_BBBB","delta":"{\"command\":"} + , + \\{"type":"response.function_call_arguments.delta","output_index":1,"item_id":"itm_CCCC","delta":"\"pwd && ls -la\"}"} + , + \\{"type":"response.function_call_arguments.done","output_index":1,"item_id":"itm_DDDD","arguments":""} + , + \\{"type":"response.output_item.done","output_index":1,"item":{"type":"function_call","id":"itm_EEEE","call_id":"call_KOEB","name":"std__shell","arguments":"{\"command\":\"pwd && ls -la\"}"}} + , + \\{"type":"response.completed","response":{"output":[{"type":"reasoning","id":"rsn_CCCC"},{"type":"function_call","id":"itm_FFFF","call_id":"call_KOEB","name":"std__shell","arguments":"{\"command\":\"pwd && ls -la\"}"}],"usage":{"input_tokens":5,"output_tokens":9}}} + , + }; + try runStreamedTurn(allocator, &conv, null, &events); + + const asst = conv.messages.items[1]; + try testing.expectEqual(@as(usize, 1), asst.content.items.len); + const tu = asst.content.items[0].ToolUse; + try testing.expectEqualStrings("call_KOEB", tu.id); + try testing.expectEqualStrings("std.shell", tu.name); + try testing.expectEqualStrings("{\"command\":\"pwd && ls -la\"}", tu.input.items); +} diff --git a/src/public.zig b/src/public.zig new file mode 100644 index 0000000..d8b9f7b --- /dev/null +++ b/src/public.zig @@ -0,0 +1,299 @@ +//! `public.zig` — the sole exported root of `libpanto`. +//! +//! This file is the **explicit public API allowlist**. Internal modules keep +//! their `pub` declarations (needed for cross-file linking) but are never +//! re-exported here; the public surface is what this file names, not +//! "whatever happens to be `pub`." +//! +//! The surface is organized around two user jobs (see +//! `docs/libpanto-cleanup.md`): +//! 1. Run an agent loop — configure a provider, register tools, submit a +//! turn, consume the pull event stream, persist. +//! 2. Construct & operate on conversations — build a `Conversation` by +//! hand, do context-management surgery. +//! +//! Every name here is a straight alias of an internal decl: the internal +//! types have been shaped to *be* the public API (public methods + a +//! transparent public field or two, every other field underscore-prefixed +//! internal state), so this file selects the surface rather than wrapping +//! it. Three buckets: +//! - **behavioral** (`Agent`, `Stream`, `Conversation`): heap-pinned +//! drivers whose `init` hands back a cheap, movable pointer; the chosen +//! methods plus a transparent field (`Agent.conversation`, +//! `Stream.state`) are the surface. +//! - **data (read/output)** (`Event`, `Usage`, `Pricing`, `SessionInfo`): +//! inspected field-by-field. +//! - **data (constructed)** (`ContentBlock`, `Message`, the block types, +//! the `Config` family): Zig users build them with `ArrayList` directly. + +const std = @import("std"); + +const config_mod = @import("config.zig"); +const auth_mod = @import("auth.zig"); +const conversation_mod = @import("conversation.zig"); +const agent_mod = @import("agent.zig"); +const stream_mod = @import("stream.zig"); +const provider_mod = @import("provider.zig"); +const tool_mod = @import("tool.zig"); +const tool_source_mod = @import("tool_source.zig"); +const pricing_mod = @import("pricing.zig"); +const session_store_mod = @import("session_store.zig"); +const fs_store_mod = @import("file_system_jsonl_store.zig"); +const null_store_mod = @import("null_store.zig"); + +// =========================================================================== +// Process lifecycle +// =========================================================================== + +/// Initialize the process-global HTTP client. Call once before any turn. +pub fn init(allocator: std.mem.Allocator, io: std.Io) void { + config_mod.initHttp(allocator, io); +} + +/// Tear down the process-global HTTP client. Call once at shutdown. +pub fn deinit() void { + config_mod.deinitHttp(); +} + +/// Borrow the process-global HTTP client (asserts `init` has run). Embedders +/// driving the OAuth auth flows need this to pass to `oauthLogin` etc. +pub const httpClient = config_mod.httpClient; + +// =========================================================================== +// Config (data, aliased) +// =========================================================================== + +pub const Config = config_mod.Config; +pub const ProviderConfig = config_mod.ProviderConfig; +pub const OpenAIChatConfig = config_mod.OpenAIChatConfig; +pub const AnthropicMessagesConfig = config_mod.AnthropicMessagesConfig; +pub const OpenAIResponsesConfig = config_mod.OpenAIResponsesConfig; +pub const APIStyle = config_mod.APIStyle; +pub const ReasoningEffort = config_mod.ReasoningEffort; +pub const Thinking = config_mod.Thinking; +pub const Effort = config_mod.Effort; +pub const CompactionConfig = config_mod.CompactionConfig; +pub const RetryConfig = config_mod.RetryConfig; +pub const WireIdentity = config_mod.WireIdentity; +pub const Header = config_mod.Header; + +// =========================================================================== +// Auth (data + flows, aliased) +// =========================================================================== + +pub const AuthConfig = auth_mod.AuthConfig; +pub const AuthType = auth_mod.AuthType; +pub const ApiKeyAuth = auth_mod.ApiKeyAuth; +pub const OAuthDeviceAuth = auth_mod.OAuthDeviceAuth; +pub const ExchangeConfig = auth_mod.ExchangeConfig; +pub const DeviceDialect = auth_mod.DeviceDialect; +pub const TokenRequestFormat = auth_mod.TokenRequestFormat; +pub const TokenSet = auth_mod.TokenSet; +pub const ParsedTokenSet = auth_mod.ParsedTokenSet; +pub const ResolvedCredential = auth_mod.ResolvedCredential; + +/// Token persistence under an embedder-chosen auth directory. Files are +/// written owner-only. +pub const loadTokenSet = auth_mod.loadTokenSet; +pub const saveTokenSet = auth_mod.saveTokenSet; +pub const deleteTokenSet = auth_mod.deleteTokenSet; + +// OAuth device-flow mechanism: interactive login, refresh, exchange, and the +// pure credential/JWT helpers the embedder's auth manager composes. +pub const AuthError = auth_mod.AuthError; +pub const Presenter = auth_mod.Presenter; +pub const DeviceCodePrompt = auth_mod.DeviceCodePrompt; +pub const OAuthTokens = auth_mod.OAuthTokens; +pub const oauthLogin = auth_mod.login; +pub const refreshTokens = auth_mod.refreshTokens; +pub const runExchange = auth_mod.runExchange; +pub const buildCredential = auth_mod.buildCredential; +pub const tokensToTokenSet = auth_mod.tokensToTokenSet; +pub const needsRefresh = auth_mod.needsRefresh; +pub const needsExchange = auth_mod.needsExchange; +pub const parseJwtExp = auth_mod.parseJwtExp; +pub const extractAccountId = auth_mod.extractAccountId; + +/// Non-streaming HTTP helper over the process-global client, plus JSON +/// path readers — the building blocks of the OAuth flows. +pub const http = @import("http_helper.zig"); + +// =========================================================================== +// Conversation construction (data, aliased) +// =========================================================================== + +pub const ContentBlock = conversation_mod.ContentBlock; +pub const Message = conversation_mod.Message; +pub const MessageRole = conversation_mod.MessageRole; +pub const TextualBlock = conversation_mod.TextualBlock; +/// Build a `TextualBlock` (the streaming text buffer used inside content +/// blocks) from a slice, copying the bytes with `alloc`. The canonical way +/// for binding code to construct `.Text`/`.Thinking`/tool-result text when +/// assembling `ContentBlock`s by hand (e.g. for conversation resumption). +pub const textualBlockFromSlice = conversation_mod.textualBlockFromSlice; +pub const SignatureOrigin = conversation_mod.SignatureOrigin; +/// Duplicate / free a `Message.identity`'s owned slices. Binding code uses +/// these to attach a per-message wire identity when rebuilding a conversation +/// for resumption (so it survives a later compaction). +pub const dupeWireIdentity = conversation_mod.dupeWireIdentity; +pub const freeWireIdentity = conversation_mod.freeWireIdentity; +pub const ThinkingBlock = conversation_mod.ThinkingBlock; +pub const ToolUseBlock = conversation_mod.ToolUseBlock; +pub const ToolResultBlock = conversation_mod.ToolResultBlock; +pub const ResultPartStored = conversation_mod.ResultPartStored; +pub const StoredMediaPart = conversation_mod.StoredMediaPart; +pub const SystemBlock = conversation_mod.SystemBlock; +pub const SystemMode = conversation_mod.SystemMode; +pub const CompactionSummaryBlock = conversation_mod.CompactionSummaryBlock; +pub const Usage = conversation_mod.Usage; +pub const effectiveSystemBlocks = conversation_mod.effectiveSystemBlocks; + +/// The conversation type. Aliased straight through: its interface has been +/// pared down to exactly the public surface (constructors `init`/`deinit`, +/// the `add*`/`replace*` builders, and the `messages`/`allocator` data +/// fields), so no façade is needed. This is what `Session.load` returns, +/// what `Agent.init` adopts, and what `Agent.conversation()` borrows a +/// pointer to (for in-place surgery). Build one standalone with `init` for +/// by-hand construction; once handed to an `Agent` it is owned by the agent. +pub const Conversation = conversation_mod.Conversation; + +// =========================================================================== +// Tools (data, aliased) +// =========================================================================== + +pub const Tool = tool_mod.Tool; +pub const ToolSource = tool_source_mod.ToolSource; +pub const ToolDecl = tool_mod.ToolDecl; +pub const ToolCall = tool_source_mod.Call; +pub const ToolCallResult = tool_source_mod.CallResult; +pub const MediaPart = tool_mod.MediaPart; +pub const ResultPart = tool_mod.ResultPart; + +/// Result-part ergonomics: a thin wrapper around `[]ResultPart` carrying +/// the `fromText`/`fromTextOwned` constructors and `deinit`. Aliased +/// straight through. +pub const ResultParts = tool_mod.ResultParts; + +// =========================================================================== +// Stream (behavioral, heap-pinned) +// =========================================================================== + +pub const Event = stream_mod.Event; +pub const ContentBlockType = provider_mod.ContentBlockType; +pub const ProviderRetryInfo = provider_mod.ProviderRetryInfo; + +/// The pull event stream for one turn. Aliased straight through: its public +/// surface is exactly `next`/`deinit` plus the transparent `state` field +/// (`State`); every other field is underscore-prefixed internal state. +/// `Agent.run` returns a `*Stream` (heap-pinned); the caller owns it and +/// must `deinit` it (which persists the turn tail and frees the allocation). +/// +/// `next()` returns `!?Event`: a value is progress (including the terminal +/// event), `null` is exhaustion, an error is a genuine failure. +pub const Stream = agent_mod.Stream; + +// =========================================================================== +// Agent (behavioral, heap-pinned) +// =========================================================================== + +pub const UserMessage = agent_mod.UserMessage; +pub const CompactionResult = agent_mod.CompactionResult; + +/// The agent loop driver. Aliased straight through: its public surface is +/// exactly the chosen methods (`init`/`deinit`, `registerTool`, +/// `registerToolSource`, `setConfig`, `run`, `addSystemMessage`, +/// `setSystemPrompt`, `compact`, `sessionId`) plus the transparent +/// `conversation` field; every other field is underscore-prefixed internal +/// state. +/// +/// `init` heap-pins the agent and returns a `*Agent` — a cheap, movable +/// handle out of the gate ("don't move the Agent" stops being a rule anyone +/// can violate). The caller owns it and must `deinit` it. `session` is +/// minted via `store.create()` (fresh) or `resolve`/`latest` (resume); when +/// `maybe_conversation` is non-null it is adopted (ownership transferred) — +/// the resume path hands the value returned by `Session.load`. +/// +/// Operate on the live conversation in place via the `conversation` field +/// (valid for the agent's lifetime; do not retain past it). +pub const Agent = agent_mod.Agent; + +// =========================================================================== +// Pricing (data, aliased) +// =========================================================================== + +pub const Pricing = pricing_mod.Pricing; +pub const PricingRegistry = pricing_mod.Registry; +/// Compute the cost of a single turn's `Usage` under the given `Pricing`, +/// in micro-cents (1/1,000,000 of a cent per token). Returns null when any +/// priced component of any nonzero category is null (the poison rule — +/// don't pretend a turn with unknown cache pricing is free). Aliased +/// through `libpanto` because the embedder accumulates session totals +/// across potentially-switched models. +pub const costMicroCents = pricing_mod.costMicroCents; +/// Add a turn's micro-cents cost to a running session total. Either +/// side null => result null. The single accumulation point for the +/// per-turn session cost (see `pricing.zig` for why). +pub const addCost = pricing_mod.addCost; + +// =========================================================================== +// Sessions +// =========================================================================== + +pub const SessionStore = session_store_mod.SessionStore; +pub const Session = session_store_mod.Session; +pub const SessionInfo = session_store_mod.SessionInfo; +pub const PersistentMessage = session_store_mod.PersistentMessage; +pub const NullStore = null_store_mod.NullStore; +pub const FileSystemJSONLStore = fs_store_mod.FileSystemJSONLStore; + +// =========================================================================== +// Tests +// =========================================================================== + +const openai_chat_json = @import("openai_chat_json.zig"); +const provider_openai_chat = @import("provider_openai_chat.zig"); +const anthropic_messages_json = @import("anthropic_messages_json.zig"); +const provider_anthropic_messages = @import("provider_anthropic_messages.zig"); +const openai_responses_json = @import("openai_responses_json.zig"); +const provider_openai_responses = @import("provider_openai_responses.zig"); +const compaction_mod = @import("compaction.zig"); +const sse_mod = @import("sse.zig"); +const tool_registry_mod = @import("tool_registry.zig"); +const session_mod = @import("session.zig"); +const turn_persist_mod = @import("turn_persist.zig"); +const image_mod = @import("image.zig"); + +test { + // See the note in the old root.zig: gate test logs at `.err` so expected + // warning-path tests stay quiet. + std.testing.log_level = .err; + + std.testing.refAllDecls(@This()); + // Internal modules' tests (not part of the public surface, but their + // tests must still run). + std.testing.refAllDecls(config_mod); + std.testing.refAllDecls(auth_mod); + std.testing.refAllDecls(@import("http_helper.zig")); + std.testing.refAllDecls(conversation_mod); + std.testing.refAllDecls(agent_mod); + std.testing.refAllDecls(stream_mod); + std.testing.refAllDecls(provider_mod); + std.testing.refAllDecls(tool_mod); + std.testing.refAllDecls(tool_source_mod); + std.testing.refAllDecls(tool_registry_mod); + std.testing.refAllDecls(pricing_mod); + std.testing.refAllDecls(session_store_mod); + std.testing.refAllDecls(fs_store_mod); + std.testing.refAllDecls(null_store_mod); + std.testing.refAllDecls(session_mod); + std.testing.refAllDecls(turn_persist_mod); + std.testing.refAllDecls(compaction_mod); + std.testing.refAllDecls(sse_mod); + std.testing.refAllDecls(image_mod); + std.testing.refAllDecls(openai_chat_json); + std.testing.refAllDecls(provider_openai_chat); + std.testing.refAllDecls(anthropic_messages_json); + std.testing.refAllDecls(provider_anthropic_messages); + std.testing.refAllDecls(openai_responses_json); + std.testing.refAllDecls(provider_openai_responses); +} diff --git a/src/session.zig b/src/session.zig new file mode 100644 index 0000000..35fd09b --- /dev/null +++ b/src/session.zig @@ -0,0 +1,1606 @@ +//! On-disk session entry types and JSON serialization. +//! +//! These types are the wire format of pantograph's session log. They are +//! intentionally separate from the in-memory `Conversation`/`Message`/ +//! `ContentBlock` model: +//! +//! - The in-memory model holds only what providers need to serialize a +//! request (`role`, `content`). +//! - The on-disk model holds the full event-log story: provider/model +//! used per request, assistant stop reason and usage, timestamps, and +//! enough tree structure (`id`/`parent_id`) to allow future branching. +//! +//! Bridge functions at the bottom convert between the two. The bridge is +//! lossy by design: assistant metadata (provider/model/stop_reason/usage) +//! is recorded in entries but does NOT round-trip into the in-memory +//! conversation, because providers don't need it for request serialization. +//! +//! Format version: 1 (see `CURRENT_VERSION`). No prior versions exist; +//! when v2 lands, a migration step on load can transform v1 entries. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Writer = std.Io.Writer; + +const conversation = @import("conversation.zig"); +const config = @import("config.zig"); + +pub const APIStyle = config.APIStyle; +pub const ReasoningEffort = config.ReasoningEffort; +pub const Thinking = config.Thinking; +pub const Effort = config.Effort; + +/// Wire-format provider identity stamped on a message entry. This is the +/// ground truth of which endpoint a turn was sent to — never a CLI config +/// alias, and never any `api_key` material. Recorded on user/assistant +/// entries; null on system entries. +pub const WireStamp = struct { + api_style: APIStyle, + base_url: []const u8, // owned + model: []const u8, // owned + /// OpenAI only. Defaults to `.default` (field omitted on the wire). + reasoning: ReasoningEffort = .default, + /// Anthropic only. Defaults to `.enabled`. + thinking: Thinking = .enabled, + /// Anthropic only; only meaningful when `thinking == .adaptive`. + effort: Effort = .medium, + /// Anthropic only; only meaningful when `thinking == .enabled`. `null` + /// means "use the config default" (falls back to `max_tokens - 1`). + thinking_budget_tokens: ?u32 = 32_000, + /// Anthropic only; only meaningful when `thinking == .enabled`. + thinking_interleaved: bool = false, + + pub fn deinit(self: WireStamp, alloc: Allocator) void { + alloc.free(self.base_url); + alloc.free(self.model); + } + + pub fn dupe(self: WireStamp, alloc: Allocator) !WireStamp { + const burl = try alloc.dupe(u8, self.base_url); + errdefer alloc.free(burl); + const mdl = try alloc.dupe(u8, self.model); + return .{ + .api_style = self.api_style, + .base_url = burl, + .model = mdl, + .reasoning = self.reasoning, + .thinking = self.thinking, + .effort = self.effort, + .thinking_budget_tokens = self.thinking_budget_tokens, + .thinking_interleaved = self.thinking_interleaved, + }; + } +}; + +/// Bumped whenever the on-disk format changes in a way that older readers +/// cannot tolerate. When that happens, add a load-time migration that +/// upgrades older files and rewrites them once. +pub const CURRENT_VERSION: u32 = 1; + +// ============================================================================= +// Header +// ============================================================================= + +/// First (and only) line of a session file. Metadata only — not part of +/// the entry tree (no id/parent_id). +pub const SessionHeader = struct { + version: u32, + id: []const u8, // UUIDv7 string, owned + timestamp: []const u8, // ISO 8601, owned + /// Opaque session-wide metadata bag. Round-trips verbatim; `libpanto` + /// never interprets it. The panto CLI records `{ "cwd": ... }` here. + metadata: ?[]const u8 = null, // owned + + pub fn deinit(self: SessionHeader, alloc: Allocator) void { + alloc.free(self.id); + alloc.free(self.timestamp); + if (self.metadata) |m| alloc.free(m); + } +}; + +// ============================================================================= +// Entries +// ============================================================================= + +/// Fields shared by every non-header entry. +pub const EntryBase = struct { + id: []const u8, // 8-char hex, owned + parent_id: ?[]const u8, // owned, null for first entry + timestamp: []const u8, // ISO 8601, owned + + pub fn deinit(self: EntryBase, alloc: Allocator) void { + alloc.free(self.id); + if (self.parent_id) |p| alloc.free(p); + alloc.free(self.timestamp); + } +}; + +pub const SessionEntry = union(enum) { + message: MessageEntry, + + pub fn base(self: SessionEntry) EntryBase { + return switch (self) { + .message => |m| m.base, + }; + } + + pub fn deinit(self: SessionEntry, alloc: Allocator) void { + switch (self) { + .message => |m| m.deinit(alloc), + } + } +}; + +pub const MessageEntry = struct { + base: EntryBase, + /// Wire-format provider identity for this entry. Recorded on user and + /// assistant message entries (both are tied to a provider API call); + /// null on system entries. + stamp: ?WireStamp = null, + message: StoredMessage, + + pub fn deinit(self: MessageEntry, alloc: Allocator) void { + self.base.deinit(alloc); + if (self.stamp) |s| s.deinit(alloc); + self.message.deinit(alloc); + } +}; + +pub const StoredMessageRole = enum { system, user, assistant }; + +/// Mode for a system-role message. Mirrors `conversation.SystemMode`. +/// `append` adds to the effective prompt; `replace` discards all prior +/// system text. Only meaningful on system messages; absent on disk means +/// `append` (back-compatible with pre-mode logs). +pub const StoredSystemMode = enum { append, replace }; + +pub const StoredMessage = struct { + role: StoredMessageRole, + content: []StoredContentBlock, // owned + /// System-message mode. Recorded only for system-role messages; an + /// absent `mode` on disk parses back as `.append`. + mode: StoredSystemMode = .append, + /// Assistant-only stop reason. Null for system/user messages. + stop_reason: ?[]const u8 = null, // owned + usage: ?Usage = null, + /// Opaque per-message metadata bag (see `conversation.Message.metadata`). + /// Round-trips verbatim; `libpanto` never interprets it. + metadata: ?[]const u8 = null, // owned + + pub fn deinit(self: StoredMessage, alloc: Allocator) void { + for (self.content) |block| block.deinit(alloc); + alloc.free(self.content); + if (self.stop_reason) |s| alloc.free(s); + if (self.metadata) |m| alloc.free(m); + } +}; + +/// Token usage reported by a provider for a single assistant turn. +/// +/// Defined in `conversation.zig` (so in-memory `Message`s can carry it +/// without a module cycle) and re-exported here for the on-disk types and +/// historical call sites that import it as `session.Usage`. +pub const Usage = conversation.Usage; + +// ============================================================================= +// Content blocks +// ============================================================================= + +pub const StoredContentBlock = union(enum) { + text: StoredTextBlock, + thinking: StoredThinkingBlock, + tool_use: StoredToolUseBlock, + tool_result: StoredToolResultBlock, + compaction_summary: StoredCompactionSummaryBlock, + + pub fn deinit(self: StoredContentBlock, alloc: Allocator) void { + switch (self) { + .text => |b| b.deinit(alloc), + .thinking => |b| b.deinit(alloc), + .tool_use => |b| b.deinit(alloc), + .tool_result => |b| b.deinit(alloc), + .compaction_summary => |b| b.deinit(alloc), + } + } +}; + +pub const StoredTextBlock = struct { + text: []const u8, // owned + pub fn deinit(self: StoredTextBlock, alloc: Allocator) void { + alloc.free(self.text); + } +}; + +pub const StoredThinkingBlock = struct { + thinking: []const u8, // owned + /// Anthropic's opaque integrity token. Other providers do not produce + /// one. Preserved here so resumed sessions can be sent back to + /// Anthropic with the original thinking block intact. + signature: ?[]const u8 = null, // owned + pub fn deinit(self: StoredThinkingBlock, alloc: Allocator) void { + alloc.free(self.thinking); + if (self.signature) |s| alloc.free(s); + } +}; + +pub const StoredToolUseBlock = struct { + id: []const u8, // owned + name: []const u8, // owned + input: []const u8, // raw JSON bytes, owned + pub fn deinit(self: StoredToolUseBlock, alloc: Allocator) void { + alloc.free(self.id); + alloc.free(self.name); + alloc.free(self.input); + } +}; + +/// One on-disk tool-result part: either text or an inline base64 media +/// attachment (no sidecar files). +pub const StoredResultPart = union(enum) { + text: []const u8, // owned + media: struct { + media_type: []const u8, // owned + data: []const u8, // owned (base64) + }, + pub fn deinit(self: StoredResultPart, alloc: Allocator) void { + switch (self) { + .text => |t| alloc.free(t), + .media => |m| { + alloc.free(m.media_type); + alloc.free(m.data); + }, + } + } +}; + +pub const StoredToolResultBlock = struct { + tool_use_id: []const u8, // owned + parts: []StoredResultPart, // owned + is_error: bool = false, + pub fn deinit(self: StoredToolResultBlock, alloc: Allocator) void { + alloc.free(self.tool_use_id); + for (self.parts) |p| p.deinit(alloc); + alloc.free(self.parts); + } +}; + +/// A compaction summary block: the synthetic seed text standing in for a +/// compacted conversation prefix. Sits alone in a `user`-role message. See +/// `conversation.CompactionSummaryBlock`. +pub const StoredCompactionSummaryBlock = struct { + text: []const u8, // owned + pub fn deinit(self: StoredCompactionSummaryBlock, alloc: Allocator) void { + alloc.free(self.text); + } +}; + +// ============================================================================= +// File entry (header or entry) +// ============================================================================= + +pub const FileEntry = union(enum) { + header: SessionHeader, + entry: SessionEntry, + + pub fn deinit(self: FileEntry, alloc: Allocator) void { + switch (self) { + .header => |h| h.deinit(alloc), + .entry => |e| e.deinit(alloc), + } + } +}; + +// ============================================================================= +// Serialization +// ============================================================================= + +/// Serialize the header as a single JSON line. Caller owns returned bytes. +/// The returned slice does NOT include a trailing newline. +pub fn serializeHeader(allocator: Allocator, header: SessionHeader) ![]u8 { + var aw: Writer.Allocating = .init(allocator); + errdefer aw.deinit(); + + var s: std.json.Stringify = .{ .writer = &aw.writer }; + try s.beginObject(); + try s.objectField("type"); + try s.write("session"); + try s.objectField("version"); + try s.write(header.version); + try s.objectField("id"); + try s.write(header.id); + try s.objectField("timestamp"); + try s.write(header.timestamp); + if (header.metadata) |md| { + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, md, .{}); + defer parsed.deinit(); + try s.objectField("metadata"); + try s.write(parsed.value); + } + try s.endObject(); + + return try aw.toOwnedSlice(); +} + +/// Serialize an entry as a single JSON line. Caller owns returned bytes. +pub fn serializeEntry(allocator: Allocator, entry: SessionEntry) ![]u8 { + var aw: Writer.Allocating = .init(allocator); + errdefer aw.deinit(); + var s: std.json.Stringify = .{ .writer = &aw.writer }; + try writeEntry(&s, entry); + return try aw.toOwnedSlice(); +} + +fn writeEntry(s: *std.json.Stringify, entry: SessionEntry) !void { + switch (entry) { + .message => |m| try writeMessageEntry(s, m), + } +} + +fn writeMessageEntry(s: *std.json.Stringify, m: MessageEntry) !void { + try s.beginObject(); + try s.objectField("type"); + try s.write("message"); + try s.objectField("id"); + try s.write(m.base.id); + try s.objectField("parentId"); + if (m.base.parent_id) |p| try s.write(p) else try s.write(null); + try s.objectField("timestamp"); + try s.write(m.base.timestamp); + // Wire-format provider identity on user/assistant entries. + if (m.stamp) |st| try writeWireStamp(s, st); + try s.objectField("message"); + try writeDiskMessage(s, m.message); + try s.endObject(); +} + +fn writeWireStamp(s: *std.json.Stringify, st: WireStamp) !void { + try s.objectField("apiStyle"); + try s.write(@tagName(st.api_style)); + try s.objectField("baseUrl"); + try s.write(st.base_url); + try s.objectField("model"); + try s.write(st.model); + // OpenAI: emit reasoning only when non-default (keeps logs compact). + if (st.reasoning != .default) { + try s.objectField("reasoning"); + try s.write(@tagName(st.reasoning)); + } + // Anthropic: emit thinking fields only when they differ from defaults. + if (st.thinking != .enabled) { + try s.objectField("thinking"); + try s.write(@tagName(st.thinking)); + } + if (st.effort != .medium) { + try s.objectField("effort"); + try s.write(@tagName(st.effort)); + } + if (st.thinking_budget_tokens) |b| { + if (b != 32_000) { + try s.objectField("thinkingBudgetTokens"); + try s.write(b); + } + } else { + // null means "use max_tokens - 1"; record the absence explicitly + // so round-trips preserve the null intent. + try s.objectField("thinkingBudgetTokens"); + try s.write(null); + } + if (st.thinking_interleaved) { + try s.objectField("thinkingInterleaved"); + try s.write(true); + } +} + +fn writeDiskMessage(s: *std.json.Stringify, msg: StoredMessage) !void { + try s.beginObject(); + try s.objectField("role"); + try s.write(@tagName(msg.role)); + // `mode` is meaningful only for system messages. Emit it there so the + // append/replace semantics round-trip; omit it everywhere else. + if (msg.role == .system) { + try s.objectField("mode"); + try s.write(@tagName(msg.mode)); + } + try s.objectField("content"); + try s.beginArray(); + for (msg.content) |block| { + try writeDiskBlock(s, block); + } + try s.endArray(); + if (msg.stop_reason) |sr| { + try s.objectField("stopReason"); + try s.write(sr); + } + if (msg.metadata) |md| { + try s.objectField("metadata"); + try s.write(md); + } + if (msg.usage) |u| { + try s.objectField("usage"); + try s.beginObject(); + try s.objectField("input"); + try s.write(u.input); + try s.objectField("output"); + try s.write(u.output); + // Omit zero-valued auxiliary fields to keep older / unused + // sessions compact. Readers default missing fields to 0, so + // round-trip behavior is preserved. + if (u.cache_read != 0) { + try s.objectField("cacheRead"); + try s.write(u.cache_read); + } + if (u.cache_write != 0) { + try s.objectField("cacheWrite"); + try s.write(u.cache_write); + } + if (u.reasoning != 0) { + try s.objectField("reasoning"); + try s.write(u.reasoning); + } + try s.endObject(); + } + try s.endObject(); +} + +fn writeDiskBlock(s: *std.json.Stringify, block: StoredContentBlock) !void { + switch (block) { + .text => |b| { + try s.beginObject(); + try s.objectField("type"); + try s.write("text"); + try s.objectField("text"); + try s.write(b.text); + try s.endObject(); + }, + .thinking => |b| { + try s.beginObject(); + try s.objectField("type"); + try s.write("thinking"); + try s.objectField("thinking"); + try s.write(b.thinking); + if (b.signature) |sig| { + try s.objectField("signature"); + try s.write(sig); + } + try s.endObject(); + }, + .tool_use => |b| { + try s.beginObject(); + try s.objectField("type"); + try s.write("toolUse"); + try s.objectField("id"); + try s.write(b.id); + try s.objectField("name"); + try s.write(b.name); + try s.objectField("input"); + try s.write(b.input); + try s.endObject(); + }, + .tool_result => |b| { + try s.beginObject(); + try s.objectField("type"); + try s.write("toolResult"); + try s.objectField("toolUseId"); + try s.write(b.tool_use_id); + // Persist the error marker only when set, so existing + // (success) tool-result logs serialize byte-identically. + if (b.is_error) { + try s.objectField("isError"); + try s.write(true); + } + // `parts` is an array of {type:"text",text} and + // {type:"image",mimeType,data} (data = inline base64). + try s.objectField("parts"); + try s.beginArray(); + for (b.parts) |part| { + switch (part) { + .text => |t| { + try s.beginObject(); + try s.objectField("type"); + try s.write("text"); + try s.objectField("text"); + try s.write(t); + try s.endObject(); + }, + .media => |m| { + try s.beginObject(); + try s.objectField("type"); + try s.write("image"); + try s.objectField("mimeType"); + try s.write(m.media_type); + try s.objectField("data"); + try s.write(m.data); + try s.endObject(); + }, + } + } + try s.endArray(); + try s.endObject(); + }, + .compaction_summary => |b| { + try s.beginObject(); + try s.objectField("type"); + try s.write("compactionSummary"); + try s.objectField("text"); + try s.write(b.text); + try s.endObject(); + }, + } +} + +// ============================================================================= +// Parsing +// ============================================================================= + +pub const ParseError = error{ + InvalidJson, + MissingField, + UnknownType, + UnknownRole, + UnknownBlockType, +} || Allocator.Error; + +/// Parse one JSON line into a `FileEntry`. Caller owns all bytes. +pub fn parseLine(allocator: Allocator, line: []const u8) ParseError!FileEntry { + var parsed = std.json.parseFromSlice(std.json.Value, allocator, line, .{}) catch { + return error.InvalidJson; + }; + defer parsed.deinit(); + return parseValue(allocator, parsed.value); +} + +fn parseValue(allocator: Allocator, v: std.json.Value) ParseError!FileEntry { + if (v != .object) return error.InvalidJson; + const type_v = v.object.get("type") orelse return error.MissingField; + if (type_v != .string) return error.MissingField; + const t = type_v.string; + if (std.mem.eql(u8, t, "session")) { + return .{ .header = try parseHeaderFromObject(allocator, v.object) }; + } else if (std.mem.eql(u8, t, "message")) { + return .{ .entry = .{ .message = try parseMessageEntry(allocator, v.object) } }; + } else { + return error.UnknownType; + } +} + +fn parseHeaderFromObject(allocator: Allocator, obj: std.json.ObjectMap) ParseError!SessionHeader { + const version: u32 = blk: { + if (obj.get("version")) |vv| { + if (vv == .integer) break :blk @intCast(vv.integer); + } + break :blk 1; + }; + const id = try dupeStringField(allocator, obj, "id"); + errdefer allocator.free(id); + const timestamp = try dupeStringField(allocator, obj, "timestamp"); + errdefer allocator.free(timestamp); + const metadata: ?[]const u8 = blk: { + if (obj.get("metadata")) |mv| { + break :blk try std.json.Stringify.valueAlloc(allocator, mv, .{}); + } + if (obj.get("cwd")) |cv| { + if (cv != .string) return error.MissingField; + const cwd_json = try std.json.Stringify.valueAlloc(allocator, cv, .{}); + defer allocator.free(cwd_json); + break :blk try std.fmt.allocPrint(allocator, "{{\"cwd\":{s}}}", .{cwd_json}); + } + break :blk null; + }; + errdefer if (metadata) |m| allocator.free(m); + return .{ + .version = version, + .id = id, + .timestamp = timestamp, + .metadata = metadata, + }; +} + +fn parseMessageEntry(allocator: Allocator, obj: std.json.ObjectMap) ParseError!MessageEntry { + const id = try dupeStringField(allocator, obj, "id"); + errdefer allocator.free(id); + const timestamp = try dupeStringField(allocator, obj, "timestamp"); + errdefer allocator.free(timestamp); + const parent_id: ?[]const u8 = blk: { + const pv = obj.get("parentId") orelse break :blk null; + if (pv == .null) break :blk null; + if (pv != .string) return error.MissingField; + break :blk try allocator.dupe(u8, pv.string); + }; + errdefer if (parent_id) |p| allocator.free(p); + + const stamp = try parseWireStamp(allocator, obj); + errdefer if (stamp) |st| st.deinit(allocator); + + const msg_v = obj.get("message") orelse return error.MissingField; + if (msg_v != .object) return error.MissingField; + const msg = try parseDiskMessage(allocator, msg_v.object); + + return .{ + .base = .{ .id = id, .parent_id = parent_id, .timestamp = timestamp }, + .stamp = stamp, + .message = msg, + }; +} + +/// Parse the wire-format provider stamp from a message entry object. +/// Returns null when no `apiStyle` field is present (system entries). +fn parseWireStamp(allocator: Allocator, obj: std.json.ObjectMap) ParseError!?WireStamp { + const style_v = obj.get("apiStyle") orelse return null; + if (style_v != .string) return null; + const api_style = std.meta.stringToEnum(APIStyle, style_v.string) orelse return error.MissingField; + const base_url = try dupeStringField(allocator, obj, "baseUrl"); + errdefer allocator.free(base_url); + const model = try dupeStringField(allocator, obj, "model"); + errdefer allocator.free(model); + // OpenAI: absent reasoning defaults to .default. + const reasoning: ReasoningEffort = blk: { + const rv = obj.get("reasoning") orelse break :blk .default; + if (rv != .string) break :blk .default; + break :blk std.meta.stringToEnum(ReasoningEffort, rv.string) orelse .default; + }; + // Anthropic: absent fields default to the same values as the config defaults. + const thinking: Thinking = blk: { + const tv = obj.get("thinking") orelse break :blk .enabled; + if (tv != .string) break :blk .enabled; + break :blk std.meta.stringToEnum(Thinking, tv.string) orelse .enabled; + }; + const effort: Effort = blk: { + const ev = obj.get("effort") orelse break :blk .medium; + if (ev != .string) break :blk .medium; + break :blk std.meta.stringToEnum(Effort, ev.string) orelse .medium; + }; + const thinking_budget_tokens: ?u32 = blk: { + const bv = obj.get("thinkingBudgetTokens") orelse break :blk 32_000; + if (bv == .null) break :blk null; + if (bv != .integer) break :blk 32_000; + if (bv.integer < 0) break :blk 32_000; + break :blk @intCast(bv.integer); + }; + const thinking_interleaved: bool = blk: { + const iv = obj.get("thinkingInterleaved") orelse break :blk false; + if (iv != .bool) break :blk false; + break :blk iv.bool; + }; + return .{ + .api_style = api_style, + .base_url = base_url, + .model = model, + .reasoning = reasoning, + .thinking = thinking, + .effort = effort, + .thinking_budget_tokens = thinking_budget_tokens, + .thinking_interleaved = thinking_interleaved, + }; +} + +fn parseDiskMessage(allocator: Allocator, obj: std.json.ObjectMap) ParseError!StoredMessage { + const role_v = obj.get("role") orelse return error.MissingField; + if (role_v != .string) return error.MissingField; + const role = std.meta.stringToEnum(StoredMessageRole, role_v.string) orelse return error.UnknownRole; + + // `mode` is optional; absent defaults to `.append`. Unknown values are + // tolerated as `.append` rather than rejecting an otherwise-valid log. + const mode: StoredSystemMode = blk: { + const mv = obj.get("mode") orelse break :blk .append; + if (mv != .string) break :blk .append; + break :blk std.meta.stringToEnum(StoredSystemMode, mv.string) orelse .append; + }; + + const content_v = obj.get("content") orelse return error.MissingField; + if (content_v != .array) return error.MissingField; + var content_list = try std.ArrayList(StoredContentBlock).initCapacity(allocator, content_v.array.items.len); + errdefer { + for (content_list.items) |b| b.deinit(allocator); + content_list.deinit(allocator); + } + for (content_v.array.items) |item| { + if (item != .object) return error.UnknownBlockType; + const block = try parseDiskBlock(allocator, item.object); + try content_list.append(allocator, block); + } + const content = try content_list.toOwnedSlice(allocator); + errdefer { + for (content) |b| b.deinit(allocator); + allocator.free(content); + } + + const stop_reason: ?[]const u8 = try dupeOptionalStringField(allocator, obj, "stopReason"); + errdefer if (stop_reason) |s| allocator.free(s); + const metadata: ?[]const u8 = try dupeOptionalStringField(allocator, obj, "metadata"); + errdefer if (metadata) |m| allocator.free(m); + + var usage: ?Usage = null; + if (obj.get("usage")) |uv| { + if (uv == .object) { + usage = .{ + .input = readU64(uv.object, "input"), + .output = readU64(uv.object, "output"), + .cache_read = readU64(uv.object, "cacheRead"), + .cache_write = readU64(uv.object, "cacheWrite"), + .reasoning = readU64(uv.object, "reasoning"), + }; + } + } + + return .{ + .role = role, + .content = content, + .mode = mode, + .stop_reason = stop_reason, + .usage = usage, + .metadata = metadata, + }; +} + +fn parseDiskBlock(allocator: Allocator, obj: std.json.ObjectMap) ParseError!StoredContentBlock { + const type_v = obj.get("type") orelse return error.MissingField; + if (type_v != .string) return error.MissingField; + const t = type_v.string; + if (std.mem.eql(u8, t, "text")) { + const text = try dupeStringField(allocator, obj, "text"); + return .{ .text = .{ .text = text } }; + } else if (std.mem.eql(u8, t, "thinking")) { + const text = try dupeStringField(allocator, obj, "thinking"); + errdefer allocator.free(text); + const sig = try dupeOptionalStringField(allocator, obj, "signature"); + return .{ .thinking = .{ .thinking = text, .signature = sig } }; + } else if (std.mem.eql(u8, t, "toolUse")) { + const id = try dupeStringField(allocator, obj, "id"); + errdefer allocator.free(id); + const name = try dupeStringField(allocator, obj, "name"); + errdefer allocator.free(name); + const input = try dupeStringField(allocator, obj, "input"); + return .{ .tool_use = .{ .id = id, .name = name, .input = input } }; + } else if (std.mem.eql(u8, t, "toolResult")) { + const tuid = try dupeStringField(allocator, obj, "toolUseId"); + errdefer allocator.free(tuid); + const parts = try parseDiskResultParts(allocator, obj); + // Missing `isError` in older logs defaults to false. + const is_err = readBool(obj, "isError"); + return .{ .tool_result = .{ .tool_use_id = tuid, .parts = parts, .is_error = is_err } }; + } else if (std.mem.eql(u8, t, "compactionSummary")) { + const text = try dupeStringField(allocator, obj, "text"); + return .{ .compaction_summary = .{ .text = text } }; + } else { + return error.UnknownBlockType; + } +} + +/// Parse the `parts` array of a `toolResult` disk block. Falls back to a +/// legacy single `content` string field (older session logs) -> one text +/// part. Each element is {type:"text",text} or {type:"image",mimeType,data}. +fn parseDiskResultParts(allocator: Allocator, obj: std.json.ObjectMap) ParseError![]StoredResultPart { + var list: std.ArrayList(StoredResultPart) = .empty; + errdefer { + for (list.items) |p| p.deinit(allocator); + list.deinit(allocator); + } + const parts_v = obj.get("parts"); + if (parts_v == null or parts_v.? == .null) { + // Legacy: a single `content` string. + const content = try dupeStringField(allocator, obj, "content"); + try list.append(allocator, .{ .text = content }); + return list.toOwnedSlice(allocator); + } + if (parts_v.? != .array) return error.MissingField; + for (parts_v.?.array.items) |item| { + if (item != .object) return error.MissingField; + const po = item.object; + const pt_v = po.get("type") orelse return error.MissingField; + if (pt_v != .string) return error.MissingField; + if (std.mem.eql(u8, pt_v.string, "text")) { + const text = try dupeStringField(allocator, po, "text"); + try list.append(allocator, .{ .text = text }); + } else if (std.mem.eql(u8, pt_v.string, "image")) { + const mt = try dupeStringField(allocator, po, "mimeType"); + errdefer allocator.free(mt); + const data = try dupeStringField(allocator, po, "data"); + try list.append(allocator, .{ .media = .{ .media_type = mt, .data = data } }); + } else { + return error.UnknownBlockType; + } + } + return list.toOwnedSlice(allocator); +} + +fn readBool(obj: std.json.ObjectMap, name: []const u8) bool { + const v = obj.get(name) orelse return false; + if (v != .bool) return false; + return v.bool; +} + +fn readU64(obj: std.json.ObjectMap, name: []const u8) u64 { + const v = obj.get(name) orelse return 0; + if (v != .integer) return 0; + if (v.integer < 0) return 0; + return @intCast(v.integer); +} + +fn dupeStringField(allocator: Allocator, obj: std.json.ObjectMap, name: []const u8) ParseError![]const u8 { + const v = obj.get(name) orelse return error.MissingField; + if (v != .string) return error.MissingField; + return try allocator.dupe(u8, v.string); +} + +fn dupeOptionalStringField(allocator: Allocator, obj: std.json.ObjectMap, name: []const u8) ParseError!?[]const u8 { + const v = obj.get(name) orelse return null; + if (v == .null) return null; + if (v != .string) return error.MissingField; + return try allocator.dupe(u8, v.string); +} + +// ============================================================================= +// Bridge between in-memory and on-disk content blocks +// ============================================================================= + +/// Convert an in-memory `ContentBlock` to a `StoredContentBlock`. All strings +/// are duplicated; the source block remains untouched and the resulting +/// disk block is independently owned. +pub fn contentBlockToDisk( + allocator: Allocator, + block: conversation.ContentBlock, +) !StoredContentBlock { + switch (block) { + .Text => |tb| { + const text = try allocator.dupe(u8, tb.items); + return .{ .text = .{ .text = text } }; + }, + .Thinking => |tb| { + const text = try allocator.dupe(u8, tb.text.items); + errdefer allocator.free(text); + const sig: ?[]const u8 = if (tb.signature) |s| try allocator.dupe(u8, s) else null; + return .{ .thinking = .{ .thinking = text, .signature = sig } }; + }, + .ToolUse => |tu| { + const id = try allocator.dupe(u8, tu.id); + errdefer allocator.free(id); + const name = try allocator.dupe(u8, tu.name); + errdefer allocator.free(name); + const input = try allocator.dupe(u8, tu.input.items); + return .{ .tool_use = .{ .id = id, .name = name, .input = input } }; + }, + .ToolResult => |tr| { + const tuid = try allocator.dupe(u8, tr.tool_use_id); + errdefer allocator.free(tuid); + var parts: std.ArrayList(StoredResultPart) = .empty; + errdefer { + for (parts.items) |p| p.deinit(allocator); + parts.deinit(allocator); + } + try parts.ensureTotalCapacity(allocator, tr.parts.items.len); + for (tr.parts.items) |src| { + switch (src) { + .text => |tb| parts.appendAssumeCapacity(.{ .text = try allocator.dupe(u8, tb.items) }), + .media => |m| { + const mt = try allocator.dupe(u8, m.media_type); + errdefer allocator.free(mt); + const data = try allocator.dupe(u8, m.data.items); + parts.appendAssumeCapacity(.{ .media = .{ .media_type = mt, .data = data } }); + }, + } + } + return .{ .tool_result = .{ + .tool_use_id = tuid, + .parts = try parts.toOwnedSlice(allocator), + .is_error = tr.is_error, + } }; + }, + // A `.System` block becomes a disk text block; its mode rides on + // the enclosing `StoredMessage.mode` (set by the session manager), + // not on the block itself. + .System => |sb| { + const text = try allocator.dupe(u8, sb.text.items); + return .{ .text = .{ .text = text } }; + }, + .CompactionSummary => |cs| { + const text = try allocator.dupe(u8, cs.text.items); + return .{ .compaction_summary = .{ .text = text } }; + }, + } +} + +/// Convert a `StoredContentBlock` to an in-memory `ContentBlock`. Allocates +/// fresh owned buffers for every string field. The returned block is +/// independently owned. +pub fn diskContentBlockToInternal( + allocator: Allocator, + block: StoredContentBlock, +) !conversation.ContentBlock { + switch (block) { + .text => |b| { + const tb = try conversation.textualBlockFromSlice(allocator, b.text); + return .{ .Text = tb }; + }, + .thinking => |b| { + const tb = try conversation.textualBlockFromSlice(allocator, b.thinking); + errdefer { + var mut = tb; + mut.deinit(allocator); + } + const sig: ?[]const u8 = if (b.signature) |s| try allocator.dupe(u8, s) else null; + return .{ .Thinking = .{ .text = tb, .signature = sig } }; + }, + .tool_use => |b| { + const id = try allocator.dupe(u8, b.id); + errdefer allocator.free(id); + const name = try allocator.dupe(u8, b.name); + errdefer allocator.free(name); + const input = try conversation.textualBlockFromSlice(allocator, b.input); + return .{ .ToolUse = .{ .id = id, .name = name, .input = input } }; + }, + .tool_result => |b| { + const tuid = try allocator.dupe(u8, b.tool_use_id); + errdefer allocator.free(tuid); + var parts: std.ArrayList(conversation.ResultPartStored) = .empty; + errdefer { + for (parts.items) |*p| p.deinit(allocator); + parts.deinit(allocator); + } + try parts.ensureTotalCapacity(allocator, b.parts.len); + for (b.parts) |src| { + switch (src) { + .text => |t| parts.appendAssumeCapacity(.{ .text = try conversation.textualBlockFromSlice(allocator, t) }), + .media => |m| { + const mt = try allocator.dupe(u8, m.media_type); + errdefer allocator.free(mt); + const data = try conversation.textualBlockFromSlice(allocator, m.data); + parts.appendAssumeCapacity(.{ .media = .{ .media_type = mt, .data = data } }); + }, + } + } + return .{ .ToolResult = .{ .tool_use_id = tuid, .parts = parts, .is_error = b.is_error } }; + }, + .compaction_summary => |b| { + const tb = try conversation.textualBlockFromSlice(allocator, b.text); + return .{ .CompactionSummary = .{ .text = tb } }; + }, + } +} + +// ============================================================================= +// Tests +// ============================================================================= + +const testing = std.testing; + +fn dupe(allocator: Allocator, s: []const u8) ![]const u8 { + return try allocator.dupe(u8, s); +} + +test "serialize/parse header round-trip" { + const a = testing.allocator; + const header: SessionHeader = .{ + .version = 1, + .id = try dupe(a, "019dc5ba-53f6-71a5-ab8f-b1f8709c2572"), + .timestamp = try dupe(a, "2026-04-25T17:40:15.990Z"), + .metadata = try dupe(a, "{\"cwd\":\"/Users/travis/Code/pantograph\"}"), + }; + defer header.deinit(a); + + const line = try serializeHeader(a, header); + defer a.free(line); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + try testing.expect(fe == .header); + try testing.expectEqual(@as(u32, 1), fe.header.version); + try testing.expectEqualStrings(header.id, fe.header.id); + try testing.expectEqualStrings(header.metadata.?, fe.header.metadata.?); +} + +test "serialize/parse user message entry round-trip (with provider/model stamp)" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + content[0] = .{ .text = .{ .text = try dupe(a, "hello world") } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "a1b2c3d4"), + .parent_id = try dupe(a, "00000000"), + .timestamp = try dupe(a, "2026-04-25T17:40:16.000Z"), + }, + .stamp = .{ + .api_style = .openai_chat, + .base_url = try dupe(a, "https://api.openai.com/v1"), + .model = try dupe(a, "gpt-4o"), + .reasoning = .high, + }, + .message = .{ + .role = .user, + .content = content, + }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + try testing.expect(fe == .entry); + const got = fe.entry.message; + try testing.expectEqualStrings("a1b2c3d4", got.base.id); + try testing.expectEqualStrings("00000000", got.base.parent_id.?); + try testing.expectEqual(APIStyle.openai_chat, got.stamp.?.api_style); + try testing.expectEqualStrings("https://api.openai.com/v1", got.stamp.?.base_url); + try testing.expectEqualStrings("gpt-4o", got.stamp.?.model); + try testing.expectEqual(ReasoningEffort.high, got.stamp.?.reasoning); + try testing.expectEqual(StoredMessageRole.user, got.message.role); + try testing.expectEqual(@as(usize, 1), got.message.content.len); + try testing.expectEqualStrings("hello world", got.message.content[0].text.text); +} + +test "serialize/parse assistant message entry with metadata" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 3); + content[0] = .{ .thinking = .{ + .thinking = try dupe(a, "let me think"), + .signature = try dupe(a, "sig-xyz"), + } }; + content[1] = .{ .text = .{ .text = try dupe(a, "I'll check.") } }; + content[2] = .{ .tool_use = .{ + .id = try dupe(a, "tool_abc"), + .name = try dupe(a, "bash"), + .input = try dupe(a, "{\"command\":\"ls\"}"), + } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "b2c3d4e5"), + .parent_id = try dupe(a, "a1b2c3d4"), + .timestamp = try dupe(a, "2026-04-25T17:40:17.000Z"), + }, + .stamp = .{ + .api_style = .anthropic_messages, + .base_url = try dupe(a, "https://api.anthropic.com"), + .model = try dupe(a, "claude-sonnet-4-20250514"), + }, + .message = .{ + .role = .assistant, + .content = content, + .stop_reason = try dupe(a, "toolUse"), + .usage = .{ .input = 1500, .output = 85 }, + .metadata = try dupe(a, "{\"k\":1}"), + }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + const got = fe.entry.message; + try testing.expectEqual(StoredMessageRole.assistant, got.message.role); + try testing.expectEqual(@as(usize, 3), got.message.content.len); + try testing.expectEqualStrings("let me think", got.message.content[0].thinking.thinking); + try testing.expectEqualStrings("sig-xyz", got.message.content[0].thinking.signature.?); + try testing.expectEqualStrings("bash", got.message.content[2].tool_use.name); + try testing.expectEqualStrings("{\"command\":\"ls\"}", got.message.content[2].tool_use.input); + try testing.expectEqualStrings("anthropic", @tagName(got.stamp.?.api_style)[0..9]); + try testing.expectEqualStrings("toolUse", got.message.stop_reason.?); + try testing.expectEqualStrings("{\"k\":1}", got.message.metadata.?); + try testing.expect(got.message.usage != null); + try testing.expectEqual(@as(u64, 1500), got.message.usage.?.input); + try testing.expectEqual(@as(u64, 85), got.message.usage.?.output); +} + +test "serialize/parse tool result message entry" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + var trp = try a.alloc(StoredResultPart, 1); + trp[0] = .{ .text = try dupe(a, "file1.txt\nfile2.txt") }; + content[0] = .{ .tool_result = .{ + .tool_use_id = try dupe(a, "tool_abc"), + .parts = trp, + } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "c3d4e5f6"), + .parent_id = try dupe(a, "b2c3d4e5"), + .timestamp = try dupe(a, "2026-04-25T17:40:18.000Z"), + }, + .stamp = .{ + .api_style = .anthropic_messages, + .base_url = try dupe(a, "https://api.anthropic.com"), + .model = try dupe(a, "claude-sonnet-4-20250514"), + }, + .message = .{ + .role = .user, + .content = content, + }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + const got = fe.entry.message; + try testing.expectEqual(StoredMessageRole.user, got.message.role); + try testing.expectEqualStrings("tool_abc", got.message.content[0].tool_result.tool_use_id); + try testing.expectEqual(@as(usize, 1), got.message.content[0].tool_result.parts.len); + try testing.expectEqualStrings("file1.txt\nfile2.txt", got.message.content[0].tool_result.parts[0].text); + try testing.expectEqual(APIStyle.anthropic_messages, got.stamp.?.api_style); + // Unset is_error defaults to false and serializes without the field. + try testing.expect(!got.message.content[0].tool_result.is_error); + try testing.expect(std.mem.indexOf(u8, line, "isError") == null); +} + +test "serialize/parse tool result preserves is_error = true" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + var trp = try a.alloc(StoredResultPart, 1); + trp[0] = .{ .text = try dupe(a, "file not found") }; + content[0] = .{ .tool_result = .{ + .tool_use_id = try dupe(a, "tool_err"), + .parts = trp, + .is_error = true, + } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "e1"), + .parent_id = try dupe(a, "e0"), + .timestamp = try dupe(a, "2026-04-25T17:40:18.000Z"), + }, + .stamp = .{ + .api_style = .anthropic_messages, + .base_url = try dupe(a, "https://api.anthropic.com"), + .model = try dupe(a, "claude-sonnet-4-20250514"), + }, + .message = .{ .role = .user, .content = content }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + try testing.expect(std.mem.indexOf(u8, line, "\"isError\":true") != null); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + try testing.expect(fe.entry.message.message.content[0].tool_result.is_error); +} + +test "parse tool result without isError defaults to false" { + const a = testing.allocator; + // A legacy line predating the is_error field. + const line = + \\{"type":"message","id":"x","parentId":"y","timestamp":"t","provider":"anthropic","model":"m","message":{"role":"user","content":[{"type":"toolResult","toolUseId":"t1","parts":[{"type":"text","text":"ok"}]}]}} + ; + var fe = try parseLine(a, line); + defer fe.deinit(a); + try testing.expect(!fe.entry.message.message.content[0].tool_result.is_error); +} + +test "serialize/parse tool result with text + image part round-trips" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + var trp = try a.alloc(StoredResultPart, 2); + trp[0] = .{ .text = try dupe(a, "here is the image") }; + trp[1] = .{ .media = .{ + .media_type = try dupe(a, "image/png"), + .data = try dupe(a, "iVBORw0KGgo="), + } }; + content[0] = .{ .tool_result = .{ + .tool_use_id = try dupe(a, "tool_img"), + .parts = trp, + } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "img00001"), + .parent_id = try dupe(a, "img00000"), + .timestamp = try dupe(a, "2026-04-25T17:40:18.000Z"), + }, + .stamp = .{ + .api_style = .anthropic_messages, + .base_url = try dupe(a, "https://api.anthropic.com"), + .model = try dupe(a, "claude-sonnet-4-20250514"), + }, + .message = .{ .role = .user, .content = content }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + const tr = fe.entry.message.message.content[0].tool_result; + try testing.expectEqualStrings("tool_img", tr.tool_use_id); + try testing.expectEqual(@as(usize, 2), tr.parts.len); + try testing.expectEqualStrings("here is the image", tr.parts[0].text); + try testing.expectEqualStrings("image/png", tr.parts[1].media.media_type); + try testing.expectEqualStrings("iVBORw0KGgo=", tr.parts[1].media.data); +} + +test "system message mode round-trips; absent mode defaults to append" { + const a = testing.allocator; + + // replace-mode system entry round-trips. + { + var content = try a.alloc(StoredContentBlock, 1); + content[0] = .{ .text = .{ .text = try dupe(a, "fresh seed") } }; + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "aabbccdd"), + .parent_id = null, + .timestamp = try dupe(a, "2026-04-25T17:40:00Z"), + }, + .message = .{ + .role = .system, + .content = content, + .mode = .replace, + }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + try testing.expect(std.mem.indexOf(u8, line, "\"mode\":\"replace\"") != null); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + try testing.expectEqual(StoredSystemMode.replace, fe.entry.message.message.mode); + } + + // A legacy system entry with no `mode` parses back as append. + { + const line = + \\{"type":"message","id":"abcdefab","parentId":null,"timestamp":"2026-04-25T17:40:00Z","message":{"role":"system","content":[{"type":"text","text":"hi"}]}} + ; + var fe = try parseLine(a, line); + defer fe.deinit(a); + try testing.expectEqual(StoredSystemMode.append, fe.entry.message.message.mode); + } +} + +test "parse: null parentId is handled" { + const a = testing.allocator; + const line = + \\{"type":"message","id":"abcdefab","parentId":null,"timestamp":"2026-04-25T17:40:00Z","message":{"role":"system","content":[{"type":"text","text":"hi"}]}} + ; + var fe = try parseLine(a, line); + defer fe.deinit(a); + try testing.expect(fe.entry.message.base.parent_id == null); +} + +test "parse: malformed JSON is reported" { + const a = testing.allocator; + try testing.expectError(error.InvalidJson, parseLine(a, "not json")); + try testing.expectError(error.InvalidJson, parseLine(a, "{\"type\":\"message\"")); +} + +test "parse: unknown entry type is reported" { + const a = testing.allocator; + const line = + \\{"type":"future_entry","id":"abcdefab","parentId":null,"timestamp":"2026-04-25T17:40:00Z"} + ; + try testing.expectError(error.UnknownType, parseLine(a, line)); +} + +test "contentBlockToDisk: Text round-trips via in-memory" { + const a = testing.allocator; + + var tb = try conversation.textualBlockFromSlice(a, "hello"); + defer tb.deinit(a); + const block: conversation.ContentBlock = .{ .Text = tb }; + + const disk = try contentBlockToDisk(a, block); + defer disk.deinit(a); + try testing.expectEqualStrings("hello", disk.text.text); +} + +test "diskContentBlockToInternal: ToolUse preserves id/name/input" { + const a = testing.allocator; + + const disk: StoredContentBlock = .{ .tool_use = .{ + .id = try a.dupe(u8, "tu_1"), + .name = try a.dupe(u8, "bash"), + .input = try a.dupe(u8, "{\"command\":\"ls\"}"), + } }; + defer disk.deinit(a); + + var inmem = try diskContentBlockToInternal(a, disk); + defer inmem.deinit(a); + try testing.expectEqualStrings("tu_1", inmem.ToolUse.id); + try testing.expectEqualStrings("bash", inmem.ToolUse.name); + try testing.expectEqualStrings("{\"command\":\"ls\"}", inmem.ToolUse.input.items); +} + +test "Usage: all five fields round-trip; zero-valued fields omitted from JSON" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + content[0] = .{ .text = .{ .text = try dupe(a, "hi") } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "deadbeef"), + .parent_id = null, + .timestamp = try dupe(a, "2026-04-25T17:40:17.000Z"), + }, + .message = .{ + .role = .assistant, + .content = content, + .stop_reason = try dupe(a, "stop"), + .usage = .{ + .input = 100, + .output = 50, + .cache_read = 800, + .cache_write = 200, + .reasoning = 30, + }, + }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + + // Every non-zero field should appear in the serialized JSON. + try testing.expect(std.mem.indexOf(u8, line, "\"input\":100") != null); + try testing.expect(std.mem.indexOf(u8, line, "\"output\":50") != null); + try testing.expect(std.mem.indexOf(u8, line, "\"cacheRead\":800") != null); + try testing.expect(std.mem.indexOf(u8, line, "\"cacheWrite\":200") != null); + try testing.expect(std.mem.indexOf(u8, line, "\"reasoning\":30") != null); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + const u = fe.entry.message.message.usage.?; + try testing.expectEqual(@as(u64, 100), u.input); + try testing.expectEqual(@as(u64, 50), u.output); + try testing.expectEqual(@as(u64, 800), u.cache_read); + try testing.expectEqual(@as(u64, 200), u.cache_write); + try testing.expectEqual(@as(u64, 30), u.reasoning); +} + +test "Usage: zero-valued auxiliary fields are omitted but parse back as 0" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + content[0] = .{ .text = .{ .text = try dupe(a, "hi") } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "deadbeef"), + .parent_id = null, + .timestamp = try dupe(a, "2026-04-25T17:40:17.000Z"), + }, + .message = .{ + .role = .assistant, + .content = content, + .usage = .{ .input = 100, .output = 50 }, + }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + + try testing.expect(std.mem.indexOf(u8, line, "cacheRead") == null); + try testing.expect(std.mem.indexOf(u8, line, "cacheWrite") == null); + try testing.expect(std.mem.indexOf(u8, line, "reasoning") == null); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + const u = fe.entry.message.message.usage.?; + try testing.expectEqual(@as(u64, 0), u.cache_read); + try testing.expectEqual(@as(u64, 0), u.cache_write); + try testing.expectEqual(@as(u64, 0), u.reasoning); +} + +test "diskContentBlockToInternal: Thinking preserves signature" { + const a = testing.allocator; + + const disk: StoredContentBlock = .{ .thinking = .{ + .thinking = try a.dupe(u8, "reasoning..."), + .signature = try a.dupe(u8, "sig123"), + } }; + defer disk.deinit(a); + + var inmem = try diskContentBlockToInternal(a, disk); + defer inmem.deinit(a); + try testing.expectEqualStrings("reasoning...", inmem.Thinking.text.items); + try testing.expectEqualStrings("sig123", inmem.Thinking.signature.?); +} + +test "compactionSummary block round-trips through serialize/parse" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + content[0] = .{ .compaction_summary = .{ .text = try dupe(a, "earlier history summary") } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "cafef00d"), + .parent_id = null, + .timestamp = try dupe(a, "2026-04-25T17:40:00Z"), + }, + .message = .{ .role = .user, .content = content }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + try testing.expect(std.mem.indexOf(u8, line, "\"type\":\"compactionSummary\"") != null); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + const got = fe.entry.message; + try testing.expectEqual(StoredMessageRole.user, got.message.role); + try testing.expectEqualStrings("earlier history summary", got.message.content[0].compaction_summary.text); +} + +test "compactionSummary bridges in-memory <-> disk both directions" { + const a = testing.allocator; + + // in-memory -> disk + const tb = try conversation.textualBlockFromSlice(a, "S1"); + const block: conversation.ContentBlock = .{ .CompactionSummary = .{ .text = tb } }; + defer { + var mut = block; + mut.deinit(a); + } + const disk = try contentBlockToDisk(a, block); + defer disk.deinit(a); + try testing.expectEqualStrings("S1", disk.compaction_summary.text); + + // disk -> in-memory + var inmem = try diskContentBlockToInternal(a, disk); + defer inmem.deinit(a); + try testing.expectEqualStrings("S1", inmem.CompactionSummary.text.items); +} + +test "WireStamp: Anthropic non-default thinking fields round-trip" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + content[0] = .{ .text = .{ .text = try dupe(a, "hi") } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "aa000001"), + .parent_id = null, + .timestamp = try dupe(a, "2026-06-01T00:00:00Z"), + }, + .stamp = .{ + .api_style = .anthropic_messages, + .base_url = try dupe(a, "https://api.anthropic.com"), + .model = try dupe(a, "claude-opus-4-8"), + .thinking = .adaptive, + .effort = .high, + .thinking_budget_tokens = null, + .thinking_interleaved = true, + }, + .message = .{ .role = .user, .content = content }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + + // Non-default fields must appear in the serialized line. + try testing.expect(std.mem.indexOf(u8, line, "\"thinking\":\"adaptive\"") != null); + try testing.expect(std.mem.indexOf(u8, line, "\"effort\":\"high\"") != null); + try testing.expect(std.mem.indexOf(u8, line, "\"thinkingBudgetTokens\":null") != null); + try testing.expect(std.mem.indexOf(u8, line, "\"thinkingInterleaved\":true") != null); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + const got = fe.entry.message.stamp.?; + try testing.expectEqual(APIStyle.anthropic_messages, got.api_style); + try testing.expectEqual(Thinking.adaptive, got.thinking); + try testing.expectEqual(Effort.high, got.effort); + try testing.expectEqual(@as(?u32, null), got.thinking_budget_tokens); + try testing.expectEqual(true, got.thinking_interleaved); + // reasoning carries its default (unused for Anthropic) + try testing.expectEqual(ReasoningEffort.default, got.reasoning); +} + +test "WireStamp: Anthropic stamp with all-default thinking fields omits non-essential keys" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + content[0] = .{ .text = .{ .text = try dupe(a, "hi") } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "bb000002"), + .parent_id = null, + .timestamp = try dupe(a, "2026-06-01T00:00:00Z"), + }, + .stamp = .{ + .api_style = .anthropic_messages, + .base_url = try dupe(a, "https://api.anthropic.com"), + .model = try dupe(a, "claude-haiku-4-5"), + // All defaults: thinking=.enabled, effort=.medium, + // thinking_budget_tokens=32_000, thinking_interleaved=false + }, + .message = .{ .role = .user, .content = content }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + + // Default-valued fields should be omitted (keeps logs compact). + try testing.expect(std.mem.indexOf(u8, line, "thinking") == null); + try testing.expect(std.mem.indexOf(u8, line, "effort") == null); + try testing.expect(std.mem.indexOf(u8, line, "thinkingInterleaved") == null); + // thinkingBudgetTokens=32_000 is the default, should be omitted too. + try testing.expect(std.mem.indexOf(u8, line, "thinkingBudgetTokens") == null); + + // Round-trip: all defaults parse back correctly. + var fe = try parseLine(a, line); + defer fe.deinit(a); + const got = fe.entry.message.stamp.?; + try testing.expectEqual(Thinking.enabled, got.thinking); + try testing.expectEqual(Effort.medium, got.effort); + try testing.expectEqual(@as(?u32, 32_000), got.thinking_budget_tokens); + try testing.expectEqual(false, got.thinking_interleaved); +} + +test "WireStamp: legacy Anthropic stamp (no thinking fields) parses with defaults" { + // Simulate a session log written before thinking fields were added. + const a = testing.allocator; + const line = + \\{"type":"message","id":"cc000003","parentId":null,"timestamp":"2026-06-01T00:00:00Z","apiStyle":"anthropic_messages","baseUrl":"https://api.anthropic.com","model":"claude-3-7-sonnet","message":{"role":"user","content":[{"type":"text","text":"hi"}]}} + ; + var fe = try parseLine(a, line); + defer fe.deinit(a); + const got = fe.entry.message.stamp.?; + try testing.expectEqual(APIStyle.anthropic_messages, got.api_style); + try testing.expectEqual(Thinking.enabled, got.thinking); + try testing.expectEqual(Effort.medium, got.effort); + try testing.expectEqual(@as(?u32, 32_000), got.thinking_budget_tokens); + try testing.expectEqual(false, got.thinking_interleaved); +} + +test "WireStamp: OpenAI stamp is unchanged by Anthropic fields" { + const a = testing.allocator; + + var content = try a.alloc(StoredContentBlock, 1); + content[0] = .{ .text = .{ .text = try dupe(a, "hi") } }; + + const entry: SessionEntry = .{ .message = .{ + .base = .{ + .id = try dupe(a, "dd000004"), + .parent_id = null, + .timestamp = try dupe(a, "2026-06-01T00:00:00Z"), + }, + .stamp = .{ + .api_style = .openai_chat, + .base_url = try dupe(a, "https://api.openai.com/v1"), + .model = try dupe(a, "gpt-4o"), + .reasoning = .high, + }, + .message = .{ .role = .user, .content = content }, + } }; + defer entry.deinit(a); + + const line = try serializeEntry(a, entry); + defer a.free(line); + + // Anthropic fields should not appear for an OpenAI stamp. + try testing.expect(std.mem.indexOf(u8, line, "thinking") == null); + try testing.expect(std.mem.indexOf(u8, line, "effort") == null); + try testing.expect(std.mem.indexOf(u8, line, "thinkingBudget") == null); + try testing.expect(std.mem.indexOf(u8, line, "thinkingInterleaved") == null); + // reasoning=high should be present + try testing.expect(std.mem.indexOf(u8, line, "\"reasoning\":\"high\"") != null); + + var fe = try parseLine(a, line); + defer fe.deinit(a); + const got = fe.entry.message.stamp.?; + try testing.expectEqual(APIStyle.openai_chat, got.api_style); + try testing.expectEqual(ReasoningEffort.high, got.reasoning); +} diff --git a/src/session_store.zig b/src/session_store.zig new file mode 100644 index 0000000..de25e21 --- /dev/null +++ b/src/session_store.zig @@ -0,0 +1,188 @@ +//! `SessionStore`: the neutral persistence seam for the `Agent`. +//! +//! The interface is **asymmetric**: rich on write (audit/provenance-capable), +//! minimal on read (resume-oriented). The store decides how much write-side +//! richness it durably keeps. +//! +//! ## Write side (maximalist) +//! +//! `appendMessages` takes `[]PersistentMessage` — the rich, audit-oriented +//! write record. Each carries the in-memory `Message` being appended, its +//! `usage`, the **wire-format** provider identity (`api_style`, `base_url`, +//! `model`, `reasoning` — never CLI config aliases, and never any `api_key` +//! material, not even a hash), and full provenance context (the entire +//! current conversation and the tool set offered for this turn). The library +//! *offers* all of it on every append; a store keeps what it wants. The +//! built-in `FileSystemJSONLStore` deliberately ignores the `conversation` +//! and `tools_available` provenance fields. +//! +//! ## Read side (minimal) +//! +//! `load` reconstructs one linear `Conversation`; `list`/`resolve`/`latest` +//! traffic in `SessionInfo` (display/selection metadata) and `Session` +//! (an `info` + a store to proxy to). The read path never reproduces a +//! `PersistentMessage` — a store may not have kept the provenance. +//! +//! ## Store construction +//! +//! Stores own their own (unprescribed) `init`: a Postgres store takes a DSN, +//! the FS store takes a directory. Nothing in the vtable carries an +//! allocator or io — the store captured whatever it needs at its own init. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const session_mod = @import("session.zig"); +const conversation_mod = @import("conversation.zig"); +const config_mod = @import("config.zig"); +const tool_source_mod = @import("tool_source.zig"); + +pub const Conversation = conversation_mod.Conversation; +pub const Message = conversation_mod.Message; +pub const Usage = conversation_mod.Usage; +pub const APIStyle = config_mod.APIStyle; +pub const ReasoningEffort = config_mod.ReasoningEffort; +pub const ToolDecl = tool_source_mod.ToolDecl; + +/// The default filesystem-JSONL backend, re-exported under its +/// interface-facing name. Its concrete constructor (`init`/`open`) and +/// catalog helpers are backend-specific and stay on that module. +pub const FileSystemJSONLStore = @import("file_system_jsonl_store.zig").FileSystemJSONLStore; + +/// Wire-format provider identity. This is the **ground truth** of which +/// endpoint a turn was sent to — never a CLI config alias (aliases get +/// renamed; two keys for one endpoint are indistinguishable on the wire). +/// `reasoning` disambiguates otherwise-identical endpoints. No `api_key` +/// material ever appears here. Aliased from `config` to avoid a module +/// cycle (config must not import session_store). +pub const WireIdentity = config_mod.WireIdentity; + +/// The rich, audit-oriented write record. The library offers all of this on +/// every append; the store keeps what it wants. +pub const PersistentMessage = struct { + /// The in-memory message being appended (carries its own `metadata`). + message: Message, + /// Provider usage for this message (assistant turns), or null. + usage: ?Usage = null, + /// Wire-format provider identity for the turn this message belongs to. + identity: WireIdentity, + /// Full provenance: the entire current conversation at write time. The + /// FS store ignores this; an audit store may content-address it. + conversation: []const Message = &.{}, + /// Full provenance: the tool set offered for this turn. The FS store + /// ignores this; an audit store may content-address it. + tools_available: []const ToolDecl = &.{}, +}; + +/// Display/selection metadata for one session — pure data, aliased. Used by +/// `panto sessions` and resume pre-selection. The last-used wire identity is +/// updated on append (for resume), never a CLI config alias. +pub const SessionInfo = struct { + id: []const u8, + created: []const u8, + modified: []const u8, + message_count: usize, + /// May be truncated. + last_user_message: []const u8, + /// Last-used wire identity, updated on append. + api_style: APIStyle, + base_url: []const u8, + model: []const u8, + reasoning: ReasoningEffort, + + pub fn deinit(self: SessionInfo, alloc: Allocator) void { + alloc.free(self.id); + alloc.free(self.created); + alloc.free(self.modified); + alloc.free(self.last_user_message); + alloc.free(self.base_url); + alloc.free(self.model); + } +}; + +/// A session handle: pure data (a `SessionInfo`) plus a store to proxy to. +pub const Session = struct { + info: SessionInfo, + store: SessionStore, + + /// Reconstruct the conversation. The id came from `resolve`/`latest`, so + /// the conversation must exist; a `null` from the store is promoted to + /// an error. + pub fn load(self: Session) !Conversation { + return (try self.store.load(self.info.id)) orelse error.SessionNotFound; + } + + /// Append a batch of messages, proxying to the store. Takes `*Session` + /// for API symmetry and to allow future in-place `info` updates; today + /// it only updates the non-owning `api_style`/`reasoning` last-used + /// fields (the `base_url`/`model` strings stay the owned originals to + /// avoid aliasing borrowed config memory — resume picks the default + /// model rather than matching the stored wire identity, so the stale + /// display strings are harmless). + pub fn append(self: *Session, messages: []PersistentMessage) !void { + try self.store.appendMessages(self.info.id, messages); + if (messages.len > 0) { + const id = messages[messages.len - 1].identity; + self.info.api_style = id.api_style; + self.info.reasoning = id.reasoning; + } + } +}; + +/// A pluggable session-persistence backend. +pub const SessionStore = struct { + ptr: *anyopaque, + vtable: *const VTable, + + pub const VTable = struct { + /// Mint an in-memory session handle. Cannot fail: nothing hits the + /// backend until the first `appendMessages` (create-on-demand), so + /// no record exists before the first assistant message. + create: *const fn (ctx: *anyopaque) Session, + + /// List known sessions, newest first. Caller frees via + /// `freeSessionInfos`. + list: *const fn (ctx: *anyopaque) anyerror![]SessionInfo, + + /// Free a slice returned by `list`. + freeSessionInfos: *const fn (ctx: *anyopaque, infos: []SessionInfo) void, + + /// Resolve a (possibly abbreviated) id to a session, or null if no + /// match. + resolve: *const fn (ctx: *anyopaque, id: []const u8) anyerror!?Session, + + /// The most recent session, or null if none exist. + latest: *const fn (ctx: *anyopaque) anyerror!?Session, + + /// Reconstruct one linear `Conversation` for `id`, or null if absent. + /// The returned `Conversation` self-describes its allocator. + load: *const fn (ctx: *anyopaque, id: []const u8) anyerror!?Conversation, + + /// Append a batch atomically. A single append is a length-1 batch. + /// The store reads what it wants off each `PersistentMessage` and + /// is responsible for any de-duplication of provenance. + appendMessages: *const fn (ctx: *anyopaque, session_id: []const u8, messages: []PersistentMessage) anyerror!void, + }; + + pub fn create(self: SessionStore) Session { + return self.vtable.create(self.ptr); + } + pub fn list(self: SessionStore) ![]SessionInfo { + return self.vtable.list(self.ptr); + } + pub fn freeSessionInfos(self: SessionStore, infos: []SessionInfo) void { + self.vtable.freeSessionInfos(self.ptr, infos); + } + pub fn resolve(self: SessionStore, id: []const u8) !?Session { + return self.vtable.resolve(self.ptr, id); + } + pub fn latest(self: SessionStore) !?Session { + return self.vtable.latest(self.ptr); + } + pub fn load(self: SessionStore, id: []const u8) !?Conversation { + return self.vtable.load(self.ptr, id); + } + pub fn appendMessages(self: SessionStore, session_id: []const u8, messages: []PersistentMessage) !void { + return self.vtable.appendMessages(self.ptr, session_id, messages); + } +}; diff --git a/src/sse.zig b/src/sse.zig new file mode 100644 index 0000000..c54d532 --- /dev/null +++ b/src/sse.zig @@ -0,0 +1,170 @@ +const std = @import("std"); + +/// A parsed SSE event payload: the concatenated `data:` content. +/// Owned by the caller; free with `freeEvents`. +const Event = []u8; + +/// Incremental SSE line parser. +/// +/// The HTTP client delivers arbitrary-sized read buffers; this module +/// reassembles them into complete `data: ...\n\n` events and returns +/// owned event payload slices. +pub const SSEParser = struct { + buf: std.ArrayList(u8) = .empty, + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) SSEParser { + return .{ .allocator = allocator }; + } + + /// Feed a chunk of raw bytes. Returns a list of complete SSE events + /// found in the buffer (may be empty). The caller owns the returned + /// slice and every event within; free with `freeEvents`. + pub fn feed(self: *SSEParser, chunk: []const u8) ![]Event { + try self.buf.appendSlice(self.allocator, chunk); + + var events: std.ArrayList(Event) = .empty; + errdefer { + for (events.items) |ev| self.allocator.free(ev); + events.deinit(self.allocator); + } + + var scan_pos: usize = 0; + while (std.mem.indexOf(u8, self.buf.items[scan_pos..], "\n\n")) |rel_delim| { + const delim = scan_pos + rel_delim; + const event_bytes = self.buf.items[scan_pos..delim]; + + // Collect data: lines into an owned payload string. + var payload: std.ArrayList(u8) = .empty; + errdefer payload.deinit(self.allocator); + + var line_start: usize = 0; + var has_data = false; + while (line_start < event_bytes.len) { + const newline = std.mem.indexOfScalarPos(u8, event_bytes, line_start, '\n') orelse event_bytes.len; + const line = event_bytes[line_start..newline]; + + if (std.mem.startsWith(u8, line, "data: ")) { + if (has_data) try payload.append(self.allocator, '\n'); + try payload.appendSlice(self.allocator, line["data: ".len..]); + has_data = true; + } + // Ignore other SSE fields (event:, id:, retry:) and comments. + + line_start = newline + 1; + } + + if (has_data) { + try events.append(self.allocator, try payload.toOwnedSlice(self.allocator)); + } else { + payload.deinit(self.allocator); + } + + scan_pos = delim + 2; + } + + // Drop processed bytes; retain the buffer allocation. + const remaining_len = self.buf.items.len - scan_pos; + if (remaining_len > 0 and scan_pos > 0) { + std.mem.copyForwards(u8, self.buf.items[0..remaining_len], self.buf.items[scan_pos..]); + } + self.buf.shrinkRetainingCapacity(remaining_len); + + return events.toOwnedSlice(self.allocator); + } + + /// Free a list of events returned by `feed`. + pub fn freeEvents(self: *SSEParser, events: []Event) void { + for (events) |ev| self.allocator.free(ev); + self.allocator.free(events); + } + + pub fn deinit(self: *SSEParser) void { + self.buf.deinit(self.allocator); + } +}; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const testing = std.testing; + +test "SSEParser - single complete event" { + var parser = SSEParser.init(testing.allocator); + defer parser.deinit(); + + const events = try parser.feed("data: hello\n\n"); + defer parser.freeEvents(events); + + try testing.expectEqual(@as(usize, 1), events.len); + try testing.expectEqualStrings("hello", events[0]); +} + +test "SSEParser - partial then complete" { + var parser = SSEParser.init(testing.allocator); + defer parser.deinit(); + + const events1 = try parser.feed("data: hel"); + defer parser.freeEvents(events1); + try testing.expectEqual(@as(usize, 0), events1.len); + + const events2 = try parser.feed("lo\n\n"); + defer parser.freeEvents(events2); + try testing.expectEqual(@as(usize, 1), events2.len); + try testing.expectEqualStrings("hello", events2[0]); +} + +test "SSEParser - multiple events in single chunk" { + var parser = SSEParser.init(testing.allocator); + defer parser.deinit(); + + const events = try parser.feed("data: one\n\ndata: two\n\n"); + defer parser.freeEvents(events); + + try testing.expectEqual(@as(usize, 2), events.len); + try testing.expectEqualStrings("one", events[0]); + try testing.expectEqualStrings("two", events[1]); +} + +test "SSEParser - data DONE signal" { + var parser = SSEParser.init(testing.allocator); + defer parser.deinit(); + + const events = try parser.feed("data: [DONE]\n\n"); + defer parser.freeEvents(events); + + try testing.expectEqual(@as(usize, 1), events.len); + try testing.expectEqualStrings("[DONE]", events[0]); +} + +test "SSEParser - empty event (keep-alive)" { + var parser = SSEParser.init(testing.allocator); + defer parser.deinit(); + + const events = try parser.feed("\n\n"); + defer parser.freeEvents(events); + try testing.expectEqual(@as(usize, 0), events.len); +} + +test "SSEParser - ignores non-data fields" { + var parser = SSEParser.init(testing.allocator); + defer parser.deinit(); + + const events = try parser.feed("event: message\ndata: payload\nid: 42\n\n"); + defer parser.freeEvents(events); + + try testing.expectEqual(@as(usize, 1), events.len); + try testing.expectEqualStrings("payload", events[0]); +} + +test "SSEParser - multi-line data joined with newline" { + var parser = SSEParser.init(testing.allocator); + defer parser.deinit(); + + const events = try parser.feed("data: line1\ndata: line2\n\n"); + defer parser.freeEvents(events); + + try testing.expectEqual(@as(usize, 1), events.len); + try testing.expectEqualStrings("line1\nline2", events[0]); +} diff --git a/src/stream.zig b/src/stream.zig new file mode 100644 index 0000000..64748b2 --- /dev/null +++ b/src/stream.zig @@ -0,0 +1,179 @@ +//! Pull-based streaming surface for `libpanto`. +//! +//! This is the spine of the language-bindings work (see +//! `docs/libpanto-bindings.md` and `docs/phase0-pull-stream-design.md`). +//! Instead of pushing events at a `Receiver` vtable, the agent loop is +//! inverted into a resumable `Stream` whose `next()` *pulls* one `Event` +//! at a time. Pull is the more primitive primitive: push composes trivially +//! on top of it, and it maps 1:1 onto Go range-over-func iterators and +//! Python generators. +//! +//! Contract (the terminal-event invariant): +//! +//! - `Event` (a value) -> streaming progress, including `turn_complete`. +//! - `null` -> the stream is exhausted (already past the +//! terminal `turn_complete`). Never returned +//! before `turn_complete`. +//! - `error.X` -> a genuine failure (network, parse, provider). +//! +//! Event payloads borrow from state owned by the stream or the +//! conversation. **An `Event` is valid only until the next `next()` call.** +//! Consumers that need to retain data copy it out before advancing. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const conversation = @import("conversation.zig"); +const provider_mod = @import("provider.zig"); +const session_mod = @import("session.zig"); + +pub const ContentBlockType = provider_mod.ContentBlockType; +pub const Usage = session_mod.Usage; +pub const ProviderRetryInfo = provider_mod.ProviderRetryInfo; + +/// The single success-only event type every binding marshals. Mirrors the +/// former `ReceiverVTable` callbacks plus the agent's tool-dispatch +/// boundaries. Provider failures are NOT a variant here — they surface as +/// the `!` in `Stream.next() !?Event`. +pub const Event = union(enum) { + /// An assistant message began streaming. + message_start: conversation.MessageRole, + + /// A content block opened. `index` is the receiver-facing block index. + block_start: BlockStart, + + /// Tool identity resolved for a ToolUse block (id + name both known). + /// Fires once per ToolUse block, after its `block_start` and before its + /// `block_complete`. Borrowed slices; valid until the next `next()`. + tool_details: ToolDetails, + + /// Streaming content for the open block (text, thinking, or tool args). + /// `delta` is borrowed; valid until the next `next()`. + content_delta: ContentDelta, + + /// A content block closed. `block` is borrowed from the message under + /// construction; valid until the next `next()`. + block_complete: BlockComplete, + + /// One assistant message finished streaming (one provider response). + /// In a tool-using turn this fires once per assistant message, not once + /// per turn — `turn_complete` is the turn-level terminal. `message` is + /// borrowed from the conversation. + message_complete: MessageComplete, + + /// Provider retry scheduled before the agent sleeps for the next + /// attempt. Purely informational; simple consumers ignore it. + provider_retry: ProviderRetryInfo, + + /// The agent began dispatching the tool calls in the just-completed + /// assistant message. Marks the boundary between a provider stream and + /// concurrent tool execution. + tool_dispatch_start: ToolDispatchStart, + + /// One tool result is available. The payload is a user-role carrier + /// containing exactly one `ToolResult` block, keyed by `tool_use_id`. + /// This may arrive before the aggregate `tool_dispatch_complete` event. + tool_dispatch_result: ToolDispatchComplete, + + /// The agent finished dispatching tools and appended a user(ToolResult) + /// message to the conversation. `message` is borrowed. + tool_dispatch_complete: ToolDispatchComplete, + + /// The turn terminal: the model stopped calling tools and the turn is + /// done. Emitted exactly once, after the final `message_complete` and + /// any tool dispatch. Every `next()` after this returns `null`. + turn_complete, + + pub const BlockStart = struct { + block_type: ContentBlockType, + index: usize, + }; + pub const ToolDetails = struct { + index: usize, + id: []const u8, + name: []const u8, + }; + pub const ContentDelta = struct { + index: usize, + delta: []const u8, + }; + pub const BlockComplete = struct { + index: usize, + block: conversation.ContentBlock, + }; + pub const MessageComplete = struct { + message: conversation.Message, + usage: ?Usage, + }; + pub const ToolDispatchStart = struct { + count: usize, + }; + pub const ToolDispatchComplete = struct { + message: conversation.Message, + }; +}; + +/// A small FIFO of decoded-but-not-yet-yielded events. One `parser.feed()` +/// can yield several SSE events, each of which can produce several `Event`s; +/// the provider decode step appends them here and `Stream.next()` drains +/// the queue before pulling more bytes. +/// +/// Transient byte payloads (delta text, tool id/name) are duped into a +/// queue-owned arena via `dupeBytes`, so they survive the provider freeing +/// its SSE/JSON scratch and any reallocation of the provider's accumulation +/// buffers. The arena (and the event list) are reset when the queue fully +/// drains, so memory is bounded by the events produced from a single byte +/// chunk. Events whose payloads live in the conversation (`block_complete`, +/// `message_complete`, `tool_dispatch_complete`) borrow directly and are +/// not duped — the conversation outlives the `next()` step. +pub const EventQueue = struct { + items: std.ArrayList(Event) = .empty, + head: usize = 0, + arena: std.heap.ArenaAllocator, + allocator: Allocator, + + pub fn init(allocator: Allocator) EventQueue { + return .{ + .allocator = allocator, + .arena = std.heap.ArenaAllocator.init(allocator), + }; + } + + pub fn deinit(self: *EventQueue) void { + self.items.deinit(self.allocator); + self.arena.deinit(); + } + + pub fn push(self: *EventQueue, ev: Event) !void { + try self.items.append(self.allocator, ev); + } + + /// Copy transient bytes into the queue's arena. The returned slice is + /// valid until the queue next fully drains (i.e. until the consumer has + /// pulled every queued event). Providers MUST route any byte payload + /// borrowed from SSE/JSON scratch or a reallocating buffer through here + /// before queueing it on an event. + pub fn dupeBytes(self: *EventQueue, bytes: []const u8) ![]const u8 { + return self.arena.allocator().dupe(u8, bytes); + } + + pub fn isEmpty(self: *const EventQueue) bool { + return self.head >= self.items.items.len; + } + + /// Pop the next event, or null if empty. Resets the backing list and + /// arena when drained so they can be refilled for the next chunk without + /// unbounded growth. + pub fn pop(self: *EventQueue) ?Event { + if (self.head >= self.items.items.len) { + // Drained: reset list + arena to reuse for the next chunk. + self.items.clearRetainingCapacity(); + self.head = 0; + _ = self.arena.reset(.retain_capacity); + return null; + } + const ev = self.items.items[self.head]; + self.head += 1; + return ev; + } +}; diff --git a/src/tool.zig b/src/tool.zig new file mode 100644 index 0000000..c96dfee --- /dev/null +++ b/src/tool.zig @@ -0,0 +1,149 @@ +//! Native tool extension API. +//! +//! A `Tool` is the boundary between the agent loop and any extension runtime +//! — native Zig code, a Lua bridge, a future Python or Go bridge. libpanto +//! itself does not parse tool inputs or outputs; it just dispatches. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// Tool metadata: everything the LLM-facing wire needs (name, +/// description, schema) without an invocation vtable. This is the more +/// atomic type, so it lives here; `tool_source.zig` imports it. +pub const ToolDecl = struct { + name: []const u8, + description: []const u8, + schema_json: []const u8, +}; + +/// A binary attachment a tool may return alongside (or instead of) text: +/// an image or a document (PDF). +/// +/// `data` is the **raw, un-encoded file bytes** — tools do no encoding. +/// libpanto owns the heavy lifting at tool-result assembly: it +/// magic-byte-detects the type when `media_type` is null, resizes large +/// rasters, and base64-encodes for storage/serialization. +pub const MediaPart = struct { + /// Optional MIME hint, e.g. "image/png". When null, libpanto detects + /// the type from `data`'s leading bytes (magic numbers). + media_type: ?[]const u8 = null, + /// Raw (un-encoded) file bytes. + data: []const u8, +}; + +/// One element of a tool's result. A tool returns a `ResultParts` (a thin +/// wrapper around `[]ResultPart`); the agent assembles these into a +/// `ToolResultBlock`. Bytes referenced by a part are owned by the allocator +/// passed to `invoke` / `invoke_batch`; ownership transfers to the agent, +/// which frees them. +pub const ResultPart = union(enum) { + text: []const u8, + media: MediaPart, + + /// Free the bytes this part owns, using `allocator`. + pub fn deinit(self: ResultPart, allocator: Allocator) void { + switch (self) { + .text => |t| allocator.free(t), + .media => |m| { + if (m.media_type) |mt| allocator.free(mt); + allocator.free(m.data); + }, + } + } +}; + +/// A tool's full result: an owned slice of `ResultPart`s. The value the +/// `Tool`/`ToolSource` vtable returns and the agent loop assembles — a thin +/// wrapper around `[]ResultPart` that carries the construction/teardown +/// ergonomics a bare slice alias can't. Build one with +/// `fromText`/`fromTextOwned` (or wrap a hand-built slice as +/// `.{ .items = slice }`); release it (slice + every part's bytes) with +/// `deinit`. +pub const ResultParts = struct { + items: []ResultPart, + + /// A single text part that owns `text` (duped from the input slice). + pub fn fromText(allocator: Allocator, text: []const u8) !ResultParts { + const owned = try allocator.dupe(u8, text); + errdefer allocator.free(owned); + const parts = try allocator.alloc(ResultPart, 1); + parts[0] = .{ .text = owned }; + return .{ .items = parts }; + } + + /// A single text part wrapping an already-owned `text` slice. Takes + /// ownership of `text` (frees it if the allocation below fails). + pub fn fromTextOwned(allocator: Allocator, text: []u8) !ResultParts { + const parts = allocator.alloc(ResultPart, 1) catch |e| { + allocator.free(text); + return e; + }; + parts[0] = .{ .text = text }; + return .{ .items = parts }; + } + + /// Free the slice and every part it owns. + pub fn deinit(self: ResultParts, allocator: Allocator) void { + for (self.items) |p| p.deinit(allocator); + allocator.free(self.items); + } +}; + +pub const Tool = struct { + /// Metadata: `name`, `description`, `schema_json`. Borrowed — the + /// lifetime of every string in `decl` is owned by whoever + /// constructs the `Tool`. Typically the same owner that backs + /// `ctx` (e.g. an adapter for an out-of-process runtime, or a + /// `comptime` static in a native tool). + decl: ToolDecl, + + /// Opaque context pointer passed back to every vtable call. + ctx: *anyopaque, + + vtable: *const VTable, + + pub const VTable = struct { + /// Invoke the tool. MUST be thread-safe — the agent may call + /// `invoke` concurrently from multiple threads when the LLM emits + /// multiple ToolUse blocks in a single response. + /// + /// `input` is the raw JSON bytes the provider sent. The tool is + /// responsible for parsing them if it cares about their structure. + /// + /// Returns a `ResultParts` allocated with `allocator`; each part's + /// bytes are likewise owned. These become the parts of the + /// ToolResult block sent back to the LLM. The agent takes ownership + /// and frees the slice and every part (see `ResultParts.deinit`). + /// Build the return value with `ResultParts.fromText` / + /// `.fromTextOwned` for the common single-text case, or wrap a + /// hand-built slice as `.{ .items = slice }`. + /// + /// Returning an error normally becomes a model-visible error + /// `ToolResult`: the agent synthesizes an error result for this + /// call (and keeps the matching `ToolResult` for every other call + /// in the batch), then lets the model continue so it can correct + /// arguments, try another tool, or explain the failure. Only hard + /// host failures (`error.Canceled`, `error.OutOfMemory`) abort the + /// whole turn and propagate to the embedder. + /// + /// Native tool implementations are responsible for catching their + /// own panics — a panic in `invoke` will crash the process. + /// Adapters that bridge to safer languages (Lua, Python, Go) should + /// convert panics/exceptions into errors. + invoke: *const fn ( + ctx: *anyopaque, + input: []const u8, + allocator: Allocator, + ) anyerror!ResultParts, + + /// Called when the tool is unregistered or the registry is torn + /// down. Frees any resources owned by `ctx`, including `ctx` + /// itself if it was heap-allocated. + /// + /// The strings inside `decl` are also typically owned by the + /// same allocation as `ctx` — the tool's deinit hook is + /// responsible for freeing them. + deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void, + }; +}; + diff --git a/src/tool_registry.zig b/src/tool_registry.zig new file mode 100644 index 0000000..430cda5 --- /dev/null +++ b/src/tool_registry.zig @@ -0,0 +1,663 @@ +//! Registry of tools owned by an `Agent`. +//! +//! Two kinds of registration coexist: +//! +//! - A single `Tool`: a thread-safe, self-contained handler. The +//! registry holds one entry keyed by `tool.decl.name`. +//! - A `ToolSource`: a batch-dispatched runtime that owns many tools. +//! The registry holds one entry per declared tool, all pointing back +//! at the same source (different `tool_index` per entry). +//! +//! Iteration yields the per-tool metadata as a uniform `ToolView` so +//! callers (chiefly: provider request serializers) don't need to know +//! which flavor of registration each tool came from. +//! +//! Iteration is not synchronized — callers must avoid mutating the +//! registry during iteration. In the current agent loop this is naturally +//! true: the provider iterates once at request-build time, and tool +//! registration only happens at agent setup. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const tool_mod = @import("tool.zig"); +const tool_source_mod = @import("tool_source.zig"); + +const Tool = tool_mod.Tool; +const ToolSource = tool_source_mod.ToolSource; +const ToolDecl = tool_source_mod.ToolDecl; + +// =========================================================================== +// Wire-name encoding +// =========================================================================== +// +// Internally, tool names use dots for namespacing (`std.read`), which our +// glob-based allow/deny policies rely on. But the OpenAI and Anthropic +// tool-name grammars forbid dots: both require `^[a-zA-Z0-9_-]{1,128}$`. +// +// So names are translated at the wire boundary only: `.` <-> `__`. The +// mapping is a clean bijection because a literal `__` is forbidden in an +// internal name (enforced by `validateName` at registration). Everything +// inside libpanto keeps speaking dots; only the serializers (via +// `toolsForLLM`) and inbound dispatch (via `lookupLLM`) cross the boundary. + +/// The largest a wire (LLM-facing) tool name may be, per the provider +/// grammars. We validate the *encoded* length against this so an encoded +/// name is always acceptable to both providers. +pub const max_wire_name_len = 128; + +pub const NameError = error{ + /// Name is empty or its encoded form exceeds `max_wire_name_len`. + NameTooLong, + /// Name contains a literal `__` (reserved as the encoded form of `.`) + /// or a character outside `[a-zA-Z0-9_.-]`. + InvalidNameChar, +}; + +/// Validate an internal tool name. Permits `[a-zA-Z0-9_.-]` but forbids a +/// literal `__` (which would collide with an encoded `.`), and requires +/// the encoded form to be 1..=`max_wire_name_len` bytes. Each `.` expands +/// to two bytes when encoded, so the cap is checked against that. +pub fn validateName(name: []const u8) NameError!void { + if (name.len == 0) return error.NameTooLong; + var encoded_len: usize = 0; + for (name, 0..) |ch, i| { + const ok = (ch >= 'a' and ch <= 'z') or (ch >= 'A' and ch <= 'Z') or + (ch >= '0' and ch <= '9') or ch == '_' or ch == '-' or ch == '.'; + if (!ok) return error.InvalidNameChar; + // Reject a literal double underscore: it is reserved for `.`. + if (ch == '_' and i + 1 < name.len and name[i + 1] == '_') return error.InvalidNameChar; + encoded_len += if (ch == '.') 2 else 1; + } + if (encoded_len > max_wire_name_len) return error.NameTooLong; +} + +/// Encode an internal name for the wire: `.` -> `__`. Writes into `buf` +/// (which must be at least `max_wire_name_len` bytes) and returns the +/// written slice. Names that passed `validateName` always fit. +pub fn encodeName(buf: []u8, name: []const u8) []const u8 { + var w: usize = 0; + for (name) |ch| { + if (ch == '.') { + buf[w] = '_'; + buf[w + 1] = '_'; + w += 2; + } else { + buf[w] = ch; + w += 1; + } + } + return buf[0..w]; +} + +/// Decode a wire name back to internal form: `__` -> `.`. Writes into +/// `buf` (at least `wire.len` bytes) and returns the written slice. The +/// decode is unambiguous because internal names never contain `__`. +pub fn decodeName(buf: []u8, wire: []const u8) []u8 { + var r: usize = 0; + var w: usize = 0; + while (r < wire.len) { + if (wire[r] == '_' and r + 1 < wire.len and wire[r + 1] == '_') { + buf[w] = '.'; + w += 1; + r += 2; + } else { + buf[w] = wire[r]; + w += 1; + r += 1; + } + } + return buf[0..w]; +} + +/// Tagged registry value. The registry stores one of these per *tool +/// name*. ToolSources expand to one entry per declared tool, each with a +/// distinct `tool_index`. +pub const Entry = union(enum) { + single: Tool, + source: SourceRef, + + pub const SourceRef = struct { + source: *ToolSource, + /// Index into `source.tools`. + tool_index: usize, + }; +}; + +/// Read-only view of a tool's metadata, uniform across `Tool` and +/// `ToolSource` registrations. Returned by registry iteration and +/// lookup. +pub const ToolView = struct { + decl: ToolDecl, + /// Which entry this view came from. Carries enough information to + /// dispatch the call (single Tool vs source-backed). + entry: Entry, + + pub fn name(self: ToolView) []const u8 { + return self.decl.name; + } +}; + +pub const ToolRegistry = struct { + /// Per-tool-name entries. + entries: std.StringHashMap(Entry), + /// Heap-allocated sources, kept in a list so `deinit` can tear each + /// down exactly once even though many entries reference a single + /// source. + sources: std.array_list.Managed(*ToolSource), + allocator: Allocator, + + pub fn init(allocator: Allocator) ToolRegistry { + return .{ + .entries = std.StringHashMap(Entry).init(allocator), + .sources = std.array_list.Managed(*ToolSource).init(allocator), + .allocator = allocator, + }; + } + + /// Tear down the registry. Each single `Tool`'s `vtable.deinit` is + /// invoked once. Each `ToolSource`'s `vtable.deinit` is invoked once + /// (not once per declared tool). + pub fn deinit(self: *ToolRegistry) void { + var it = self.entries.iterator(); + while (it.next()) |entry| { + switch (entry.value_ptr.*) { + .single => |t| t.vtable.deinit(t.ctx, self.allocator), + .source => {}, + } + } + self.entries.deinit(); + + for (self.sources.items) |src| { + src.vtable.deinit(src.ctx, self.allocator); + self.allocator.destroy(src); + } + self.sources.deinit(); + } + + /// Register a single tool. The registry takes ownership. + /// + /// Returns `error.DuplicateTool` if a tool with the same name is + /// already registered (whether from a single Tool or from a source). + /// In the duplicate case the caller's tool is NOT taken over; the + /// caller is responsible for tearing it down. + pub fn register(self: *ToolRegistry, tool: Tool) !void { + try validateName(tool.decl.name); + const gop = try self.entries.getOrPut(tool.decl.name); + if (gop.found_existing) return error.DuplicateTool; + gop.value_ptr.* = .{ .single = tool }; + } + + /// Register a tool source. The registry takes ownership of `src` — + /// it is heap-copied into the registry's source list and freed at + /// deinit. + /// + /// Returns `error.DuplicateTool` if any of the source's declared + /// tools collides with an existing registration. On collision the + /// source is NOT taken over (caller still owns it and must tear it + /// down) and any tools that *had* been inserted before the collision + /// are rolled back. + pub fn registerSource(self: *ToolRegistry, src: ToolSource) !void { + // First pass: validate names and check for any collision before + // committing anything. + for (src.tools) |decl| { + try validateName(decl.name); + if (self.entries.contains(decl.name)) return error.DuplicateTool; + } + + // Allocate the persistent heap copy of the source. From this + // point forward, on any failure we must free the allocation and + // roll back any entries we inserted. + const heap = try self.allocator.create(ToolSource); + errdefer self.allocator.destroy(heap); + heap.* = src; + + var inserted: usize = 0; + errdefer { + // Roll back any inserts we made before the failure. + for (src.tools[0..inserted]) |decl| { + _ = self.entries.remove(decl.name); + } + } + + for (src.tools, 0..) |decl, i| { + const gop = try self.entries.getOrPut(decl.name); + if (gop.found_existing) return error.DuplicateTool; + gop.value_ptr.* = .{ .source = .{ .source = heap, .tool_index = i } }; + inserted = i + 1; + } + + try self.sources.append(heap); + } + + /// Remove a single-tool registration by name. Calls the tool's + /// `vtable.deinit`. No-op if the name is not registered or if it + /// belongs to a source (sources are removed as a unit; not yet + /// exposed). + pub fn unregister(self: *ToolRegistry, name: []const u8) void { + const entry_ptr = self.entries.getPtr(name) orelse return; + switch (entry_ptr.*) { + .single => |t| { + _ = self.entries.remove(name); + t.vtable.deinit(t.ctx, self.allocator); + }, + .source => {}, // ignore — sources tear down at registry deinit + } + } + + /// Look up a tool by name. Returns a uniform `ToolView`. Pointer + /// invariants are the same as `std.StringHashMap.getPtr`: invalidated + /// by subsequent register/unregister calls. + pub fn lookup(self: *const ToolRegistry, name: []const u8) ?ToolView { + const entry = self.entries.get(name) orelse return null; + return makeView(entry); + } + + pub fn count(self: *const ToolRegistry) usize { + return self.entries.count(); + } + + pub fn iterator(self: *const ToolRegistry) Iterator { + return .{ .inner = self.entries.iterator() }; + } + + pub const Iterator = struct { + inner: std.StringHashMap(Entry).Iterator, + + pub fn next(self: *Iterator) ?ToolView { + const entry = self.inner.next() orelse return null; + return makeView(entry.value_ptr.*); + } + }; + + /// Iterate tools with their names **wire-encoded** (`.` -> `__`) for + /// the LLM. The yielded `ToolView.decl.name` borrows the iterator's + /// internal buffer and is only valid until the next `next()` call; + /// serializers consume it immediately, so this is safe. Description + /// and schema are unchanged. + pub fn toolsForLLM(self: *const ToolRegistry) LLMIterator { + return .{ .inner = self.entries.iterator() }; + } + + pub const LLMIterator = struct { + inner: std.StringHashMap(Entry).Iterator, + name_buf: [max_wire_name_len]u8 = undefined, + + pub fn next(self: *LLMIterator) ?ToolView { + const entry = self.inner.next() orelse return null; + var view = makeView(entry.value_ptr.*); + view.decl.name = encodeName(&self.name_buf, view.decl.name); + return view; + } + }; + + fn makeView(entry: Entry) ToolView { + return switch (entry) { + .single => |t| .{ .decl = t.decl, .entry = entry }, + .source => |sr| .{ .decl = sr.source.tools[sr.tool_index], .entry = entry }, + }; + } +}; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const testing = std.testing; + +/// A trivial in-test Tool implementation backed by a single owned counter +/// allocation. Used to verify ownership/deinit behavior. +const TestTool = struct { + invocations: u32 = 0, + name_owned: []u8, + desc_owned: []u8, + schema_owned: []u8, + + fn create(allocator: Allocator, name: []const u8) !Tool { + const self = try allocator.create(TestTool); + errdefer allocator.destroy(self); + + const name_owned = try allocator.dupe(u8, name); + errdefer allocator.free(name_owned); + const desc_owned = try allocator.dupe(u8, "test tool"); + errdefer allocator.free(desc_owned); + const schema_owned = try allocator.dupe(u8, "{}"); + errdefer allocator.free(schema_owned); + + self.* = .{ + .name_owned = name_owned, + .desc_owned = desc_owned, + .schema_owned = schema_owned, + }; + return .{ + .decl = .{ + .name = self.name_owned, + .description = self.desc_owned, + .schema_json = self.schema_owned, + }, + .ctx = self, + .vtable = &vt, + }; + } + + const vt: Tool.VTable = .{ + .invoke = invoke, + .deinit = deinit, + }; + + fn invoke(ctx: *anyopaque, input: []const u8, allocator: Allocator) anyerror!tool_mod.ResultParts { + const self: *TestTool = @ptrCast(@alignCast(ctx)); + self.invocations += 1; + return tool_mod.ResultParts.fromText(allocator, input); + } + + fn deinit(ctx: *anyopaque, allocator: Allocator) void { + const self: *TestTool = @ptrCast(@alignCast(ctx)); + allocator.free(self.name_owned); + allocator.free(self.desc_owned); + allocator.free(self.schema_owned); + allocator.destroy(self); + } +}; + +/// A minimal source backing N tools. Each tool name maps to a configured +/// response prefix; invoke_batch returns "<prefix>:<input>" for each +/// call. Tracks the batch sizes it was called with for inspection. +const TestSource = struct { + name_owned: []u8, + decls: []ToolDecl, + /// Allocations backing every `decl`'s strings. Freed at deinit. + allocations: std.array_list.Managed([]u8), + batch_sizes: std.array_list.Managed(usize), + allocator: Allocator, + + fn create( + allocator: Allocator, + source_name: []const u8, + tool_names: []const []const u8, + ) !ToolSource { + const self = try allocator.create(TestSource); + errdefer allocator.destroy(self); + + var allocations = std.array_list.Managed([]u8).init(allocator); + errdefer { + for (allocations.items) |s| allocator.free(s); + allocations.deinit(); + } + + const name_owned = try allocator.dupe(u8, source_name); + try allocations.append(name_owned); + + const decls = try allocator.alloc(ToolDecl, tool_names.len); + errdefer allocator.free(decls); + + for (tool_names, 0..) |tn, i| { + const n = try allocator.dupe(u8, tn); + try allocations.append(n); + const d = try allocator.dupe(u8, "test src tool"); + try allocations.append(d); + const s = try allocator.dupe(u8, "{}"); + try allocations.append(s); + decls[i] = .{ .name = n, .description = d, .schema_json = s }; + } + + self.* = .{ + .name_owned = name_owned, + .decls = decls, + .allocations = allocations, + .batch_sizes = std.array_list.Managed(usize).init(allocator), + .allocator = allocator, + }; + + return ToolSource{ + .name = self.name_owned, + .tools = self.decls, + .ctx = self, + .vtable = &vt, + }; + } + + const vt: ToolSource.VTable = .{ + .invoke_batch = invokeBatch, + .deinit = deinitSrc, + }; + + fn invokeBatch( + ctx: *anyopaque, + calls: []const tool_source_mod.Call, + results: []tool_source_mod.CallResult, + allocator: Allocator, + ) anyerror!void { + const self: *TestSource = @ptrCast(@alignCast(ctx)); + try self.batch_sizes.append(calls.len); + for (calls, 0..) |call, i| { + const buf = std.fmt.allocPrint( + allocator, + "{s}:{s}", + .{ call.tool_name, call.input }, + ) catch |e| { + results[i] = .{ .err = e }; + continue; + }; + results[i] = .{ + .ok = tool_mod.ResultParts.fromTextOwned(allocator, buf) catch |e| { + results[i] = .{ .err = e }; + continue; + }, + }; + } + } + + fn deinitSrc(ctx: *anyopaque, _: Allocator) void { + const self: *TestSource = @ptrCast(@alignCast(ctx)); + for (self.allocations.items) |s| self.allocator.free(s); + self.allocations.deinit(); + self.batch_sizes.deinit(); + self.allocator.free(self.decls); + self.allocator.destroy(self); + } +}; + +test "register, lookup, count" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + + try reg.register(try TestTool.create(allocator, "echo")); + try reg.register(try TestTool.create(allocator, "ls")); + + try testing.expectEqual(@as(usize, 2), reg.count()); + try testing.expect(reg.lookup("echo") != null); + try testing.expect(reg.lookup("ls") != null); + try testing.expect(reg.lookup("missing") == null); + try testing.expectEqualStrings("echo", reg.lookup("echo").?.decl.name); +} + +test "duplicate registration returns error and leaves original in place" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + + try reg.register(try TestTool.create(allocator, "echo")); + + // The second tool isn't taken over on duplicate; tear it down ourselves. + var dup = try TestTool.create(allocator, "echo"); + try testing.expectError(error.DuplicateTool, reg.register(dup)); + dup.vtable.deinit(dup.ctx, allocator); + + try testing.expectEqual(@as(usize, 1), reg.count()); +} + +test "unregister calls deinit and removes" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + + try reg.register(try TestTool.create(allocator, "tmp")); + try testing.expectEqual(@as(usize, 1), reg.count()); + + reg.unregister("tmp"); + try testing.expectEqual(@as(usize, 0), reg.count()); + try testing.expect(reg.lookup("tmp") == null); + + // No-op on missing. + reg.unregister("never_existed"); +} + +test "iterator visits every tool" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + + try reg.register(try TestTool.create(allocator, "a")); + try reg.register(try TestTool.create(allocator, "b")); + try reg.register(try TestTool.create(allocator, "c")); + + var saw_a = false; + var saw_b = false; + var saw_c = false; + + var it = reg.iterator(); + while (it.next()) |t| { + if (std.mem.eql(u8, t.decl.name, "a")) saw_a = true; + if (std.mem.eql(u8, t.decl.name, "b")) saw_b = true; + if (std.mem.eql(u8, t.decl.name, "c")) saw_c = true; + } + try testing.expect(saw_a and saw_b and saw_c); +} + +test "deinit frees all remaining tools" { + // If this leaks, the testing allocator will catch it. + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + try reg.register(try TestTool.create(allocator, "x")); + try reg.register(try TestTool.create(allocator, "y")); + reg.deinit(); +} + +test "registerSource exposes every declared tool by name" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + + const src = try TestSource.create(allocator, "panto-lua", &.{ "alpha", "beta", "gamma" }); + try reg.registerSource(src); + + try testing.expectEqual(@as(usize, 3), reg.count()); + const v = reg.lookup("beta") orelse return error.NotFound; + try testing.expectEqualStrings("beta", v.decl.name); + try testing.expect(v.entry == .source); +} + +test "registerSource: collision with existing single tool aborts and rolls back" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + + try reg.register(try TestTool.create(allocator, "shared")); + + // Build a source that includes the colliding name. We must tear it + // down ourselves on failure. + var src = try TestSource.create(allocator, "src", &.{ "first", "shared", "third" }); + try testing.expectError(error.DuplicateTool, reg.registerSource(src)); + src.vtable.deinit(src.ctx, allocator); + + // No partial state from the source remains. + try testing.expectEqual(@as(usize, 1), reg.count()); + try testing.expect(reg.lookup("first") == null); + try testing.expect(reg.lookup("third") == null); +} + +test "registerSource: collision between two sources" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + + try reg.registerSource(try TestSource.create(allocator, "a", &.{ "foo", "bar" })); + + var s = try TestSource.create(allocator, "b", &.{ "baz", "foo" }); + try testing.expectError(error.DuplicateTool, reg.registerSource(s)); + s.vtable.deinit(s.ctx, allocator); + + try testing.expectEqual(@as(usize, 2), reg.count()); +} + +test "source view exposes per-tool metadata uniformly" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + + try reg.registerSource(try TestSource.create(allocator, "lua", &.{ "x", "y" })); + try reg.register(try TestTool.create(allocator, "z")); + + try testing.expectEqual(@as(usize, 3), reg.count()); + + // Every entry has the canonical fields populated. + var it = reg.iterator(); + var n: usize = 0; + while (it.next()) |v| : (n += 1) { + try testing.expect(v.decl.name.len > 0); + try testing.expect(v.decl.description.len > 0); + try testing.expect(v.decl.schema_json.len > 0); + } + try testing.expectEqual(@as(usize, 3), n); +} + +// --- wire-name encoding --- + +test "validateName: accepts dotted names, rejects literal __ and bad chars" { + try validateName("std.read"); + try validateName("pkg.read_file"); + try validateName("a-b_c.d"); + try testing.expectError(error.InvalidNameChar, validateName("std__read")); + try testing.expectError(error.InvalidNameChar, validateName("has space")); + try testing.expectError(error.InvalidNameChar, validateName("slash/name")); + try testing.expectError(error.NameTooLong, validateName("")); + // 64 dots -> 128 encoded bytes: OK; 65 -> 130: too long. + try validateName("." ** 64); + try testing.expectError(error.NameTooLong, validateName("." ** 65)); +} + +test "encode/decode: dots <-> double underscores, bijective" { + var buf: [max_wire_name_len]u8 = undefined; + var buf2: [max_wire_name_len]u8 = undefined; + + const cases = [_][]const u8{ "std.read", "pkg.read_file", "a.b.c", "plain", "a-b" }; + inline for (cases) |internal| { + const wire = encodeName(&buf, internal); + try testing.expect(std.mem.indexOf(u8, wire, ".") == null); + const back = decodeName(&buf2, wire); + try testing.expectEqualStrings(internal, back); + } + + // Spot-check the exact wire form and the read_file distinction. + try testing.expectEqualStrings("std__read", encodeName(&buf, "std.read")); + try testing.expectEqualStrings("pkg__read_file", encodeName(&buf, "pkg.read_file")); + try testing.expectEqualStrings("pkg__read__file", encodeName(&buf, "pkg.read.file")); +} + +test "register rejects names with literal double underscore" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + + var bad = try TestTool.create(allocator, "std__read"); + try testing.expectError(error.InvalidNameChar, reg.register(bad)); + // Registration refused ownership; tear the tool down ourselves. + bad.vtable.deinit(bad.ctx, allocator); +} + +test "toolsForLLM yields wire-encoded names; iterator keeps dotted names" { + const allocator = testing.allocator; + var reg = ToolRegistry.init(allocator); + defer reg.deinit(); + try reg.register(try TestTool.create(allocator, "std.read")); + + var llm = reg.toolsForLLM(); + const v = llm.next().?; + try testing.expectEqualStrings("std__read", v.decl.name); + try testing.expect(llm.next() == null); + + // The internal iterator is unchanged. + var it = reg.iterator(); + try testing.expectEqualStrings("std.read", it.next().?.decl.name); +} diff --git a/src/tool_source.zig b/src/tool_source.zig new file mode 100644 index 0000000..d5bc716 --- /dev/null +++ b/src/tool_source.zig @@ -0,0 +1,104 @@ +//! Batch-dispatched tool extension API: `ToolSource`. +//! +//! Where `Tool` is a single, thread-safe handler (one tool, one vtable, +//! reentrant), `ToolSource` is a single owner of many tools whose runtime +//! prefers to receive calls in *batches* on a single thread. +//! +//! Motivation: Lua. A Lua extension runtime maintains one long-lived +//! `lua_State` so that module-globals, lazy connection pools, rate +//! limiters, etc. survive across calls. A single `lua_State` is not safe +//! for concurrent host entry, so the runtime can't satisfy `Tool`'s +//! thread-safety contract directly. The runtime *can* dispatch many calls +//! cooperatively (coroutines + an event loop), but it needs to be told +//! all of them at once. +//! +//! The contract libpanto provides: +//! +//! - For a given turn, every `ToolUse` block whose tool name belongs to +//! a particular source is delivered in a single `invoke_batch` call, +//! on one thread. +//! - Distinct sources still execute concurrently (one OS thread per +//! source per turn), so a Lua source and a native source can run in +//! parallel. +//! - Single `Tool` registrations are unchanged. They each get their own +//! thread when they appear alongside other tool calls in a turn. +//! +//! The "thread-safe" promise that `Tool.invoke` carries relaxes to +//! "coroutine-safe within the source's runtime" for source-backed tools — +//! enforcement is the source's problem. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const tool = @import("tool.zig"); + +/// Tool metadata: re-exported from `tool.zig`, which owns the more atomic +/// type. `ToolSource`s declare their tools this way because they share a +/// single dispatch path. +pub const ToolDecl = tool.ToolDecl; +pub const ResultPart = tool.ResultPart; +pub const ResultParts = tool.ResultParts; + +/// One pending invocation passed to `invoke_batch`. Slices borrowed from +/// the caller for the duration of the call. +pub const Call = struct { + /// Which of the source's declared tools this call targets. + tool_name: []const u8, + /// Raw JSON bytes the provider sent. Borrowed. + input: []const u8, +}; + +/// Result for a single call. Mirrors the success/error split of +/// `Tool.invoke`'s return shape. Owned by the caller-supplied allocator. +pub const CallResult = union(enum) { + /// Owned parts (the `ResultParts` slice + each part's bytes), freed by + /// libpanto after assembling the ToolResult block (see + /// `tool.ResultParts.deinit`). + ok: ResultParts, + err: anyerror, +}; + +/// A grouped tool runtime. +pub const ToolSource = struct { + /// Diagnostic name; surfaced in error messages and logs. Example + /// values: `"panto-lua"`, `"panto-python"`. Borrowed; lifetime owned + /// by the source. + name: []const u8, + /// Tool metadata for every tool this source owns. Borrowed. + tools: []const ToolDecl, + ctx: *anyopaque, + vtable: *const VTable, + + pub const VTable = struct { + /// libpanto guarantees: for a given turn, every ToolUse call + /// whose tool name belongs to this source is delivered in one + /// `invoke_batch`, on one thread. Different sources still + /// execute in parallel. + /// + /// `calls` and `results` are parallel arrays of length N. + /// `results` is pre-allocated by libpanto; the source fills each + /// slot. The source decides internal scheduling — sequential, + /// coroutine fan-out, worker pool, etc. + /// + /// Two failure modes, both normally model-visible: + /// - Per-call: record `.{ .err = e }` in a `results[i]` slot. + /// That call gets an error `ToolResult`; siblings are + /// unaffected. + /// - Whole-batch: return an error from this function. libpanto + /// frees any `ok` slots already filled and maps the error onto + /// *every* member call as an error `ToolResult`. + /// In both cases the agent loop continues so the model can react. + /// Only hard host failures (`error.Canceled`, `error.OutOfMemory`) + /// abort the whole turn and propagate to the embedder. + invoke_batch: *const fn ( + ctx: *anyopaque, + calls: []const Call, + results: []CallResult, + allocator: Allocator, + ) anyerror!void, + + /// Called when the source is removed from the registry or the + /// registry is torn down. Frees any resources owned by `ctx`, + /// including `ctx` itself if heap-allocated. + deinit: *const fn (ctx: *anyopaque, allocator: Allocator) void, + }; +}; diff --git a/src/turn_persist.zig b/src/turn_persist.zig new file mode 100644 index 0000000..6d260c8 --- /dev/null +++ b/src/turn_persist.zig @@ -0,0 +1,133 @@ +//! Turn → session-log persistence: map in-memory `Conversation` messages +//! to rich `PersistentMessage` write records and append them through a +//! `Session` handle. +//! +//! This logic lives in `libpanto` and is called by the `Agent` itself, so +//! every embedder gets persistence for free. The functions here are +//! stateless helpers over a `Session`; the agent owns the store and the +//! conversation. +//! +//! The write record is **maximalist** (full wire identity + usage + the +//! entire current conversation + the offered tool set). The library offers +//! it all on every append; each store keeps what it wants. `PersistentMessage` +//! borrows the in-memory `Message` directly — no disk conversion happens +//! here; the store does its own serialization. +//! +//! Per-message usage is read directly off `Message.usage`. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const conversation = @import("conversation.zig"); +const session_store = @import("session_store.zig"); + +const PersistentMessage = session_store.PersistentMessage; +const WireIdentity = session_store.WireIdentity; +const ToolDecl = session_store.ToolDecl; +const Session = session_store.Session; + +/// Persist every conversation message at index `>= start_index` through +/// `session` as a single atomic batch. +/// +/// Each `PersistentMessage` borrows the in-memory `Message`, the full +/// current conversation, and `tools` (the offered tool set) — all owned by +/// the caller and valid for the duration of the call. The wire `identity` +/// is stamped on every message; the store decides per-role what to keep +/// (the FS store drops the stamp on system entries). +/// +/// An assistant message carrying a ToolUse with no following matching +/// ToolResult is skipped (a dangling tool call from an interrupted turn); +/// persisting it would make the log un-replayable. +pub fn persistTurn( + alloc: Allocator, + session: *Session, + conv: *conversation.Conversation, + start_index: usize, + identity: WireIdentity, + tools: []const ToolDecl, +) !void { + return persistRange(alloc, session, conv, start_index, conv.messages.items.len, identity, tools); +} + +/// Persist conversation messages in `[start_index, end_index)`. Like +/// `persistTurn` but with an explicit upper bound, so an incremental flush can +/// exclude a trailing not-yet-coherent message (a dangling tool call awaiting +/// its results) while still committing everything before it. +pub fn persistRange( + alloc: Allocator, + session: *Session, + conv: *conversation.Conversation, + start_index: usize, + end_index: usize, + identity: WireIdentity, + tools: []const ToolDecl, +) !void { + var batch: std.ArrayList(PersistentMessage) = .empty; + defer batch.deinit(alloc); + + const all_messages = conv.messages.items; + var i = start_index; + while (i < end_index) : (i += 1) { + const msg = &all_messages[i]; + if (msg.role == .assistant and hasToolUseWithoutFollowingResults(conv, i)) { + continue; + } + // Stamp the producing identity once, on first persist. A message that + // already carries one (e.g. a kept-verbatim turn carried through + // compaction, or one rebuilt from disk) keeps it — only the messages + // freshly produced under `identity` are stamped now. This is what + // makes the stamp survive a later compaction onto a different model. + if (msg.identity == null) { + msg.identity = try conversation.dupeWireIdentity(alloc, identity); + } + try batch.append(alloc, .{ + .message = msg.*, + .usage = msg.usage, + .identity = msg.identity.?, + .conversation = all_messages, + .tools_available = tools, + }); + } + + if (batch.items.len == 0) return; + try session.append(batch.items); +} + +/// Persist a compaction result. The agent rewrote the conversation to +/// `[system..., summary, kept-suffix...]`; persist everything from the +/// latest compaction summary onward as fresh entries. +pub fn persistCompaction( + alloc: Allocator, + session: *Session, + conv: *conversation.Conversation, + identity: WireIdentity, + tools: []const ToolDecl, +) !void { + const start = conversation.latestCompactionIndex(conv.messages.items) orelse return; + try persistTurn(alloc, session, conv, start, identity, tools); +} + +/// True when the assistant message at `index` contains a ToolUse block +/// with no matching ToolResult in the immediately following user message +/// — i.e. a dangling tool call (e.g. a turn cut short by an error). +pub fn hasToolUseWithoutFollowingResults( + conv: *const conversation.Conversation, + index: usize, +) bool { + const msg = conv.messages.items[index]; + for (msg.content.items) |block| { + if (block != .ToolUse) continue; + if (index + 1 >= conv.messages.items.len) return true; + const next = conv.messages.items[index + 1]; + if (next.role != .user) return true; + var found = false; + for (next.content.items) |next_block| { + if (next_block == .ToolResult and std.mem.eql(u8, next_block.ToolResult.tool_use_id, block.ToolUse.id)) { + found = true; + break; + } + } + if (!found) return true; + } + return false; +} |
