KRISTENI®

UniqueCore · C ABI documentation

C ABI

Source/API is the boundary between the core and its frontends: Unreal, the Blender server, and the tests. This page is the map; the comments in the headers contain the authoritative details.

Release 0.0.1, ABI 25. See UC_GetVersion and UC_GetAbiVersion. The ABI history is recorded in Types/APIAbi.h.

Units and coordinate system

  • Lengths are in centimetres and angles are in radians. Values at the boundary use single precision (float); the core calculates in double precision, and conversion happens in the API layer and nowhere else.
  • The coordinate system is right-handed: X forward, Y left, Z up. Rotation around Z (yaw: YawRadians, RotationYaw) is measured from +X towards +Y. A wall's left face is on its +Y side, to the left when looking along the wall's direction.
  • A face is front-facing when its triangle (V0, V1, V2) is counter-clockwise as viewed from the normal. For UV mapping, U × V = N.
  • A left-handed system (Unreal: X forward, Y right) must invert Y on its side; the core performs no conversion.
  • Wall and slab coordinates, both on input and in the mesh (Origin, vertices), are relative to the elevation of their story (ABI 25). The core stores the elevation but never adds it; see “Stories”.

Headers

Include everything at once:

#include <UniqueCore/UniqueCore.h>
HeaderContents
App/APIApp.hUC_Initialize, UC_Shutdown
Core/APICore.hUC_GetVersion, the failure reason (UC_GetLastError), and logging (UC_SetLogCallback, UC_SetLogLevel, UC_GetLogLevel)
Types/APIAbi.hUC_ABI_VERSION, UC_GetAbiVersion, and the ABI history
Types/APIResult.hUC_Result, UC_GetResultName
Types/APIHandles.hHouse, story, wall, opening, and slab handles
Types/APIMesh.hThe “KMSH” mesh block
House/APIHouse.hHouses
House/APIStory.hStories: UC_CreateStory, UC_UpdateStory, UC_DeleteStory
House/APIWall.hWalls
House/APIOpening.hOpenings
House/APIJoinery.hJoinery
House/APISlab.hSlabs

The headers support both C and C++ and use only <stdint.h> and plain structures. UNIQUE_CORE_EXTERN_C and UNIQUE_CORE_NOEXCEPT expand according to the language.

Data ownership

The core stores only one graph per house: its stories, walls, their intersections, openings, and slabs. UV mapping, materials, and joinery parameters belong to the frontend and are supplied with every mesh request. The core does not store meshes either; it builds them on demand.

The frontend is the source of truth for everything visible; the core is the source of truth for graph geometry.

Result codes

UC_Result is an int32_t, not a C enum, so its size at the boundary is fixed.

ValueNameMeaning
0UC_Result_SuccessSuccess
1UC_Result_InvalidArgumentNull pointer, NaN, invalid size, or a handle that is not live in this house
2UC_Result_InternalErrorInternal error; the exception is written to the log and to UC_GetLastError
3UC_Result_NotInitializedThe core is not initialized
4UC_Result_NotImplementedThe command is present in the API, but this release does not implement it yet
5UC_Result_OverflowThe caller's array is too small; the count reports the required size

UC_GetResultName returns the code name for logging. For an unknown code it returns "UC_Result_Unknown", never NULL.

Failure reason: UC_GetLastError

Since ABI 25 (docs/integration.md D20), every result other than UC_Result_Success also leaves a human-readable reason.

UNIQUE_CORE_EXTERN_C UNIQUE_CORE_API const char* UC_GetLastError(void) UNIQUE_CORE_NOEXCEPT;
WhatThe last failure message on the current thread: UTF-8, null-terminated, in Bulgarian, and beginning with the function name, for example "UC_IntersectWalls: [Bulgarian failure reason: Parallel]"
After successAn empty string "", not NULL. This is also returned on a thread that has not called anything yet
What writes itEvery boundary failure: InvalidArgument, NotInitialized, Overflow, InternalError; argument checks, dead handles, graph rejections (the UC_IntersectWalls cases and story checks), degenerate walls, and exceptions caught by Guard
What clears itThe next successful UC_ call on the same thread; a failing call replaces it with its own reason
What leaves it unchangedUC_GetLastError and UC_GetResultName: both may be called after a failure, in either order
LifetimeThe pointer is owned by the core and remains valid until the next UC_ call on the thread. Copy it if it is needed later
LoggingIndependent of the log level: a Release core (logging Error and above) returns the same text as a Debug core
ResetDoes not require UC_Initialize and is not lost on UC_Shutdown; a subsequent success clears it
LengthUp to 1023 bytes; longer messages are truncated at a character boundary

