Subscreens: Difference between revisions
No edit summary |
|||
| Line 74: | Line 74: | ||
</pre> | </pre> | ||
For example, a 320 by 120 subscreen requires about 153 | For example, a 320 by 120 subscreen requires about 153 kilobytes of pixel storage before allocation overhead. Surface dimensions should match the composition rather than the full display unless a full-screen effect is actually required. | ||
=== Lifetime and ownership === | === Lifetime and ownership === | ||
Revision as of 01:05, 16 August 2026
Subscreens are script-allocated, off-screen 32-bit drawing surfaces. Scripts can compose sprites, text, primitive shapes, and selected parts of the sprite queue on a subscreen, then submit the completed result to the main drawing queue with drawscreen().
Unlike queued draw commands, pixels written to a subscreen remain there until they are cleared, overwritten, or the screen is released. This retained surface makes subscreens useful both as composition workspaces and as temporary graphic caches.
Common uses include:
- Picture-in-picture displays.
- Mirrors, monitors, and video marquees.
- Animated title and menu compositions.
- Full-screen rotation or scaling.
- Localized color and distortion effects.
- Layer capture and recomposition.
- Custom HUD elements.
- Scripted cutscenes.
- Perspective and landscape effects.
- Temporary graphic caches.
- Anaglyphs, mosaic transitions, multilayer masking, and other composite effects.
- Feedback effects such as trails and “Predator”-style invisibility.
- Movie containers.
How subscreens work
Subscreen rendering normally follows five stages.
| Stage | Purpose |
|---|---|
| Allocate | Create a 32-bit surface with allocscreen(). Reusable surfaces should be allocated once during setup.
|
| Build | Write sprites, text, dots, lines, and boxes directly to the surface. drawspriteq() can also render selected queued layers into it.
|
| Process | Retain, clear, redraw, recolor, scale, rotate, clip, or otherwise transform the composition as needed. |
| Submit | Use drawscreen() to place the completed surface in the main depth-sorted drawing queue.
|
| Release | Call free() when the script-owned surface is no longer needed.
|
Drawing into a subscreen is immediate. Drawing the completed subscreen with drawscreen() is queued. Call order controls overlap inside the surface, while the Z value passed to drawscreen() controls where the completed surface appears among other queued graphics.
Allocating a subscreen
allocscreen
void screen = allocscreen(width, height);
allocscreen() creates a script-owned screen in 32-bit pixel format and clears its pixels to 0. The returned handle can be passed to screen drawing, inspection, queue capture, and composition functions.
OpenBOR stores screen rows at widths divisible by four. Requested widths are rounded down to the nearest multiple of four, so creators should choose an aligned width deliberately.
void panel = allocscreen(320, 120);
int actual_width = getgfxproperty(panel, "width");
int actual_height = getgfxproperty(panel, "height");
Keep the handle for as long as the surface is needed. Repeatedly allocating and releasing the same working surface during normal updates creates needless allocation work.
Memory use
Pixel storage requires approximately:
width * height * 4 bytes
For example, a 320 by 120 subscreen requires about 153 kilobytes of pixel storage before allocation overhead. Surface dimensions should match the composition rather than the full display unless a full-screen effect is actually required.
Lifetime and ownership
Surfaces returned by allocscreen() belong to the script and should be released with free() after their useful lifetime ends. Engine-owned screens such as vscreen are borrowed handles and must never be passed to free().
Do not release a subscreen immediately after submitting it with drawscreen(). The queued entry still refers to that surface until the frame is rendered.
The following scripts allocate a reusable panel at level start, rebuild and submit it during updates, then release it at level end.
level.c
void main()
{
void panel = allocscreen(160, 48);
setglobalvar("subscreen_panel", panel);
}
updated.c
void main()
{
void panel = getglobalvar("subscreen_panel");
int no_blend = openborconstant("BLEND_MODE_NONE");
if(isempty(panel)) {
return;
}
clearscreen(panel);
drawboxtoscreen(panel, 0, 0, 160, 48,
rgbcolor(24, 28, 36), no_blend);
drawlinetoscreen(panel, 0, 0, 159, 0,
rgbcolor(255, 192, 64), no_blend);
drawstringtoscreen(panel, 8, 8, 0, "Subscreen example");
drawscreen(panel, 80, 24, 1000, no_blend);
}
endlevel.c
void main()
{
void panel = getglobalvar("subscreen_panel");
if(!isempty(panel)) {
free(panel);
setglobalvar("subscreen_panel", NULL());
}
}
Drawing into a subscreen
Target-screen functions write pixels as soon as they are called. They do not enter the sprite queue and do not accept a Z value. Later calls appear above earlier calls wherever their pixels overlap.
| Function | Purpose | Notes |
|---|---|---|
drawspritetoscreen(sprite, screen, x, y)
|
Draws a sprite directly to the target screen. | Sprite comes before screen - the argument order differs from the other target-screen functions. Uses the common drawmethod. |
drawstringtoscreen(screen, x, y, font, value)
|
Draws text directly to the target screen. | Uses the selected OpenBOR font. Common drawmethod transformations do not apply to this function. |
drawdottoscreen(screen, x, y, color, blend)
|
Writes one pixel. | Blend is optional. Repeated dots are the foundation for curves, plots, particles, custom rasterizers, and irregular procedural shapes. |
drawlinetoscreen(screen, x1, y1, x2, y2, color, blend)
|
Draws a line between two points. | Blend is optional. Uses the common drawmethod when enabled. |
drawboxtoscreen(screen, x, y, width, height, color, blend)
|
Draws a filled rectangle. | Blend is optional. Uses the common drawmethod when enabled. |
Use rgbcolor() to construct portable 32-bit colors.
void canvas = getglobalvar("composition_canvas");
void marker = getglobalvar("composition_marker");
int no_blend = openborconstant("BLEND_MODE_NONE");
clearscreen(canvas);
drawboxtoscreen(canvas, 0, 0, 128, 64,
rgbcolor(16, 20, 28), no_blend);
drawlinetoscreen(canvas, 8, 48, 120, 16,
rgbcolor(64, 192, 255), no_blend);
drawdottoscreen(canvas, 64, 32,
rgbcolor(255, 255, 255), no_blend);
drawspritetoscreen(marker, canvas, 64, 32);
drawstringtoscreen(canvas, 8, 8, 0, "READY");
The sprite and screen in drawspritetoscreen() must remain valid while the call runs. Once drawn, the resulting pixels belong to the subscreen - later release of the source sprite does not erase pixels already written there.
Clearing and retaining pixels
clearscreen
clearscreen(screen);
clearscreen() sets every pixel on the supplied surface to 0. It does not release or resize the screen.
Clear before rebuilding a composition when the new frame should replace the old one. Skip the clear deliberately when earlier pixels should remain, such as paint tools, accumulated maps, trails, damage marks, or feedback effects.
Static content can be drawn once and submitted many times. Dynamic content only needs regeneration when its underlying state changes.
void panel;
int panel_dirty = 1;
void rebuild_panel()
{
int no_blend = openborconstant("BLEND_MODE_NONE");
clearscreen(panel);
drawboxtoscreen(panel, 0, 0, 192, 64,
rgbcolor(20, 24, 32), no_blend);
drawstringtoscreen(panel, 12, 12, 0, "OPTIONS");
panel_dirty = 0;
}
void main()
{
int no_blend = openborconstant("BLEND_MODE_NONE");
if(panel == NULL()) {
panel = allocscreen(192, 64);
}
if(panel_dirty) {
rebuild_panel();
}
drawscreen(panel, 64, 40, 1000, no_blend);
}
Inspecting a subscreen
getgfxproperty() reads dimensions, format information, and pixel values from a subscreen.
| Property | Return | Subscreen behavior |
|---|---|---|
width
|
Integer | Actual allocated width after four-pixel alignment. |
height
|
Integer | Allocated height. |
srcwidth
|
Integer | Same value as width for a screen.
|
srcheight
|
Integer | Same value as height for a screen.
|
centerx
|
Integer | Returns 0 for a screen. |
centery
|
Integer | Returns 0 for a screen. |
palette
|
Pointer | Returns the screen's palette pointer. Indexed palette data is normally unnecessary for a 32-bit subscreen. |
pixelformat
|
Integer | Returns the native screen format. Script-allocated subscreens use 32-bit format. |
pixel
|
Integer | Returns the packed pixel value at the supplied X and Y coordinates. Out-of-bounds coordinates return 0. |
The pixel property requires coordinates.
int width = getgfxproperty(screen, "width");
int height = getgfxproperty(screen, "height");
int sample = getgfxproperty(screen, "pixel", 4, 6);
Pixel inspection supports procedural masks, collision-like image tests, analysis tools, and effects that react to generated artwork.
Capturing the sprite queue
drawspriteq
drawspriteq(screen, newonly, minz, maxz, dx, dy);
drawspriteq() renders entries from the current sprite queue into a supplied screen. This turns already-queued scenery, entities, effects, primitives, and other surfaces into pixels that can be retained or processed as one composition.
Only the first argument is required. Optional arguments default to 0 for newonly, the full Z range, and 0 for both offsets.
| Argument | Description |
|---|---|
screen
|
Destination surface. Passing an empty or null handle selects vscreen.
|
newonly
|
When nonzero and the queue is locked, skips entries that were already present at the lock point. Ordinary scripts normally use 0. |
minz
|
Lowest Z value to render, inclusive. |
maxz
|
Highest Z value to render, inclusive. |
dx
|
Horizontal offset added to every rendered queue entry. |
dy
|
Vertical offset added to every rendered queue entry. |
The next example captures a 160 by 96 area from queue layers 100 through 900, shifts the selected scene into local subscreen coordinates, then places it as a monitor display.
void monitor = getglobalvar("monitor_screen");
int no_blend = openborconstant("BLEND_MODE_NONE");
clearscreen(monitor);
drawspriteq(monitor, 0, 100, 900, -80, -40);
drawscreen(monitor, 144, 16, 1200, no_blend);
Captured entries remain in the queue. They will still render normally unless another operation intentionally changes or clears the queue. Routine scripts should avoid clearspriteq() because native backgrounds, entities, shadows, and interface elements share the same queue.
Queue capture reflects only entries available when drawspriteq() runs. Scripts that need native game layers normally perform the capture from updated.c, after those layers have been queued and before the final queue render.
Submitting a subscreen
drawscreen
drawscreen(screen, x, y, z, blend);
drawscreen() adds the completed surface to the main sprite queue. The blend argument is optional.
| Argument | Description |
|---|---|
screen
|
Completed screen handle to submit. |
x
|
Horizontal position of the surface in the destination view. |
y
|
Vertical position of the surface in the destination view. |
z
|
Queue depth. Larger values normally render later and appear above smaller values. |
blend
|
Optional blend mode for the completed surface. |
Supplying the blend argument has an important transparency effect. OpenBOR starts from the plain drawmethod, applies the selected blend, and treats zero-valued pixels as transparent. Passing BLEND_MODE_NONE therefore gives an unblended surface whose cleared background remains transparent.
Omitting the blend argument copies the current common drawmethod instead. Under the default common method, zero-valued pixels are copied with the rest of the rectangular surface and may appear as an opaque black background.
int no_blend = openborconstant("BLEND_MODE_NONE");
/* Transparent zero-valued background, without color blending. */
drawscreen(panel, 40, 20, 1000, no_blend);
/* Uses the current common drawmethod as configured. */
drawscreen(panel, 40, 20, 1000);
Choose the form intentionally. Rectangular full-screen images may need opaque zero pixels, while irregular interfaces and effects usually need transparent cleared areas.
Transforming the completed surface
The common drawmethod can transform an entire composition when it is submitted. This is one of the most powerful subscreen techniques: many detailed internal elements can rotate, scale, flip, clip, tint, fade, repeat, or distort together as one surface.
Queued entries receive a copy of the common drawmethod when drawscreen() is called. Configure the shared method, submit the surface, then restore the default state.
void common = openborvariant("drawmethod_common");
void defaults = openborvariant("drawmethod_default");
int config = openborconstant("DRAWMETHOD_CONFIG_ENABLED")
| openborconstant("DRAWMETHOD_CONFIG_BACKGROUND_TRANSPARENCY");
copy_drawmethod(common, defaults);
set_drawmethod_property(common,
openborconstant("DRAWMETHOD_PROPERTY_CONFIG"),
config);
set_drawmethod_property(common,
openborconstant("DRAWMETHOD_PROPERTY_CENTER_X"),
80);
set_drawmethod_property(common,
openborconstant("DRAWMETHOD_PROPERTY_CENTER_Y"),
48);
set_drawmethod_property(common,
openborconstant("DRAWMETHOD_PROPERTY_SCALE_X"),
384);
set_drawmethod_property(common,
openborconstant("DRAWMETHOD_PROPERTY_SCALE_Y"),
384);
set_drawmethod_property(common,
openborconstant("DRAWMETHOD_PROPERTY_ROTATE"),
16);
drawscreen(monitor, 160, 100, 1200);
copy_drawmethod(common, defaults);
Scale values use 256 as 100 percent, so 384 produces 150 percent scaling. See Drawmethod for the full property set, including channel control, tinting, clipping, repetition, and water or perspective effects.
Passing an explicit blend argument to drawscreen() starts from the plain method instead of the configured common method. Omit the blend argument when applying common drawmethod transformations, then include background transparency in the drawmethod configuration when zero pixels should remain transparent.
Layering and nested composition
Subscreens can participate in several composition passes. Completed surfaces can be queued with drawscreen(), captured into another surface with drawspriteq(), transformed, and submitted again. This enables nested interfaces, monitor-within-monitor effects, layered masks, and recursive visual processing.
No dedicated drawscreentoscreen() function exists. Screen nesting is performed through the queue: submit one subscreen, then render the applicable Z range into another target with drawspriteq().
Use separate source and destination surfaces for feedback effects when a pass needs to read the previous result while building the next one. Alternating between two buffers avoids reading and writing the same pixel storage during one composition step.
Technique patterns
| Effect | Subscreen pattern |
|---|---|
| Picture-in-picture or monitor | Capture a queue layer range into a small surface, then submit it at the desired interface depth. |
| Mirror or reflection | Capture the applicable scene range, flip or distort the completed surface with the common drawmethod, then submit it behind the foreground. |
| Cached HUD or menu | Draw static panel art once, rebuild only changed values or sections, and submit the retained surface each frame. |
| Localized recolor | Compose the affected elements separately, then apply tint or channel properties when submitting the surface. |
| Rotation or scaling | Assemble the scene at convenient local coordinates and transform the complete result with one drawscreen() call.
|
| Mosaic transition | Compose or capture the source image, then draw clipped, scaled, or offset portions through controlled passes. |
| Perspective landscape | Build a wide source surface and use drawmethod water, perspective, span, or scale properties during final composition. |
| Trails or invisibility | Retain selected pixels between updates, fade or distort the prior result, and composite the current subject into the next pass. |
| Multilayer mask | Build color, mask, and effect passes separately, then combine them through queued captures and blend modes. |
Performance and queue use
Large subscreens cost memory and fill bandwidth, but a completed surface consumes only one sprite-queue entry when submitted with drawscreen(), allowing subscreens to act as sprite queue multipliers. Keep off-screen surfaces near the size of the content being composed, use widths divisible by four, and redraw only when their contents need to change.
The standard 5,000-entry sprite queue already provides ample headroom for ordinary games, including object-heavy and bullet-heavy designs. Queue consolidation is therefore a useful capability, not a requirement for typical object counts. Subscreens are most valuable for retained composition, whole-surface effects, layer capture, and avoiding repeated construction work.
Pixel cost still applies while building the surface. Hundreds of direct draws merged into one queued item still perform their raster work, and large transformed surfaces require proportionally more memory traffic. Favor smaller working areas, cached static content, and deliberate rebuild points.
Common mistakes
| Symptom | Cause | Correction |
|---|---|---|
| Memory or allocation churn grows during play | The script calls allocscreen() every update.
|
Allocate once, retain the handle, and reuse the same surface. |
| Old pixels remain behind moving artwork | The retained surface is not cleared before a replacement frame is drawn. | Call clearscreen() before rebuilding, unless accumulation is intentional.
|
| Elements appear in the wrong order inside the subscreen | Immediate target-screen drawing has no Z sort. | Issue draw calls from back to front, or queue and capture a sorted range with drawspriteq().
|
drawspritetoscreen() fails or receives the wrong handles
|
The sprite and screen arguments were reversed. | Use drawspritetoscreen(sprite, screen, x, y).
|
| Cleared areas appear as opaque black | drawscreen() copied zero-valued pixels under a common method without background transparency.
|
Pass an explicit blend such as BLEND_MODE_NONE, or enable DRAWMETHOD_CONFIG_BACKGROUND_TRANSPARENCY in the common drawmethod.
|
| Queue capture is empty or incomplete | Required entries had not been queued when drawspriteq() ran, or the Z range excluded them.
|
Capture later in the render setup, commonly from updated.c, and verify minz and maxz.
|
| Engine rendering disappears | The script called clearspriteq() during the normal shared render pass.
|
Leave the shared queue intact unless a controlled custom renderer intentionally replaces it. |
| Invalid pointer errors or unstable rendering occur | The script freed an engine-owned screen, reused a released handle, or released a queued screen before rendering completed. | Free only script-owned surfaces after their final rendered use, then replace stored handles with NULL().
|
Function reference
| Function | Role |
|---|---|
allocscreen(width, height)
|
Allocates and clears a script-owned 32-bit subscreen. |
clearscreen(screen)
|
Sets every pixel on the supplied screen to 0. |
drawspritetoscreen(sprite, screen, x, y)
|
Draws a sprite immediately into a screen. |
drawstringtoscreen(screen, x, y, font, value)
|
Draws text immediately into a screen. |
drawdottoscreen(screen, x, y, color, blend)
|
Draws one pixel immediately into a screen. Blend is optional. |
drawlinetoscreen(screen, x1, y1, x2, y2, color, blend)
|
Draws a line immediately into a screen. Blend is optional. |
drawboxtoscreen(screen, x, y, width, height, color, blend)
|
Draws a filled box immediately into a screen. Blend is optional. |
drawspriteq(screen, newonly, minz, maxz, dx, dy)
|
Renders all or part of the current sprite queue into a selected screen. Arguments after screen are optional. |
drawscreen(screen, x, y, z, blend)
|
Submits a completed screen to the main drawing queue. Blend is optional. |
getgfxproperty(screen, property, ...)
|
Reads dimensions, pixel format, palette, or pixel data. |
free(screen)
|
Releases a script-owned subscreen after its useful lifetime ends. |