mirror of
https://codeberg.org/virtualxt/virtualxt.git
synced 2026-10-23 14:40:58 +00:00
Bug fixes: - pic: honor auto-EOI mode (ICW4 bit 1); ISR bit now only set when auto-EOI is disabled (previously set unconditionally) - spec: correct port 0xB0-in polarity (0x00 = hard disk, 0xFF = floppy) to match both the Odin and C implementations - debug: gate cga_regs/cga_vram handlers on the video adapter actually being CGA (returned garbage when EGA/HGC was active) Dead code removal: - unused public APIs: vxt_cpu_flush_prefetch, vxt_bus_set_pic, vxt_uart_registers, vxt_uart_set_error, vxt_disk_bootstrap - write-only CPU instruction-stream fields + MAX_INSTRUCTION_SIZE - unused macros VXT_CPU_USE_PREFETCH, MBOX_TOTAL_SIZE - debug server: g_active_server, single_step, last_segments, SEG_TAG_* - UART iir field, always-false ega_config stub, empty destroy stubs (joystick, UART, mouse), exec_wait/exec_esc folded into exec_nop - frontend: BIOS_ROM_BASE/VXTX_ROM_BASE, mixer HOSTFS_DURATION/ HOSTFS_VOLUME/seek_volume, unreachable clamps, ini_parse return value - misc dead stores/clamps (hostfs, hgc), dangling comment (cpu.c) Makefile: drop 12 redundant pattern rules, replace dead check-circle test with a real compiler check, drop duplicated distclean line, complete .PHONY, collapse flag lists, develop honors LIBRETRO_OUT. STATUS.md: correct stale module test counts (110 -> 119).
477 lines
21 KiB
Markdown
477 lines
21 KiB
Markdown
# VirtualXT -- Specification
|
|
|
|
## Philosophy
|
|
|
|
VirtualXT shall be a Turbo PC/XT emulator. Its design shall be governed by three
|
|
principles:
|
|
|
|
- **Simplicity.** Every component shall have one job and one interface. The
|
|
peripheral system shall have one vtable. The bus shall have one dispatch
|
|
mechanism. The CPU shall have one decode-execute loop. When a simpler design
|
|
can do the job, it wins.
|
|
|
|
- **Minimalism.** No external dependencies beyond the libretro API header.
|
|
No threading. No dynamic plugin loading. No abstraction layers that
|
|
don't map directly to hardware concepts.
|
|
|
|
- **Transparency.** A developer familiar with PC/XT hardware should be able
|
|
to read any module and recognize the hardware it models. The emulator's
|
|
structure shall mirror the machine it emulates.
|
|
|
|
The implementation language shall be C11. The core emulator shall compile in a
|
|
freestanding environment (`-ffreestanding`) with no standard library
|
|
dependency, ensuring portability to bare-metal targets. Optional modules
|
|
(networking, debugging) and unit tests may use hosted facilities such as
|
|
threads and sockets, but these shall never be required for the core to build
|
|
and run.
|
|
|
|
|
|
## Design Characteristics
|
|
|
|
Several choices shall be applied consistently across the codebase:
|
|
|
|
1. Freestanding-first core design
|
|
2. Flat lookup-table dispatch instead of dynamic discovery chains
|
|
3. Statically allocated singleton-style system state
|
|
4. Small, explicit interfaces between layers
|
|
5. Minimal dependencies and minimal hidden control flow
|
|
|
|
The intended result is a codebase where major behavior is easy to trace from
|
|
the frontend entry points down to CPU, bus, and peripheral interactions.
|
|
|
|
|
|
## System Overview
|
|
|
|
The emulator shall be organized as four layers:
|
|
|
|
┌──────────────────────────────────────────┐
|
|
│ Frontend │
|
|
│ (libretro callbacks, frame loop) │
|
|
├──────────────────────────────────────────┤
|
|
│ Machine │
|
|
│ (step loop, timer dispatch, reset) │
|
|
├────────┬────────────────────────┬────────┤
|
|
│ CPU │ Bus │ Timers │
|
|
│8088/V20│ memory map · I/O map │ │
|
|
├────────┴────────────────────────┴────────┤
|
|
│ Peripheral Modules │
|
|
│ PIT · PIC · PPI · DMA · ROM · CGA · … │
|
|
└──────────────────────────────────────────┘
|
|
|
|
### Frontend
|
|
|
|
The frontend shall be the boundary between the emulator and the host
|
|
environment. In normal use this shall be the libretro integration layer.
|
|
|
|
Its responsibilities shall be:
|
|
|
|
1. Accept host callbacks for video, audio, input, timing, and file access
|
|
2. Create and initialize the emulated machine
|
|
3. Apply runtime configuration and machine setup
|
|
4. Drive frame-by-frame execution
|
|
5. Present audio/video output back to the host
|
|
|
|
At a high level, the frontend shall own host integration while the emulator core
|
|
shall own emulation behavior.
|
|
|
|
### Machine
|
|
|
|
The machine shall be the central coordinator for the emulated system.
|
|
|
|
Its responsibilities shall be:
|
|
|
|
1. Own global emulator lifecycle operations such as create, init, reset, and destroy
|
|
2. Register and initialize peripherals
|
|
3. Step the CPU forward by a requested cycle budget
|
|
4. Advance timers alongside CPU execution
|
|
5. Route configuration requests to the appropriate layer
|
|
|
|
The machine shall be the point where the frontend's frame-level view meets the
|
|
core's cycle-level execution model. The machine shall be a singleton: all state
|
|
shall be module-scoped. There shall be exactly one machine per process.
|
|
|
|
### Core Services
|
|
|
|
Core services shall be the internal mechanisms shared by the whole emulator:
|
|
|
|
1. CPU instruction execution
|
|
2. Bus dispatch for memory and I/O
|
|
3. Timer scheduling and updates
|
|
4. Interrupt routing
|
|
|
|
These services shall form the runtime backbone of the emulator. They shall not
|
|
model a single external device; instead, they shall provide the execution environment in
|
|
which peripherals and the CPU interact.
|
|
|
|
### Peripheral Modules
|
|
|
|
Peripheral modules shall represent hardware devices attached to the machine.
|
|
Examples include memory-mapped devices, I/O devices, video, storage, and
|
|
support chips.
|
|
|
|
A module may instantiate multiple peripherals. For example, a VGA module
|
|
could register both a video peripheral (for the CRTC and attribute
|
|
controller I/O) and a ROM peripheral (for the VGA BIOS). Each peripheral
|
|
still implements the same vtable interface independently.
|
|
|
|
Each peripheral shall follow the same broad model:
|
|
|
|
1. It exposes a common interface to the machine and bus
|
|
2. It shall register the address ranges or ports it owns
|
|
3. It shall maintain its own device state
|
|
4. It shall react to reads, writes, resets, timers, and configuration as needed
|
|
5. It may read frontend-provided resources via the Frontend API during `install()`
|
|
|
|
This shall keep the system modular without introducing deep abstraction layers.
|
|
|
|
|
|
## Runtime Flow
|
|
|
|
The emulator shall run in a simple top-down flow:
|
|
|
|
1. The frontend shall receive a frame callback from the host
|
|
2. It shall gather input and determine how many emulated CPU cycles to run
|
|
3. It shall ask the machine to execute that cycle budget
|
|
4. The machine shall repeatedly step the CPU and update timers
|
|
5. CPU memory and I/O operations shall be dispatched through the bus to peripherals
|
|
6. Peripherals shall update internal state and may raise interrupts or produce output
|
|
7. The frontend shall submit the resulting audio and video to the host
|
|
|
|
This structure shall keep host concerns separated from core emulation while still
|
|
allowing the whole system to advance in a predictable loop.
|
|
|
|
|
|
## Bus
|
|
|
|
The bus shall be two flat arrays that map addresses to peripheral indices.
|
|
|
|
**Memory map.** The 8088 has a 20-bit address space (1 MB). The memory map
|
|
shall divide this into 16-byte pages, yielding 65,536 entries. Each entry shall
|
|
hold the index of the peripheral responsible for that page. A memory access
|
|
shall mask the address to 20 bits, shift right by 4 to find the page, look up
|
|
the peripheral index, and call that peripheral's read or write callback.
|
|
|
|
**I/O map.** The 8088 has a 16-bit I/O port space (65,536 ports). The I/O
|
|
map shall have one entry per port, each holding a peripheral index. Port access
|
|
shall look up the index directly and call the peripheral's I/O callback.
|
|
|
|
**Default peripheral.** Peripheral index 0 shall be special. It shall be created
|
|
first and claim the entire memory and I/O space. It shall provide 1 MB of RAM
|
|
and return open-bus values (0xFF) for all I/O reads. Every other peripheral
|
|
shall overlay its ranges on top by writing its own index into the map entries.
|
|
This means unregistered regions shall always fall through to sensible defaults
|
|
without any conditional logic in the dispatch path.
|
|
|
|
The lookup tables shall be the only dispatch mechanism. There shall be no linked
|
|
lists, priority chains, or fallback searches.
|
|
|
|
|
|
## Peripheral Interface
|
|
|
|
Every hardware device -- RAM, ROM, timers, interrupt controller, video
|
|
adapter, disk controller -- shall implement the same interface. This shall be a
|
|
struct of function pointers (a vtable) with the following callbacks:
|
|
|
|
**Lifecycle:**
|
|
- `install` -- called once; the peripheral shall register its memory and I/O
|
|
ranges by writing its index into the bus lookup tables.
|
|
- `reset` -- shall restore the peripheral to power-on state.
|
|
- `destroy` -- shall free resources.
|
|
- `config` -- shall accept a section/key/value triple for runtime configuration.
|
|
The section corresponds to an INI section name during data-driven setup.
|
|
- `name` -- shall return a human-readable identifier.
|
|
|
|
**Memory bus:**
|
|
- `read(address)` -- shall return a byte from the peripheral's address space.
|
|
- `write(address, byte)` -- shall store a byte.
|
|
|
|
**I/O bus:**
|
|
- `io_in(port)` -- shall return a byte from an I/O port.
|
|
- `io_out(port, byte)` -- shall write a byte to an I/O port.
|
|
|
|
**Timer:**
|
|
- `timer(id, cycles)` -- shall be called when a registered timer fires.
|
|
|
|
**Interrupt controller (PIC only):**
|
|
- `pic_irq(line)` -- shall assert an interrupt request line.
|
|
- `pic_next` -- shall return the highest-priority pending interrupt vector, or -1.
|
|
|
|
**DMA controller (DMA only):**
|
|
- `dma_read(channel)` / `dma_write(channel, byte)` -- DMA transfer
|
|
callbacks.
|
|
|
|
All callbacks shall be nullable. The bus shall check for null and substitute safe
|
|
defaults: 0xFF for reads, no-op for writes.
|
|
|
|
**Device state** shall be attached by embedding the vtable struct as the first
|
|
field of a larger device-specific struct. Because the vtable is at offset
|
|
zero, a pointer to either type shall be interchangeable. This gives each device
|
|
private state with zero indirection cost and no separate allocation.
|
|
|
|
**Class tags.** Each peripheral shall carry a bitmask of class flags (generic,
|
|
PIC, DMA, PPI, PIT, video, audio, etc.). The machine shall use these to
|
|
discover specific peripherals -- for example, locating the PIC for
|
|
interrupt routing. Peripherals may also read frontend-provided
|
|
configuration via the Frontend API during `install()`.
|
|
|
|
The system shall support up to 64 peripherals. This is more than sufficient for
|
|
a PC/XT and shall allow the index to fit in a single byte in the lookup tables.
|
|
|
|
|
|
## Frontend API
|
|
|
|
Modules may occasionally need configuration or resources that cannot be
|
|
expressed through the peripheral vtable alone. The Frontend API shall be a set
|
|
of global configuration functions that sit alongside the peripheral manager.
|
|
Modules shall read these values during `install()`; the frontend shall set them before
|
|
`vxt_machine_init()`.
|
|
|
|
This shall be intentionally separate from the vtable because it represents
|
|
module ↔ frontend communication, not module ↔ machine communication.
|
|
The peripheral vtable shall handle runtime interaction; the Frontend API shall handle
|
|
setup-time resource binding such as framebuffers, file loaders, and logging.
|
|
|
|
Global setters shall be called once by the frontend before machine init.
|
|
Global getters shall be used by modules during `install()` to discover
|
|
frontend-provided resources. This eliminates the need for type-specific
|
|
setup functions or module-specific global pointers in the frontend.
|
|
|
|
|
|
## CPU
|
|
|
|
The CPU shall model an Intel 8088 processor implementing the 8086 instruction
|
|
set, with optional NEC V20 emulation. It shall follow the **decode-dispatch** pattern:
|
|
|
|
1. **Prepare** -- shall clear per-instruction state and set default segment bases.
|
|
2. **Prefix** -- shall consume prefix bytes: segment overrides, LOCK, REP/REPNE.
|
|
3. **Decode** -- shall read the opcode byte, decode operands (ModR/M, immediates,
|
|
displacements), and resolve a function pointer for execution.
|
|
4. **Execute** -- shall call the resolved function.
|
|
5. **REP** -- for string instructions, shall decrement CX and repeat if nonzero
|
|
(with flag checks for REPE/REPNE variants).
|
|
6. **Interrupt check** -- if interrupts are enabled, shall poll the PIC for a
|
|
pending vector and service it.
|
|
7. **Prefetch** -- shall use any remaining bus cycles to speculatively fill the
|
|
prefetch queue from ahead of IP.
|
|
|
|
**Cycle accounting** shall model the 8088's split architecture. The execution
|
|
unit (EU) and bus interface unit (BIU) shall operate in parallel. Memory and
|
|
port accesses shall charge bus cycles; arithmetic shall charge execution cycles.
|
|
The true cost of an instruction shall be the greater of the two.
|
|
|
|
**Cycle lookup tables.** Static cycle costs shall be stored in per-CPU-type
|
|
tables indexed by opcode. A zero entry shall mean dynamic cost computed by the
|
|
handler.
|
|
|
|
**Prefetch queue.** A 4-byte circular buffer shall model the 8088's instruction
|
|
prefetch. Instruction bytes shall be fetched from the queue when available,
|
|
falling back to direct memory reads. After execution, idle bus cycles shall fill
|
|
the queue ahead of the instruction pointer.
|
|
|
|
**Register file.** The 8086 registers (AX/BX/CX/DX with 8-bit halves,
|
|
SP, BP, SI, DI, segment registers, IP, FLAGS) shall be stored in a struct with
|
|
overlapping 8-bit and 16-bit fields. Flag manipulation shall use a 256-entry
|
|
parity lookup table and bitmask operations.
|
|
|
|
**ALU.** Each arithmetic and logic operation (ADD, SUB, AND, OR, XOR,
|
|
shifts, rotates, MUL, DIV, BCD adjustments) shall be a standalone function that
|
|
computes the result and updates the relevant flags. Operations shall be generic
|
|
over byte and word widths.
|
|
|
|
|
|
## Timers
|
|
|
|
Peripherals shall register timers during installation, specifying an interval in
|
|
microseconds. The peripheral manager shall maintain a flat list of timers, each
|
|
tracking accumulated CPU cycles. After every instruction, the machine shall
|
|
advance all timers by the instruction's cycle count. When a timer's
|
|
accumulated cycles reach its interval threshold (converted from microseconds
|
|
using the CPU frequency), the owning peripheral's timer callback shall fire.
|
|
For non-zero intervals, the accumulator subtracts the threshold, carrying
|
|
the remainder forward for timing accuracy. For zero intervals (fire every
|
|
update), the accumulator resets to zero.
|
|
|
|
An interval of zero shall mean the timer fires on every update, which the PIT
|
|
shall use for sub-instruction-granularity counting.
|
|
|
|
|
|
## Interrupts
|
|
|
|
Peripherals shall never interrupt the CPU directly. All hardware interrupts shall route
|
|
through the PIC (Intel 8259):
|
|
|
|
1. A peripheral shall call `interrupt(irq_line)` on the bus.
|
|
2. The bus shall forward this to the PIC, which sets the corresponding request
|
|
bit.
|
|
3. After each CPU instruction, the CPU shall poll the PIC via `next()`.
|
|
4. If the PIC returns a valid vector and interrupts are enabled in FLAGS,
|
|
the CPU shall push FLAGS, CS, and IP onto the stack, clear the interrupt
|
|
flag, and jump to the address in the interrupt vector table.
|
|
|
|
This shall mirror real hardware: the PIC shall arbitrate priority, masks, and
|
|
end-of-interrupt signaling. The CPU shall be unaware of individual IRQ lines.
|
|
|
|
|
|
## Disk Emulation
|
|
|
|
Unlike most PC/XT peripherals, the disk controller shall not be a hardware-level
|
|
device model. Instead, it shall emulate the **BIOS INT 0x13/0x19 interface**
|
|
directly through two I/O ports. This avoids the need to model a real floppy
|
|
controller (Intel 8272/NEC 765) while still providing full disk support for
|
|
DOS and DOS applications.
|
|
|
|
**Architecture.** The disk peripheral shall register I/O ports 0xB0 and 0xB1.
|
|
The VXTX extension ROM (see below) shall hook INT 0x13 and INT 0x19 and
|
|
translate them into port I/O that the disk peripheral shall handle. The disk
|
|
controller shall read CPU registers directly from the machine to get the INT 0x13
|
|
parameters (drive number, CHS address, sector count, buffer address) and
|
|
write results back the same way.
|
|
|
|
**I/O ports:**
|
|
|
|
- **Port 0xB0 out** -- bootstrap. Shall load the first sector of the boot drive
|
|
into memory at 0x7C00.
|
|
- **Port 0xB0 in** -- shall return 0x00 if the boot drive is a hard disk, 0xFF
|
|
if it is a floppy.
|
|
- **Port 0xB1 out** -- shall dispatch the INT 0x13 function in AH. Supported
|
|
functions: reset (0x00), status (0x01), read sectors (0x02), write
|
|
sectors (0x03), verify/format (0x04-0x07), drive geometry (0x08).
|
|
- **Port 0xB1 in** -- shall return 0x00 if drive DL has a disk mounted, 0xFF
|
|
otherwise.
|
|
|
|
**Asynchronous completion protocol.** Because the libretro VFS interface does
|
|
not define an asynchronous I/O mechanism, the disk controller shall support an
|
|
optional guest-visible polling path for frontends where physical device I/O
|
|
may block:
|
|
|
|
1. When `file_api.read()` or `file_api.write()` returns `VXT_DISK_NOT_READY`
|
|
(`-2`), the controller shall record the pending operation and return `AH = 0x80`
|
|
with carry clear.
|
|
2. The VXTX BIOS shall poll port `0xB1`: `0x80` means busy, `0x00` means complete,
|
|
`0xFF` means error. On completion it shall read final `AL`/`AH` from ports
|
|
`0xB2`/`0xB3`.
|
|
3. The frontend shall complete the blocking call outside the emulation thread and
|
|
make the result available on the next `read()`/`write()` call for the same
|
|
drive.
|
|
4. Frontends that never return `VXT_DISK_NOT_READY` shall see no behavioral
|
|
difference.
|
|
|
|
**Drive management.** The controller shall support up to 256 drives (indices
|
|
0x00-0xFF). Floppy drives shall use indices 0x00-0x7F; hard disks shall use 0x80-0xFF.
|
|
On mount, the controller shall auto-detect floppy geometry from well-known image
|
|
sizes (160K, 360K, 720K, 1.2M, 1.44M) or compute hard disk geometry
|
|
(63 sectors, 16 heads, cylinders derived from image size). All addressing
|
|
shall use LBA, computed from the CHS values passed by the BIOS.
|
|
|
|
**File access.** The controller shall not access files directly. Instead, it
|
|
shall receive a `vxt_disk_file_api_t` struct of function pointers (open, read,
|
|
write, seek, tell, close, size, flush, set_led) from the frontend. The
|
|
frontend shall bridge these to the libretro VFS interface.
|
|
|
|
**Configuration.** The disk controller shall accept INI configuration keys under
|
|
the `[disk]` section: `boot` (set boot drive number), `auto` (mount with
|
|
auto-detect), `A` (mount as floppy 0), `C` (mount as hard disk 0x80), and
|
|
`umount` (unmount a floppy drive).
|
|
|
|
|
|
## VXTX Extension ROM
|
|
|
|
VXTX shall be a 2 KB extension ROM mapped at address 0xFD800. It shall be the glue
|
|
between the system BIOS and the emulated disk controller.
|
|
|
|
**Purpose.** A real PC/XT uses a hardware floppy controller with its own
|
|
BIOS extension ROM that hooks INT 0x13. VirtualXT skips the hardware
|
|
controller entirely. VXTX shall fill the same role in software: it installs
|
|
INT 0x13 and INT 0x19 handlers that forward disk requests to the emulated
|
|
disk peripheral via I/O ports.
|
|
|
|
**Initialization.** During POST, the system BIOS shall scan the ROM space for
|
|
expansion ROMs (signature 0x55AA). When it finds VXTX at 0xFD800, it shall call
|
|
the ROM's init entry point. VXTX shall install its interrupt handlers and update
|
|
the BIOS Data Area equipment word at 0x40:0x10 to report one floppy drive
|
|
present.
|
|
|
|
**INT 0x13 handler.** Shall perform `OUT 0xB1, AL` to dispatch the disk request
|
|
to the disk peripheral. The peripheral shall read the function number and
|
|
parameters directly from CPU registers. After the I/O completes, the handler
|
|
shall copy the CPU flags to the interrupt stack frame so the caller sees the
|
|
correct carry flag result, then returns via IRET.
|
|
|
|
**INT 0x19 handler.** Shall perform `OUT 0xB0, AL` to trigger the bootstrap
|
|
sequence (loading sector 1 to 0x7C00). Before the OUT, it patches the
|
|
return address on the stack to 0x0000:0x7C00, so the IRET transfers control
|
|
directly to the loaded boot sector.
|
|
|
|
|
|
## Build Modes
|
|
|
|
VirtualXT shall be organized around a freestanding emulator core, with hosted code
|
|
used only where it is clearly needed.
|
|
|
|
At a high level:
|
|
|
|
1. The core emulator and normal frontend build shall be freestanding-oriented
|
|
2. Tests shall run in a hosted environment
|
|
3. Some development-only tooling shall be enabled only in develop builds
|
|
|
|
This shall keep the main emulator architecture portable while still allowing richer
|
|
debugging and test support during development.
|
|
|
|
|
|
## Testing
|
|
|
|
Tests shall be hosted (they may use libc for stdio, malloc, etc.) and shall run
|
|
against the same `libvxt.a` core library that ships in the emulator.
|
|
|
|
**CPU opcode tests.** The 8088 Machine Opcode Operation (MOO) test suite
|
|
covers the full instruction set. A binary parser shall read `.MOO` files,
|
|
apply initial CPU/memory state, execute to completion, and validate
|
|
final state against expected results. It shall be run with `make test-cpu` (requires
|
|
`make testdata` to fetch the test files).
|
|
|
|
**Module tests.** A lightweight test framework (`module.h`) shall provide
|
|
peripheral-level unit tests that instantiate individual modules, wire them
|
|
to the bus, and exercise their behavior in isolation. Each test file
|
|
(e.g. `test_pic.c`, `test_pit.c`) shall compile into a single `module-runner`
|
|
binary. It shall be run with `make test-modules`.
|
|
|
|
Both suites shall run together via `make test`.
|
|
|
|
|
|
## Optional Debug Server
|
|
|
|
Develop builds may include an optional debug server layer.
|
|
|
|
This shall not be part of the normal emulation path, but an auxiliary development
|
|
feature that can observe machine-visible events and support debugging without
|
|
changing the core architectural split between frontend, machine, core
|
|
services, and peripherals.
|
|
|
|
|
|
## Frontend Effects
|
|
|
|
The frontend may apply optional visual and audio effects during presentation,
|
|
such as a CRT filter and simulated hardware sounds. These are purely
|
|
cosmetic and do not affect emulation state.
|
|
|
|
|
|
## Bare-Metal Multicore Frontends
|
|
|
|
On bare-metal targets such as the Raspberry Pi, the frontend may utilize
|
|
multiple CPU cores. The design shall maintain a strict separation: the emulator
|
|
core shall remain single-threaded, and host-integration concerns shall be offloaded to
|
|
dedicated cores.
|
|
|
|
- Core 0 shall handle initialization, USB input polling, and device hot-plug.
|
|
- Core 1 shall run the emulation frame loop.
|
|
- Core 2 shall handle video presentation, including optional CRT filtering and
|
|
scaling.
|
|
- Core 3 shall drive audio output from a shared ring buffer.
|
|
|
|
Synchronization shall be achieved through spinlocks, atomic flags with memory
|
|
barriers, and trampoline callbacks. The emulation core shall call standard libretro
|
|
video and audio callbacks; the frontend's callback implementations shall act as
|
|
trampolines that queue work to the appropriate core via lockless atomics. The
|
|
emulation core shall be unaware of threading; it shall be driven by a single caller exactly
|
|
as in any other frontend.
|