Consumer behaviour: after every result other than UC_Result_Success, read UC_GetLastError() immediately and on the same thread, before the next UC_ call, including UC_SetLogCallback, and include it in the error next to UC_GetResultName(code). KUB does this in one place in KristeniCoreLocalBackend.cpp. The server uses ApplyError.message_user / message_log.

The message is for humans. Programs branch on the result code; they must not compare or parse the text because its wording may change without an ABI bump. Translation is not the core's responsibility (D11).

The storage is a fixed-size array per thread with no destructor: writing does not allocate or throw. Once the library is unloaded, the pointer is no longer valid, just like a mesh-buffer pointer.

Sizes

These are checked by a C test on every build.

TypeBytes
UC_Result4
UC_Version12
UC_HouseHandle, UC_StoryHandle, UC_WallHandle, UC_OpeningHandle, UC_SlabHandle16
UC_Vector2f8
UC_Vector3f12
UC_UVData20
UC_WallRequest64
UC_WallParameters48 (44 bytes of fields + 4 bytes of trailing padding)
UC_OpeningParameters16
UC_SlabParameters24 (20 bytes of fields + 4 bytes of trailing padding)
UC_FrameParameters20
UC_DoorSashParameters4
UC_SingleSashWindowParameters60
UC_SingleSashDoorParameters64
UC_DoubleSashWindowParameters68
UC_MeshHeader36
UC_SurfaceHeader16
UC_MeshVertexInstance36

UC_StoryHandle raises the alignment of UC_WallParameters and UC_SlabParameters to 8, which is why each has 4 bytes of trailing padding. A ctypes.Structure wrapper gets this automatically when fields are declared in header order; do not add a padding field manually.

Lifecycle

UC_GetVersion, UC_GetAbiVersion,
UC_SetLogCallback, UC_SetLogLevel,
UC_GetLogLevel, UC_GetResultName,
UC_GetLastError                          at any time

UC_Initialize
    UC_CreateHouse                       only while the core is initialized
        UC_CreateStory                   before the first wall or slab
            UC_BuildWall, UC_BuildSlab, ...
        UC_DeleteStory
    UC_DeleteHouse
UC_Shutdown
  • UC_Initialize is idempotent: a second call succeeds.
  • UC_Shutdown called while the core is not initialized returns UC_Result_NotInitialized.
  • After UC_Shutdown, all handles are invalid and remain invalid after a new UC_Initialize. Generations do not restart, so an old handle cannot resolve to a new house.
  • The C ABI is single-threaded: the host calls one UC_* function at a time and is responsible for serializing calls.

Reset

UC_Shutdown(); UC_Initialize(); is the intended way to start from a clean state between levels, scenes, or documents. There is no UC_Reset, and there will not be one.

  • It is safe on an empty core (initialized, with no houses) and may be repeated as often as necessary within one session.
  • The only cost is releasing house memory; their graphs are discarded. There is no additional work.
  • Logging is unaffected: the callback set by UC_SetLogCallback and the level set by UC_SetLogLevel remain in place and do not need to be supplied again.
  • The thread-local mesh buffer (see “Meshes”) is not released; the next mesh request reuses it.
  • Every handle created before the reset returns UC_Result_InvalidArgument afterwards (see above).

Logging

UC_SetLogCallback redirects logging to a host callback; NULL restores std::cout. The callback receives the level as an int32_t, from UC_LogLevel_Trace (0) through UC_LogLevel_Critical (5), together with the category and message.

UC_SetLogLevel sets the lowest level delivered to the callback, and UC_GetLogLevel returns it (ABI 24):

  • A level outside 0..5 returns UC_Result_InvalidArgument, and the existing level remains unchanged; it is not clamped. UC_GetLogLevel(NULL) does the same.
  • The level cannot be lower than the compiled level (KRISTENI_COMPILED_LOG_LEVEL: Debug includes everything, while Release includes only Error and Critical). Lower-severity messages are removed at compile time. A request below the compiled level is nevertheless accepted with UC_Result_Success, and UC_GetLogLevel returns the requested level, but the higher of the two levels takes effect. A Release core set to Trace still emits only Error and Critical.
  • Before the first UC_SetLogLevel, UC_GetLogLevel returns the compiled level (UC_LogLevel_Critical when logging is compiled out).
  • A failed check (KRISTENI_CHECK) bypasses the threshold: it always arrives as Critical with category "Check".
  • Neither function requires UC_Initialize, and the level survives UC_Shutdown; it belongs to the logger, not the core, just like the callback.

