Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Engine: OS layer, timers and audio

Threads, timers, sounds and mixing

  • The OS layer's threads (0x46e2a4-0x46f7a4). The Mohawk OS layer runs threads of its own, cooperatively, on the application thread, as on the Mac: each (RTTI class thread) has a stack carved from a buffer WinMain gives osStartup, and a saved register context. The scheduler (schedule, 0x46e90e) runs at calls into the layer and every 20 ms (an OS-layer timer, running only while two or more threads are started): it picks, round the ring from the current thread, the first of the highest priority that isn't sleeping, suspended or waiting, ending waits that have timed out, and idles (running timers) when none can run. Priorities above 1 are urgent: while one is runnable, the timer fires at every chance. Mutexes (recursive, released when their holder ends, with a deadlock check) and events (set and reset through the layer's DeferLock, so they can be posted from real threads such as waveOut callbacks) are the other sync objects (sync, tagged 'sync'), waited for with waitSync. The classes' methods are __cdecl and the module was compiled without exception handling (-x-): with it, BCC32 counts every object a constructor makes, which the original doesn't. A thread deleting itself switches to the next thread's stack before deleting its own (deleteSync).
  • The OS layer's manager (0x46da64-0x46e2a4) holds inline assembly (the atomic operations, the breakpoint), so it went through TASM32: osStartup's sub ax, 1 has TASM's accumulator encoding (66 2d 01 00) where BCC32 emits 66 83 e8 01.
  • Deferred calls (the OS layer, 0x46d7f8-0x46d95c). A DeferLock is a lock whose holders count up and down (enterLock/leaveLock, with the Interlocked functions). While it's held, calls posted to it (deferCall) wait in a lock-free queue, and the last leaveLock runs them in the order they were posted, each as many times as it was posted. Locks can be listed, and 0x46dc45 takes all the listed ones at once.
  • Timers (0x492dc4-0x49324c). Timer events (72 bytes, tagged 'TEvt') wrap multimedia timers (timeSetEvent, at the finest resolution timeGetDevCaps allows, set with timeBeginPeriod). The multimedia callback only posts the event's call to the timers' DeferLock, so procedures run on whichever thread holds or releases it, never concurrently with code holding it (lockTimers). Delays longer than the device's maximum are counted down in steps. A periodic event that falls behind switches to one-shot timers, each shortened by how late the last one was.
  • Sounds (0x4764bc-0x4778b0). MIDI and wave sounds are C++ objects of class audioObj (subclasses midiObj, waveObj and wavestreamObj, named by their RTTI descriptors, which sit in the code after each module's methods), tagged 'AObj' after the vtable pointer, and handed around as handles (their addresses). They're created from Mohawk MIDI or WAVE files in a handle, or streamed from a resource's file. Their vtable has 22 slots, and the base class implements seven of them (activating, opening, rate, volume, play, stop, close; for MIDI the rate scales the tempo and the volume picks a velocity curve). The abstract base's vtable (0x4a8448) fills the rest with the runtime's pure-virtual stub. Subclasses' vtables are at 0x4a81f4 (MIDI), 0x4a8278 (wave) and 0x4a84ac. MOHAWK.INI's [Audio] section picks the default devices (DefaultMidiDevice and DefaultWaveDevice, by number or name, else the Sound Mapper's playback device from the registry), can keep them open (fCacheDefault...Device), and maps wave rates a device can't play to others (fTranslateWaveRateOnError, [Audio.WaveRateTranslations]). A settings section can also list keys with versions ("name";1.2+), looked up by findIniEntry (0x47690f).
  • Streamed wave sounds (0x47cb30-0x47e0eb). A wave sound can play straight from its resource's file (or a file of its own): newStreamedWave reads the WAVE header and the Cue# chunk (byte-swapped once, in memory), and optionally preloads the first part. Opening the device starts a thread (streamThread, 4 KB of stack) that waits on an event; each time a buffer finishes playing (the waveOut callback posts it to the sound's DeferLock), the event is set and the thread reads more, in buffers of 4 KB, until about three seconds are queued. Buffers are split at cue points and at the loop's ends; a loop that fits in one buffer is looped by the device (WHDR_BEGINLOOP/WHDR_ENDLOOP), otherwise the loop's first buffer is kept and the reading goes back to the loop's start. Starting the device raises the calling and reading threads' priority while the reader queues the first buffers.
  • WaveMix's output buffers (wavebuf, 0x47af1c-0x47cb30). Wave sounds don't call waveOut directly: they go through a waveOut-like API (wavebufOpen, wavebufWrite, ...) over WaveMix objects (tag 'WMix'), which either pass straight through to waveOut (wmxWaveOut, when [WaveMix] fEnable is off or the device can't be mixed) or are mixed in software (wmxMixer) into one output buffer per wave device (wmxDevice). The output buffer is a wavebuf: wavebufWO keeps a ring of waveOut blocks written ahead by a polling thread (sizes from [WaveMix.DeviceInfo], per device name and driver version, else default, or not supported), and wavebufDS uses a looping one-second DirectSound buffer when [WaveMix] fEnableDirectSound is set and DSOUND.DLL loads (this path gets DirectSoundCreate and DirectSoundEnumerateA with LoadLibrary/GetProcAddress). The mix's format comes from [WaveMix] ulFrameRate (11025, 22050 or 44100), ulFrameSize (8 or 16) and fStereo.
  • WaveMix's mixer (0x47e0ec-0x480028). Each wmxMixer resamples its queued blocks by a 16.16 step (the device's rate over the object's rate times its playback rate) and mixes them into the device's buffer when the buffer asks for more; the first object mixed copies, the rest add with saturation, and 8-bit samples go through a 256-byte volume table per output channel (left and right levels from setLevels, like waveOutSetVolume's, scaled by the fixed-point volume). Loops (WHDR_BEGINLOOP/WHDR_ENDLOOP) are mixed by position arithmetic, capped so a loop's total stays under 2^30 samples. The inner loops (0x47fae8-0x47fd5b) are hand-written assembly (xlat through the table, jo to saturate, rep movs/stos), decompiled as portable functional C. 16-bit samples are kept big-endian (as on the Mac): the mixing loops swap bytes, and wmxWaveOut swaps a 16-bit block's bytes when it's prepared or unprepared (inline xchg ah, al).
  • The mixer's objects call the owner's callback as waveOut would (WOM_OPEN, WOM_DONE, WOM_CLOSE, and 0x8000 each time a loop comes round unless bit 30 of the open flags is set; that bit also makes the message a 32-bit value rather than a word). A header's lpNext holds its mixer block and reserved the tag 'WMix'.