diff options
| author | t <t@tjp.lol> | 2026-07-07 11:29:23 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-07-07 12:15:28 -0600 |
| commit | 621d7fee0ace729f8d44126032d2c6e13f72ee7f (patch) | |
| tree | a2938b0a9e5c0930e8644721abbe94875df9ff08 /libpanto | |
| parent | 0fa6d4209ff9b4a95e7d1955887aa4c73ee3423c (diff) | |
Remove the in-repo libpanto sources and binding projects from pantograph.
Consume libpantograph through the Zig package URL at
code.tjp.lol/libpantograph.git, including the Lua module artifact used by
CLI extensions.
Diffstat (limited to 'libpanto')
33 files changed, 0 insertions, 44276 deletions
diff --git a/libpanto/build.zig b/libpanto/build.zig deleted file mode 100644 index 65f4093..0000000 --- a/libpanto/build.zig +++ /dev/null @@ -1,47 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - // Public module — other packages can import this via dependency. - // The image module pulls in the vendored stb/jebp single-header C - // libraries, so the module needs libc + the C translation unit + the - // include path for `@cImport`. - const panto_mod = b.addModule("panto", .{ - .root_source_file = b.path("src/public.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, - }); - addImageCDeps(b, panto_mod); - - // Unit tests - const test_module = b.createModule(.{ - .root_source_file = b.path("src/public.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, - }); - addImageCDeps(b, test_module); - - const unit_tests = b.addTest(.{ - .name = "panto-tests", - .root_module = test_module, - }); - - const run_unit_tests = b.addRunArtifact(unit_tests); - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_unit_tests.step); -} - -/// Wire the vendored stb/jebp single-header image libraries into `mod`: -/// the include path for `@cImport`, and the single C translation unit -/// that compiles their implementations. Callers must also link libc. -fn addImageCDeps(b: *std.Build, mod: *std.Build.Module) void { - mod.addIncludePath(b.path("src/cdeps")); - mod.addCSourceFile(.{ - .file = b.path("src/cdeps/image_impl.c"), - .flags = &.{ "-std=gnu99", "-O2", "-fno-sanitize=undefined" }, - }); -} diff --git a/libpanto/build.zig.zon b/libpanto/build.zig.zon deleted file mode 100644 index 10d9111..0000000 --- a/libpanto/build.zig.zon +++ /dev/null @@ -1,10 +0,0 @@ -.{ - .fingerprint = 0x687da1a5092d6c39, - .name = .panto, - .version = "0.0.0", - .paths = .{ - "build.zig", - "build.zig.zon", - "src", - }, -} diff --git a/libpanto/src/agent.zig b/libpanto/src/agent.zig deleted file mode 100644 index 09b0994..0000000 --- a/libpanto/src/agent.zig +++ /dev/null @@ -1,3925 +0,0 @@ -//! 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/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig deleted file mode 100644 index 5085149..0000000 --- a/libpanto/src/anthropic_messages_json.zig +++ /dev/null @@ -1,1619 +0,0 @@ -//! 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/libpanto/src/auth.zig b/libpanto/src/auth.zig deleted file mode 100644 index f70b031..0000000 --- a/libpanto/src/auth.zig +++ /dev/null @@ -1,954 +0,0 @@ -//! 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/libpanto/src/cdeps/image_impl.c b/libpanto/src/cdeps/image_impl.c deleted file mode 100644 index 6266630..0000000 --- a/libpanto/src/cdeps/image_impl.c +++ /dev/null @@ -1,25 +0,0 @@ -// 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/libpanto/src/cdeps/jebp.h b/libpanto/src/cdeps/jebp.h deleted file mode 100644 index dc95d06..0000000 --- a/libpanto/src/cdeps/jebp.h +++ /dev/null @@ -1,2457 +0,0 @@ -/** - * 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/libpanto/src/cdeps/stb_image.h b/libpanto/src/cdeps/stb_image.h deleted file mode 100644 index 9eedabe..0000000 --- a/libpanto/src/cdeps/stb_image.h +++ /dev/null @@ -1,7988 +0,0 @@ -/* 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/libpanto/src/cdeps/stb_image_resize2.h b/libpanto/src/cdeps/stb_image_resize2.h deleted file mode 100644 index 0798976..0000000 --- a/libpanto/src/cdeps/stb_image_resize2.h +++ /dev/null @@ -1,10679 +0,0 @@ -/* 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/libpanto/src/cdeps/stb_image_write.h b/libpanto/src/cdeps/stb_image_write.h deleted file mode 100644 index e4b32ed..0000000 --- a/libpanto/src/cdeps/stb_image_write.h +++ /dev/null @@ -1,1724 +0,0 @@ -/* 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/libpanto/src/compaction.zig b/libpanto/src/compaction.zig deleted file mode 100644 index f620f30..0000000 --- a/libpanto/src/compaction.zig +++ /dev/null @@ -1,838 +0,0 @@ -//! 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/libpanto/src/config.zig b/libpanto/src/config.zig deleted file mode 100644 index f2f04e7..0000000 --- a/libpanto/src/config.zig +++ /dev/null @@ -1,403 +0,0 @@ -//! 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/libpanto/src/conversation.zig b/libpanto/src/conversation.zig deleted file mode 100644 index ea4309c..0000000 --- a/libpanto/src/conversation.zig +++ /dev/null @@ -1,730 +0,0 @@ -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/libpanto/src/file_system_jsonl_store.zig b/libpanto/src/file_system_jsonl_store.zig deleted file mode 100644 index 9bc9436..0000000 --- a/libpanto/src/file_system_jsonl_store.zig +++ /dev/null @@ -1,2103 +0,0 @@ -//! 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/libpanto/src/http_helper.zig b/libpanto/src/http_helper.zig deleted file mode 100644 index a075d72..0000000 --- a/libpanto/src/http_helper.zig +++ /dev/null @@ -1,160 +0,0 @@ -//! 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/libpanto/src/image.zig b/libpanto/src/image.zig deleted file mode 100644 index 26474d1..0000000 --- a/libpanto/src/image.zig +++ /dev/null @@ -1,358 +0,0 @@ -//! 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/libpanto/src/null_store.zig b/libpanto/src/null_store.zig deleted file mode 100644 index 1f3ab74..0000000 --- a/libpanto/src/null_store.zig +++ /dev/null @@ -1,108 +0,0 @@ -//! `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/libpanto/src/openai_chat_json.zig b/libpanto/src/openai_chat_json.zig deleted file mode 100644 index f00c97f..0000000 --- a/libpanto/src/openai_chat_json.zig +++ /dev/null @@ -1,1067 +0,0 @@ -//! 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/libpanto/src/openai_responses_json.zig b/libpanto/src/openai_responses_json.zig deleted file mode 100644 index 358fc45..0000000 --- a/libpanto/src/openai_responses_json.zig +++ /dev/null @@ -1,941 +0,0 @@ -//! 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/libpanto/src/pricing.zig b/libpanto/src/pricing.zig deleted file mode 100644 index 0565593..0000000 --- a/libpanto/src/pricing.zig +++ /dev/null @@ -1,383 +0,0 @@ -//! 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/libpanto/src/provider.zig b/libpanto/src/provider.zig deleted file mode 100644 index e12ff59..0000000 --- a/libpanto/src/provider.zig +++ /dev/null @@ -1,554 +0,0 @@ -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/libpanto/src/provider_anthropic_messages.zig b/libpanto/src/provider_anthropic_messages.zig deleted file mode 100644 index 955991a..0000000 --- a/libpanto/src/provider_anthropic_messages.zig +++ /dev/null @@ -1,1267 +0,0 @@ -//! 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/libpanto/src/provider_openai_chat.zig b/libpanto/src/provider_openai_chat.zig deleted file mode 100644 index 4ac1e11..0000000 --- a/libpanto/src/provider_openai_chat.zig +++ /dev/null @@ -1,1306 +0,0 @@ -//! 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/libpanto/src/provider_openai_responses.zig b/libpanto/src/provider_openai_responses.zig deleted file mode 100644 index d764e6f..0000000 --- a/libpanto/src/provider_openai_responses.zig +++ /dev/null @@ -1,1139 +0,0 @@ -//! 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/libpanto/src/public.zig b/libpanto/src/public.zig deleted file mode 100644 index d8b9f7b..0000000 --- a/libpanto/src/public.zig +++ /dev/null @@ -1,299 +0,0 @@ -//! `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/libpanto/src/session.zig b/libpanto/src/session.zig deleted file mode 100644 index 35fd09b..0000000 --- a/libpanto/src/session.zig +++ /dev/null @@ -1,1606 +0,0 @@ -//! 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/libpanto/src/session_store.zig b/libpanto/src/session_store.zig deleted file mode 100644 index de25e21..0000000 --- a/libpanto/src/session_store.zig +++ /dev/null @@ -1,188 +0,0 @@ -//! `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/libpanto/src/sse.zig b/libpanto/src/sse.zig deleted file mode 100644 index c54d532..0000000 --- a/libpanto/src/sse.zig +++ /dev/null @@ -1,170 +0,0 @@ -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/libpanto/src/stream.zig b/libpanto/src/stream.zig deleted file mode 100644 index 64748b2..0000000 --- a/libpanto/src/stream.zig +++ /dev/null @@ -1,179 +0,0 @@ -//! 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/libpanto/src/tool.zig b/libpanto/src/tool.zig deleted file mode 100644 index c96dfee..0000000 --- a/libpanto/src/tool.zig +++ /dev/null @@ -1,149 +0,0 @@ -//! 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/libpanto/src/tool_registry.zig b/libpanto/src/tool_registry.zig deleted file mode 100644 index 430cda5..0000000 --- a/libpanto/src/tool_registry.zig +++ /dev/null @@ -1,663 +0,0 @@ -//! 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/libpanto/src/tool_source.zig b/libpanto/src/tool_source.zig deleted file mode 100644 index d5bc716..0000000 --- a/libpanto/src/tool_source.zig +++ /dev/null @@ -1,104 +0,0 @@ -//! 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/libpanto/src/turn_persist.zig b/libpanto/src/turn_persist.zig deleted file mode 100644 index 6d260c8..0000000 --- a/libpanto/src/turn_persist.zig +++ /dev/null @@ -1,133 +0,0 @@ -//! 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; -} |