The callback is invoked synchronously on the thread that called the UC_ function, from inside that call, before the function returns. There is no queue or background thread. When the function returns, all of its log lines have already passed through the callback. The callback must not throw or call UC_SetLogCallback itself.

Failed checks

A failed KRISTENI_CHECK indicates a core bug, not rejected input. Valid input must never reach one. If it nevertheless happens:

CoreBehaviour
ReleaseA Critical "Check" line is emitted; the check throws Kristeni::CheckError, the boundary Guard catches it, emits an Error "API" line with the function name, and returns UC_Result_InternalError. The process remains alive; the state of the house where the failure occurred is not guaranteed.
DebugA Critical "Check" line is emitted, followed by abort() so the process stops in the debugger. K_CHECK exists only in Debug and always stops.

Houses and handles

UC_CreateHouse returns a house handle. UC_DeleteHouse deletes the house, including its graph and everything in it, stories included.

A house has no world-space transform: story elevations are relative to the house origin, while wall and slab points are relative to their story elevation; meshes use the same frame. The frontend decides where the house is placed.

A handle is a pair consisting of an index and a generation. A deleted object is not resurrected when another object takes its slot because their generations do not match.

Every command operating on a story, wall, opening, or slab also receives its house. Their handles carry the house identity as well, in the upper 32 bits of both the index and the generation, and the core verifies that the two match. A handle belonging to another or deleted house returns UC_Result_InvalidArgument; it never silently edits another house.

[!important] Handles are opaque to consumers. Store and return them exactly as received; never calculate or assemble them manually.

Stories

Since ABI 25 (House/APIStory.h, docs/integration.md D4), every wall and slab belongs to exactly one story, supplied at creation: Story in UC_WallRequest and UC_SlabParameters. A house does not create a story automatically. After UC_CreateHouse, the frontend calls UC_CreateStory before the first wall or slab. Restoration order is house → stories → walls → slabs → openings → joinery.

FunctionBehaviourAffected walls
UC_CreateStory(House, ElevationCm, &Story)Creates a story at an elevation relative to the house originNone
UC_UpdateStory(House, Story, ElevationCm)Sets a new elevation; rebuilds nothingNone returned
UC_DeleteStory(House, Story)Cascades deletion through the story's walls (and their openings) and slabsNone returned

The elevation may be any finite number, positive or negative; two stories at the same elevation are still distinct stories. NaN and infinity return UC_Result_InvalidArgument (even when the core is not initialized, because this validation happens first).

Elevation is not part of the geometry. Input and mesh coordinates, Origin and vertices, are relative to story elevation; the core never adds the elevation. The frontend attaches meshes to the story object (an actor in Unreal), which carries the elevation. Consequently, UC_UpdateStory returns no affected walls: meshes remain byte-for-byte identical, and the frontend moves only the story object. The core has no story-height property; height belongs to the wall and is supplied in its request.

Boundary rules:

  • UC_BuildWall and UC_BuildSlab without a live story in the same house (a zero Story, a deleted one, or one from another house) return UC_Result_InvalidArgument; nothing is created, and OutWall/OutSlab remain zero. An ABI 24 request copied with a zero Story is rejected; there is no default story.
  • UC_SetWallParameters (UC_WallParameters.Story) and UC_SetSlabParameters supplied with a story different from the one used at creation return UC_Result_InvalidArgument, with a zero count and an unchanged graph. Walls and slabs cannot move between stories; changing story requires deletion and reconstruction.
  • Intersections do not cross stories: the end of a wall on one story and the start of a wall on another do not form a corner, even if they occupy the same point and both stories have the same elevation. UC_IntersectWalls rejects walls on different stories (see below).
  • UC_DeleteStory returns no list. Other stories remain unchanged, and the frontend already knows the deleted objects: everything it created with that story. Handles for deleted walls, openings, and slabs immediately return UC_Result_InvalidArgument, as does the story handle itself, including on a second deletion attempt.

Commands and affected walls

Mutating commands (creation, editing, intersection, and deletion) return the affected walls in a caller-owned array: every wall whose mesh must be requested again. The list includes the wall itself and neighbours whose joints have changed.

UC_StoryHandle story;

UC_CreateStory(house, 0.0f, &story);   /* once, before the first wall */

UC_WallRequest request = {house, story, {0, 0, 0}, {400, 0, 0}, 25.0f, 280.0f};
UC_WallHandle  wall;
UC_WallHandle  affected[64];
uint32_t       count = 0;

if (UC_BuildWall(&request, &wall, affected, 64, &count) == UC_Result_Success)
{
    for (uint32_t i = 0; i < count; ++i)
    {
        /* UV mapping for each wall is owned by the frontend. */
        const void* data = NULL;
        uint32_t    size = 0;

        UC_GetWallMesh(house, affected[i], &left_uv[i], &right_uv[i], &data, &size);

        /* Copy data immediately; see “Meshes”. */
    }
}

Array rules:

  • The count is always written and reports the required capacity, not the number of elements actually written.
  • A zero capacity with NULL is a valid request.
  • UC_Result_Overflow from a mutating command does not mean “nothing happened”: the graph has already changed. Do not call the command again; a second creation would create a second wall. Refresh every wall in the house for this call, and allocate the capacity reported by the count next time.
  • The new object's identity is returned separately (OutWall, OutOpening, or OutSlab) and is written before capacity is checked. The caller therefore receives the new handle even when the result is UC_Result_Overflow.

A wall has two distinct ends only when they are more than 0.05 cm apart, the tolerance below which the graph merges points. UC_BuildWall with closer StartPosition/EndPosition values and UC_SetWallParameters with a Length of 0.05 cm or less return UC_Result_InvalidArgument before modifying the graph: nothing is created, and the wall remains unchanged. This is the only geometric validation performed at the boundary (ABI 22). Openings are not validated either at the boundary or in the graph; see “Openings and joinery”. The graph validates slab contours.

For read operations such as UC_GetWallOpenings, UC_Result_Overflow simply means “allocate more space and call again”.

Intersections rejected by the graph

UC_IntersectWalls extends or shortens both walls to the point where their centre lines intersect in plan, moving the closer endpoint of each wall. Since ABI 24 the graph rejects three cases; since ABI 25 it rejects four. A rejection returns UC_Result_InvalidArgument with a zero count and leaves the graph unchanged. Both wall meshes remain byte-for-byte identical, so nothing needs to be refreshed.

ReasonCondition
DifferentStoriesThe walls belong to different stories (ABI 25), even if those stories have the same elevation. This is checked first.
ParallelThe centre lines are parallel or collinear in plan, so there is no single intersection point. This also covers a wall whose endpoints lie directly above one another.
DifferentLevelsThe endpoints are not at the same elevation: any of the four differs in Z by more than 0.01 cm. A sloped wall is rejected as well.
TooFarAn endpoint would need to move in plan by more than twice the length of its wall, which is the near-parallel case. Shortening never reaches this limit.

There are no separate result codes for these cases (lead decision dated 08.10). The reason is available through UC_GetLastError (ABI 25), including its name, and is identical in Debug and Release. The logger also receives a line at UC_LogLevel_Warning with category "API"; a Release core does not emit Warning (see “Logging”), so the line is absent there, but the error text remains available.

The same result is returned when a wall is intersected with itself or when a wall is not live in the supplied house.

Meshes

A mesh is built on request from the graph and the supplied UV mapping:

FunctionUV mapping
UC_GetWallMeshLeft and right faces
UC_GetSlabMeshTop, bottom, and edge
UC_GetJoineryMeshIncluded in the type parameters

Passing NULL for UV mapping selects the default: scale 1, no rotation, and no offset.

The buffer is owned by the core and remains valid until the next call on the same thread. The caller must copy it immediately; no cross-boundary release function is provided.

Read the “KMSH” block (Types/APIMesh.h) from beginning to end:

  1. UC_MeshHeader: the KMSH signature, version 3, surface count, and frame relative to story elevation: origin, rotation around Z, and wall length. The frontend (the story object) adds the elevation, not the core.
  2. For each surface: UC_SurfaceHeader, followed by vertices (three float values each), vertex instances (UC_MeshVertexInstance: normal, tangent, UV), and indices (uint32_t, three per triangle).

Vertices are expressed in the block's frame. The surface number (Side) identifies the part: left and right for a wall; top, bottom, and edge for a slab; frame, sash, and glass for joinery.

Empty meshes

A node with no surface is not an error (ABI 23). A wall whose entire face is cut away by an opening, and a slab whose contour cannot be triangulated or is fully consumed by its openings, continue to exist and retain their handles; the wall also continues to trim its neighbours. UC_GetWallMesh and UC_GetSlabMesh return UC_Result_Success and a block of exactly sizeof(UC_MeshHeader), which is 36 bytes:

FieldValue
Magic, VersionAs always: KMSH, 3
SurfaceCount0
Origin, RotationYaw, LengthThe node's values, as in a full mesh (RotationYaw and Length are zero for a slab)

A surface with no triangles is omitted completely; the block never contains a surface with zero counts. Therefore:

  • Always read the surface count from SurfaceCount; do not assume it. At present, a wall returns two surfaces or none. The one-surface case (one empty face) currently returns UC_Result_InternalError (B-67). Once fixed, a one-surface block will also be possible, so code must continue to read SurfaceCount; Side identifies which surface was returned.
  • An empty block means “render nothing”: the frontend removes the object's old mesh but retains the object and its handle. After UC_DeleteOpening or another edit, the same handle may return surfaces again.
  • A face split into pieces by an opening remains one surface with multiple islands: triangles that do not share a vertex.

Openings and joinery

An opening is a resource with its own handle: it survives movement, intersection, and wall-thickness changes, and is destroyed with its wall. UC_GetWallOpenings returns a wall's openings in creation order.

The opening is cut from the wall face, and nothing checks whether it lies inside that face. Its rectangle may extend beyond the face, split it in two horizontally or vertically, or cover it completely. All of these return UC_Result_Success with an opening handle; the last case produces an empty mesh (see “Empty meshes”). Shortening a wall so it no longer reaches its opening is also a successful edit: the opening remains live and returns to the face once the wall reaches it again.

One case is unsupported: an opening vertex in an intersection zone, between the trimmed edges of the two faces. The resulting geometry is incorrect; until this is fixed, the frontend must prevent such openings.

Joinery has no handle. Its type and parameters belong to the frontend, while UC_GetJoineryMesh builds the mesh from the opening and those parameters: width and height come from the opening, while placement and rotation come from the wall. The frontend must therefore request the joinery mesh again after every command that changes its wall.

Depth across the wall currently does not come from the wall:

TypeFrame thicknessFrame widthAcross the wall, relative to the frame
Windows (single-sash, double-sash)ThicknessCmFrameWidthCmCentred: −ThicknessCm/2 … +ThicknessCm/2
Door10 cm; ThicknessCm is ignored3 cm; FrameWidthCm is ignoredAt the +Y face: WallWidth/2 − 10 … WallWidth/2

ThicknessCm = 0 is accepted, but “through the entire wall” is not implemented: the window has zero depth. MullionWidthCm = 0 is also accepted, but does not produce overlapping sashes. The mullion has zero width, and the glass is not split by the mullion in any case.

The mesh frame begins at the opening's start point on the wall centre line, shifted by AxisOffsetCm along +Y. In joinery-local coordinates (X along the wall from the opening start, 0 … Length; Y across the wall, with +Y at the left face; Z up), mirroring works as follows:

SidePanelMirrorPoint (x, y, z) becomesAxisOffsetCm
Left0(x, y, z)+offset
Right0(x, −y, z), mirrored across the centre line−offset
Left1(Length − x, y, z), mirrored across the opening midpoint+offset
Right1(Length − x, −y, z), rotated 180° around the vertical axis through (Length/2, 0)−offset

The single-sash window applies neither Side nor PanelMirror to its mesh. The API nevertheless reverses the offset for Side = Right, so the window moves to −offset without mirroring (its current box is symmetrical, so the shape does not differ). This is a known limitation, not a contract.

Boundary rules

  • No C++ classes, STL containers, or exceptions cross the boundary.
  • Output parameters are zeroed on failure, so the caller may read them even then.
  • Every failure leaves a reason in UC_GetLastError; every success clears it.
  • Numeric validation happens before core initialization is checked: invalid numeric input returns UC_Result_InvalidArgument even when the core is not initialized.
  • A Python ctypes wrapper uses c_int32 for UC_Result, c_uint64 for handle fields, and c_float for coordinates and dimensions.
  • On Windows, symbols are exported with __declspec(dllexport). On GCC and Clang, internal symbols are hidden and C symbols are visible.

Tests

Source/API/Tests contains three kinds of tests:

  • CApiTests.cpp: boundary behaviour, including lifecycle, houses, stories, walls, openings, joinery, slabs, and byte-by-byte decoding of the “KMSH” block as the frontend sees it.
  • CApiHeadersCompileTest.c: verifies that the headers compile as C and that structure sizes match expectations.
  • ApiGuardTests.cpp: tests the boundary guard itself (ApiGuard.h). Kristeni::CheckError and every other exception become UC_Result_InternalError, with a reason in the thread-local message (ApiLastError.h); success clears it, and a failure without a specific reason receives at least its result-code name. This is the only test here that includes an internal header. A failed check must be unreachable from valid input through the C functions, so the guard is tested directly.