# HG changeset patch # User sam # Date 1730737650 -25200 # Node ID 3dbf77ca78b9b9896c8a0804e55ddda8e502bc54 # Parent 48df70e8aeed57995d3cf7e9d554d75b4666de14 did: refactor loading, adding threaded background loading (still missing stuff though) diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine.nim --- a/semicongine.nim Mon Nov 04 00:06:30 2024 +0700 +++ b/semicongine.nim Mon Nov 04 23:27:30 2024 +0700 @@ -4,6 +4,12 @@ import ./semicongine/resources export resources +import ./semicongine/loaders +export loaders + +import ./semicongine/background_loader +export background_loader + import ./semicongine/image export image diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/audio.nim --- a/semicongine/audio.nim Mon Nov 04 00:06:30 2024 +0700 +++ b/semicongine/audio.nim Mon Nov 04 23:27:30 2024 +0700 @@ -1,5 +1,10 @@ -include ./audio/mixer -include ./audio/generators -include ./audio/resources +import ./audio/mixer_module +export mixer_module + +import ./audio/generators +export generators + +import ./audio/resources +export resources startMixerThread() diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/audio/generators.nim --- a/semicongine/audio/generators.nim Mon Nov 04 00:06:30 2024 +0700 +++ b/semicongine/audio/generators.nim Mon Nov 04 23:27:30 2024 +0700 @@ -1,3 +1,7 @@ +import std/math + +import ./mixer_module + proc sinewave(f: float): proc(x: float): float = proc ret(x: float): float = sin(x * 2 * Pi * f) diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/audio/mixer.nim --- a/semicongine/audio/mixer.nim Mon Nov 04 00:06:30 2024 +0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,353 +0,0 @@ -import std/os -import std/locks -import std/logging -import std/math -import std/monotimes -import std/strformat -import std/tables -import std/times - -const NBUFFERS = 32 -# it seems that some alsa hardware has a problem with smaller buffers than 512 -when defined(linux): - const BUFFERSAMPLECOUNT = 512 -else: - const BUFFERSAMPLECOUNT = 256 -const AUDIO_SAMPLE_RATE* = 44100 - -type - Level* = 0'f .. 1'f - Sample* = array[2, int16] - SoundData* = seq[Sample] - - Playback = object - sound: SoundData - position: int - loop: bool - levelLeft: Level - levelRight: Level - paused: bool - - Track = object - playing: Table[uint64, Playback] - level: Level - targetLevel: Level - fadeTime: float - fadeStep: float - -proc `=copy`(dest: var Playback, source: Playback) {.error.} -proc `=copy`(dest: var Track, source: Track) {.error.} - -when defined(windows): - include ./platform/windows -when defined(linux): - include ./platform/linux - -type Mixer* = object - playbackCounter: uint64 - tracks: Table[string, Track] - sounds*: Table[string, SoundData] - level: Level - device: NativeSoundDevice - lock: Lock - buffers: seq[SoundData] - currentBuffer: int - lastUpdate: MonoTime - -proc `=copy`(dest: var Mixer, source: Mixer) {.error.} - -proc initMixer(): Mixer = - result = Mixer(tracks: initTable[string, Track](), level: 1'f) - result.tracks[""] = Track(level: 1) - result.lock.initLock() - -proc setupDevice(mixer: var Mixer) = - # call this inside audio thread - var bufferaddresses: seq[ptr SoundData] - for i in 0 ..< NBUFFERS: - mixer.buffers.add newSeq[Sample](BUFFERSAMPLECOUNT) - for i in 0 ..< mixer.buffers.len: - bufferaddresses.add (addr mixer.buffers[i]) - mixer.device = OpenSoundDevice(AUDIO_SAMPLE_RATE, bufferaddresses) - -# TODO: this should probably be in the load-code-stuff -# proc LoadSound*(mixer: var Mixer, name: string, resource: string) = -# assert not (name in mixer.sounds) -# mixer.sounds[name] = LoadAudio(resource) - -proc addSound*(mixer: var Mixer, name: string, sound: SoundData) = - assert not (name in mixer.sounds) - mixer.sounds[name] = sound - -proc replaceSound*(mixer: var Mixer, name: string, sound: SoundData) = - assert (name in mixer.sounds) - mixer.sounds[name] = sound - -proc addTrack*(mixer: var Mixer, name: string, level: Level = 1'f) = - assert not (name in mixer.tracks) - mixer.lock.withLock: - mixer.tracks[name] = Track(level: level) - -proc play*( - mixer: var Mixer, - soundName: string, - track = "", - stopOtherSounds = false, - loop = false, - levelLeft, levelRight: Level, -): uint64 = - assert track in mixer.tracks, &"Track '{track}' does not exists" - assert soundName in mixer.sounds, soundName & " not loaded" - mixer.lock.withLock: - if stopOtherSounds: - mixer.tracks[track].playing.clear() - mixer.tracks[track].playing[mixer.playbackCounter] = Playback( - sound: mixer.sounds[soundName], - position: 0, - loop: loop, - levelLeft: levelLeft, - levelRight: levelRight, - paused: false, - ) - result = mixer.playbackCounter - inc mixer.playbackCounter - -proc play*( - mixer: var Mixer, - soundName: string, - track = "", - stopOtherSounds = false, - loop = false, - level: Level = 1'f, -): uint64 = - play( - mixer = mixer, - soundName = soundName, - track = track, - stopOtherSounds = stopOtherSounds, - loop = loop, - levelLeft = level, - levelRight = level, - ) - -proc stop*(mixer: var Mixer) = - mixer.lock.withLock: - for track in mixer.tracks.mvalues: - track.playing.clear() - -proc getLevel*(mixer: var Mixer): Level = - mixer.level - -proc getLevel*(mixer: var Mixer, track: string): Level = - mixer.tracks[track].level - -proc getLevel*(mixer: var Mixer, playbackId: uint64): (Level, Level) = - for track in mixer.tracks.mvalues: - if playbackId in track.playing: - return (track.playing[playbackId].levelLeft, track.playing[playbackId].levelRight) - -proc setLevel*(mixer: var Mixer, level: Level) = - mixer.level = level - -proc setLevel*(mixer: var Mixer, track: string, level: Level) = - mixer.lock.withLock: - mixer.tracks[track].level = level - -proc setLevel*(mixer: var Mixer, playbackId: uint64, levelLeft, levelRight: Level) = - mixer.lock.withLock: - for track in mixer.tracks.mvalues: - if playbackId in track.playing: - track.playing[playbackId].levelLeft = levelLeft - track.playing[playbackId].levelRight = levelRight - -proc setLevel*(mixer: var Mixer, playbackId: uint64, level: Level) = - setLevel(mixer, playbackId, level, level) - -proc stop*(mixer: var Mixer, track: string) = - assert track in mixer.tracks - mixer.lock.withLock: - mixer.tracks[track].playing.clear() - -proc stop*(mixer: var Mixer, playbackId: uint64) = - mixer.lock.withLock: - for track in mixer.tracks.mvalues: - if playbackId in track.playing: - track.playing.del(playbackId) - break - -proc pause*(mixer: var Mixer, value: bool) = - mixer.lock.withLock: - for track in mixer.tracks.mvalues: - for playback in track.playing.mvalues: - playback.paused = value - -proc pause*(mixer: var Mixer, track: string, value: bool) = - mixer.lock.withLock: - for playback in mixer.tracks[track].playing.mvalues: - playback.paused = value - -proc pause*(mixer: var Mixer, playbackId: uint64, value: bool) = - mixer.lock.withLock: - for track in mixer.tracks.mvalues: - if playbackId in track.playing: - track.playing[playbackId].paused = value - -proc pause*(mixer: var Mixer) = - mixer.pause(true) - -proc pause*(mixer: var Mixer, track: string) = - mixer.pause(track, true) - -proc pause*(mixer: var Mixer, playbackId: uint64) = - mixer.pause(playbackId, true) - -proc unpause*(mixer: var Mixer) = - mixer.pause(false) - -proc unpause*(mixer: var Mixer, track: string) = - mixer.pause(track, false) - -proc unpause*(mixer: var Mixer, playbackId: uint64) = - mixer.pause(playbackId, false) - -proc fadeTo*(mixer: var Mixer, track: string, level: Level, time: float) = - mixer.tracks[track].targetLevel = level - mixer.tracks[track].fadeTime = time - mixer.tracks[track].fadeStep = level.float - mixer.tracks[track].level.float / time - -proc isPlaying*(mixer: var Mixer): bool = - mixer.lock.withLock: - for track in mixer.tracks.mvalues: - for playback in track.playing.values: - if not playback.paused: - return true - return false - -proc isPlaying*(mixer: var Mixer, track: string): bool = - mixer.lock.withLock: - if mixer.tracks.contains(track): - for playback in mixer.tracks[track].playing.values: - if not playback.paused: - return true - return false - -func applyLevel(sample: Sample, levelLeft, levelRight: Level): Sample = - [int16(float(sample[0]) * levelLeft), int16(float(sample[1]) * levelRight)] - -func clip(value: int32): int16 = - int16(max(min(int32(high(int16)), value), int32(low(int16)))) - -# used for combining sounds -func mix(a, b: Sample): Sample = - [clip(int32(a[0]) + int32(b[0])), clip(int32(a[1]) + int32(b[1]))] - -proc updateSoundBuffer(mixer: var Mixer) = - let t = getMonoTime() - - let dt = (t - mixer.lastUpdate).inNanoseconds.float64 / 1_000_000_000'f64 - mixer.lastUpdate = t - - # update fadings - for track in mixer.tracks.mvalues: - if track.fadeTime > 0: - track.fadeTime -= dt - track.level = - (track.level.float64 + track.fadeStep.float64 * dt).clamp(Level.low, Level.high) - if track.fadeTime <= 0: - track.level = track.targetLevel - # mix - var hasData = false - for i in 0 ..< mixer.buffers[mixer.currentBuffer].len: - var mixedSample = [0'i16, 0'i16] - mixer.lock.withLock: - for track in mixer.tracks.mvalues: - var stoppedSounds: seq[uint64] - for (id, playback) in track.playing.mpairs: - if playback.paused: - continue - let sample = applyLevel( - playback.sound[playback.position], - mixer.level * track.level * playback.levelLeft, - mixer.level * track.level * playback.levelRight, - ) - mixedSample = mix(mixedSample, sample) - hasData = true - inc playback.position - if playback.position >= playback.sound.len: - if playback.loop: - playback.position = 0 - else: - stoppedSounds.add id - for id in stoppedSounds: - track.playing.del(id) - mixer.buffers[mixer.currentBuffer][i] = mixedSample - # send data to sound device - if hasData: - mixer.device.WriteSoundData(mixer.currentBuffer) - mixer.currentBuffer = (mixer.currentBuffer + 1) mod mixer.buffers.len - -# DSP functions -# TODO: finish implementation, one day - -#[ -# -proc lowPassFilter(data: var SoundData, cutoff: int) = - let alpha = float(cutoff) / AUDIO_SAMPLE_RATE - var value = data[0] - for i in 0 ..< data.len: - value[0] += int16(alpha * float(data[i][0] - value[0])) - value[1] += int16(alpha * float(data[i][1] - value[1])) - data[i] = value - - proc downsample(data: var SoundData, n: int) = - let newLen = (data.len - 1) div n + 1 - for i in 0 ..< newLen: - data[i] = data[i * n] - data.setLen(newLen) - - proc upsample(data: var SoundData, m: int) = - data.setLen(data.len * m) - var i = data.len - 1 - while i < 0: - if i mod m == 0: - data[i] = data[i div m] - else: - data[i] = [0, 0] - i.dec - - proc slowdown(data: var SoundData, m, n: int) = - data.upsample(m) - # TODO - # data.lowPassFilter(m) - data.downsample(n) - - ]# - -proc destroy(mixer: var Mixer) = - mixer.lock.deinitLock() - mixer.device.CloseSoundDevice() - -var - mixer* = createShared(Mixer) - audiothread: Thread[void] - -proc audioWorker() {.thread.} = - mixer[].setupDevice() - onThreadDestruction( - proc() = - mixer[].lock.withLock(mixer[].destroy()) - freeShared(mixer) - ) - while true: - mixer[].updateSoundBuffer() - -# for thread priority (really necessary?) -when defined(windows): - import ../thirdparty/winim/winim/inc/winbase -when defined(linux): - import std/posix - -proc startMixerThread() = - mixer[] = initMixer() - audiothread.createThread(audioWorker) - debug "Created audio thread" diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/audio/mixer_module.nim --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/semicongine/audio/mixer_module.nim Mon Nov 04 23:27:30 2024 +0700 @@ -0,0 +1,354 @@ +import std/os +import std/locks +import std/logging +import std/math +import std/monotimes +import std/strformat +import std/tables +import std/times + +import ../core/globals + +const NBUFFERS = 32 +# it seems that some alsa hardware has a problem with smaller buffers than 512 +when defined(linux): + const BUFFERSAMPLECOUNT = 512 +else: + const BUFFERSAMPLECOUNT = 256 + +type + Level* = 0'f .. 1'f + Sample* = array[2, int16] + SoundData* = seq[Sample] + + Playback = object + sound: SoundData + position: int + loop: bool + levelLeft: Level + levelRight: Level + paused: bool + + Track = object + playing: Table[uint64, Playback] + level: Level + targetLevel: Level + fadeTime: float + fadeStep: float + +proc `=copy`(dest: var Playback, source: Playback) {.error.} +proc `=copy`(dest: var Track, source: Track) {.error.} + +when defined(windows): + include ./platform/windows +when defined(linux): + include ./platform/linux + +type Mixer* = object + playbackCounter: uint64 + tracks: Table[string, Track] + sounds*: Table[string, SoundData] + level: Level + device: NativeSoundDevice + lock: Lock + buffers: seq[SoundData] + currentBuffer: int + lastUpdate: MonoTime + +proc `=copy`(dest: var Mixer, source: Mixer) {.error.} + +proc initMixer(): Mixer = + result = Mixer(tracks: initTable[string, Track](), level: 1'f) + result.tracks[""] = Track(level: 1) + result.lock.initLock() + +proc setupDevice(mixer: var Mixer) = + # call this inside audio thread + var bufferaddresses: seq[ptr SoundData] + for i in 0 ..< NBUFFERS: + mixer.buffers.add newSeq[Sample](BUFFERSAMPLECOUNT) + for i in 0 ..< mixer.buffers.len: + bufferaddresses.add (addr mixer.buffers[i]) + mixer.device = OpenSoundDevice(AUDIO_SAMPLE_RATE, bufferaddresses) + +# TODO: this should probably be in the load-code-stuff +# proc LoadSound*(mixer: var Mixer, name: string, resource: string) = +# assert not (name in mixer.sounds) +# mixer.sounds[name] = LoadAudio(resource) + +proc addSound*(mixer: var Mixer, name: string, sound: SoundData) = + assert not (name in mixer.sounds) + mixer.sounds[name] = sound + +proc replaceSound*(mixer: var Mixer, name: string, sound: SoundData) = + assert (name in mixer.sounds) + mixer.sounds[name] = sound + +proc addTrack*(mixer: var Mixer, name: string, level: Level = 1'f) = + assert not (name in mixer.tracks) + mixer.lock.withLock: + mixer.tracks[name] = Track(level: level) + +proc play*( + mixer: var Mixer, + soundName: string, + track = "", + stopOtherSounds = false, + loop = false, + levelLeft, levelRight: Level, +): uint64 = + assert track in mixer.tracks, &"Track '{track}' does not exists" + assert soundName in mixer.sounds, soundName & " not loaded" + mixer.lock.withLock: + if stopOtherSounds: + mixer.tracks[track].playing.clear() + mixer.tracks[track].playing[mixer.playbackCounter] = Playback( + sound: mixer.sounds[soundName], + position: 0, + loop: loop, + levelLeft: levelLeft, + levelRight: levelRight, + paused: false, + ) + result = mixer.playbackCounter + inc mixer.playbackCounter + +proc play*( + mixer: var Mixer, + soundName: string, + track = "", + stopOtherSounds = false, + loop = false, + level: Level = 1'f, +): uint64 = + play( + mixer = mixer, + soundName = soundName, + track = track, + stopOtherSounds = stopOtherSounds, + loop = loop, + levelLeft = level, + levelRight = level, + ) + +proc stop*(mixer: var Mixer) = + mixer.lock.withLock: + for track in mixer.tracks.mvalues: + track.playing.clear() + +proc getLevel*(mixer: var Mixer): Level = + mixer.level + +proc getLevel*(mixer: var Mixer, track: string): Level = + mixer.tracks[track].level + +proc getLevel*(mixer: var Mixer, playbackId: uint64): (Level, Level) = + for track in mixer.tracks.mvalues: + if playbackId in track.playing: + return (track.playing[playbackId].levelLeft, track.playing[playbackId].levelRight) + +proc setLevel*(mixer: var Mixer, level: Level) = + mixer.level = level + +proc setLevel*(mixer: var Mixer, track: string, level: Level) = + mixer.lock.withLock: + mixer.tracks[track].level = level + +proc setLevel*(mixer: var Mixer, playbackId: uint64, levelLeft, levelRight: Level) = + mixer.lock.withLock: + for track in mixer.tracks.mvalues: + if playbackId in track.playing: + track.playing[playbackId].levelLeft = levelLeft + track.playing[playbackId].levelRight = levelRight + +proc setLevel*(mixer: var Mixer, playbackId: uint64, level: Level) = + setLevel(mixer, playbackId, level, level) + +proc stop*(mixer: var Mixer, track: string) = + assert track in mixer.tracks + mixer.lock.withLock: + mixer.tracks[track].playing.clear() + +proc stop*(mixer: var Mixer, playbackId: uint64) = + mixer.lock.withLock: + for track in mixer.tracks.mvalues: + if playbackId in track.playing: + track.playing.del(playbackId) + break + +proc pause*(mixer: var Mixer, value: bool) = + mixer.lock.withLock: + for track in mixer.tracks.mvalues: + for playback in track.playing.mvalues: + playback.paused = value + +proc pause*(mixer: var Mixer, track: string, value: bool) = + mixer.lock.withLock: + for playback in mixer.tracks[track].playing.mvalues: + playback.paused = value + +proc pause*(mixer: var Mixer, playbackId: uint64, value: bool) = + mixer.lock.withLock: + for track in mixer.tracks.mvalues: + if playbackId in track.playing: + track.playing[playbackId].paused = value + +proc pause*(mixer: var Mixer) = + mixer.pause(true) + +proc pause*(mixer: var Mixer, track: string) = + mixer.pause(track, true) + +proc pause*(mixer: var Mixer, playbackId: uint64) = + mixer.pause(playbackId, true) + +proc unpause*(mixer: var Mixer) = + mixer.pause(false) + +proc unpause*(mixer: var Mixer, track: string) = + mixer.pause(track, false) + +proc unpause*(mixer: var Mixer, playbackId: uint64) = + mixer.pause(playbackId, false) + +proc fadeTo*(mixer: var Mixer, track: string, level: Level, time: float) = + mixer.tracks[track].targetLevel = level + mixer.tracks[track].fadeTime = time + mixer.tracks[track].fadeStep = level.float - mixer.tracks[track].level.float / time + +proc isPlaying*(mixer: var Mixer): bool = + mixer.lock.withLock: + for track in mixer.tracks.mvalues: + for playback in track.playing.values: + if not playback.paused: + return true + return false + +proc isPlaying*(mixer: var Mixer, track: string): bool = + mixer.lock.withLock: + if mixer.tracks.contains(track): + for playback in mixer.tracks[track].playing.values: + if not playback.paused: + return true + return false + +func applyLevel(sample: Sample, levelLeft, levelRight: Level): Sample = + [int16(float(sample[0]) * levelLeft), int16(float(sample[1]) * levelRight)] + +func clip(value: int32): int16 = + int16(max(min(int32(high(int16)), value), int32(low(int16)))) + +# used for combining sounds +func mix(a, b: Sample): Sample = + [clip(int32(a[0]) + int32(b[0])), clip(int32(a[1]) + int32(b[1]))] + +proc updateSoundBuffer(mixer: var Mixer) = + let t = getMonoTime() + + let dt = (t - mixer.lastUpdate).inNanoseconds.float64 / 1_000_000_000'f64 + mixer.lastUpdate = t + + # update fadings + for track in mixer.tracks.mvalues: + if track.fadeTime > 0: + track.fadeTime -= dt + track.level = + (track.level.float64 + track.fadeStep.float64 * dt).clamp(Level.low, Level.high) + if track.fadeTime <= 0: + track.level = track.targetLevel + # mix + var hasData = false + for i in 0 ..< mixer.buffers[mixer.currentBuffer].len: + var mixedSample = [0'i16, 0'i16] + mixer.lock.withLock: + for track in mixer.tracks.mvalues: + var stoppedSounds: seq[uint64] + for (id, playback) in track.playing.mpairs: + if playback.paused: + continue + let sample = applyLevel( + playback.sound[playback.position], + mixer.level * track.level * playback.levelLeft, + mixer.level * track.level * playback.levelRight, + ) + mixedSample = mix(mixedSample, sample) + hasData = true + inc playback.position + if playback.position >= playback.sound.len: + if playback.loop: + playback.position = 0 + else: + stoppedSounds.add id + for id in stoppedSounds: + track.playing.del(id) + mixer.buffers[mixer.currentBuffer][i] = mixedSample + # send data to sound device + if hasData: + mixer.device.WriteSoundData(mixer.currentBuffer) + mixer.currentBuffer = (mixer.currentBuffer + 1) mod mixer.buffers.len + +# DSP functions +# TODO: finish implementation, one day + +#[ +# +proc lowPassFilter(data: var SoundData, cutoff: int) = + let alpha = float(cutoff) / AUDIO_SAMPLE_RATE + var value = data[0] + for i in 0 ..< data.len: + value[0] += int16(alpha * float(data[i][0] - value[0])) + value[1] += int16(alpha * float(data[i][1] - value[1])) + data[i] = value + + proc downsample(data: var SoundData, n: int) = + let newLen = (data.len - 1) div n + 1 + for i in 0 ..< newLen: + data[i] = data[i * n] + data.setLen(newLen) + + proc upsample(data: var SoundData, m: int) = + data.setLen(data.len * m) + var i = data.len - 1 + while i < 0: + if i mod m == 0: + data[i] = data[i div m] + else: + data[i] = [0, 0] + i.dec + + proc slowdown(data: var SoundData, m, n: int) = + data.upsample(m) + # TODO + # data.lowPassFilter(m) + data.downsample(n) + + ]# + +proc destroy(mixer: var Mixer) = + mixer.lock.deinitLock() + mixer.device.CloseSoundDevice() + +var + mixer* = createShared(Mixer) + audiothread: Thread[void] + +proc audioWorker() {.thread.} = + mixer[].setupDevice() + onThreadDestruction( + proc() = + mixer[].lock.withLock(mixer[].destroy()) + freeShared(mixer) + ) + while true: + mixer[].updateSoundBuffer() + +# for thread priority (really necessary?) +when defined(windows): + import ../thirdparty/winim/winim/inc/winbase +when defined(linux): + import std/posix + +proc startMixerThread*() = + mixer[] = initMixer() + audiothread.createThread(audioWorker) + debug "Created audio thread" diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/audio/resources.nim --- a/semicongine/audio/resources.nim Mon Nov 04 00:06:30 2024 +0700 +++ b/semicongine/audio/resources.nim Mon Nov 04 23:27:30 2024 +0700 @@ -1,11 +1,14 @@ import std/endians import std/os import std/streams +import std/strformat import std/strutils import ../core import ../resources +import ./mixer_module + type Encoding {.size: sizeof(uint32).} = enum # Unspecified = 0 @@ -113,21 +116,3 @@ "Only support mono and stereo audio at the moment (1 or 2 channels), but found " & $channels, ) - -proc loadAudio*(path: string, package = DEFAULT_PACKAGE): SoundData = - if path.splitFile().ext.toLowerAscii == ".au": - loadResource_intern(path, package = package).readAU() - elif path.splitFile().ext.toLowerAscii == ".ogg": - loadResource_intern(path, package = package).readVorbis() - else: - raise newException(Exception, "Unsupported audio file type: " & path) - -proc loadAudio*( - path: static string, package: static string = DEFAULT_PACKAGE -): SoundData = - if path.splitFile().ext.toLowerAscii == ".au": - loadResource_intern(path, package = package).readAU() - elif path.splitFile().ext.toLowerAscii == ".ogg": - loadResource_intern(path, package = package).readVorbis() - else: - raise newException(Exception, "Unsupported audio file type: " & path) diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/background_loader.nim --- a/semicongine/background_loader.nim Mon Nov 04 00:06:30 2024 +0700 +++ b/semicongine/background_loader.nim Mon Nov 04 23:27:30 2024 +0700 @@ -2,51 +2,57 @@ import std/tables type - LoaderThreadArgs[T] = - (ptr Channel[string], ptr Channel[LoaderResponse[T]], proc(f: string): T {.gcsafe.}) + LoaderThreadArgs[T] = ( + ptr Channel[(string, string)], + ptr Channel[LoaderResponse[T]], + proc(f, p: string): T {.gcsafe.}, + ) LoaderResponse[T] = object - file: string + path: string + package: string data: T error: string BackgroundLoader[T] = object - loadRequestCn: Channel[string] # used for sending load requests + loadRequestCn: Channel[(string, string)] # used for sending load requests responseCn: Channel[LoaderResponse[T]] # used for sending back loaded data worker: Thread[LoaderThreadArgs[T]] # does the actual loading from the disk responseTable: Table[string, LoaderResponse[T]] # stores results proc loader[T](args: LoaderThreadArgs[T]) {.thread.} = while true: - let file = args[0][].recv() + let (path, package) = args[0][].recv() try: - args[1][].send(LoaderResponse[T](file: file, data: args[2](file))) + args[1][].send( + LoaderResponse[T](path: path, package: package, data: args[2](path, package)) + ) except Exception as e: - args[1][].send(LoaderResponse[T](file: file, error: e.msg)) + args[1][].send(LoaderResponse[T](path: path, package: package, error: e.msg)) proc fetchAll*(ld: var BackgroundLoader) = var (hasData, response) = ld.responseCn.tryRecv() while hasData: - ld.responseTable[response.file] = response + ld.responseTable[response.package & ":" & response.path] = response (hasData, response) = ld.responseCn.tryRecv() -proc requestLoading*(ld: var BackgroundLoader, file: string) = - ld.loadRequestCn.send(file) +proc requestLoading*(ld: var BackgroundLoader, path, package: string) = + ld.loadRequestCn.send((path, package)) -proc isLoaded*(ld: var BackgroundLoader, file: string): bool = - ld.fetchAll * () - file in ld.responseTable +proc isLoaded*(ld: var BackgroundLoader, path, package: string): bool = + fetchAll(ld) + (package & ":" & path) in ld.responseTable -proc getLoaded*[T](ld: var BackgroundLoader[T], file: string): T = +proc getLoadedData*[T](ld: var BackgroundLoader[T], path, package: string): T = var item: LoaderResponse[T] - doAssert ld.responseTable.pop(file, item) + doAssert ld.responseTable.pop(package & ":" & path, item) if item.error != "": raise newException(Exception, item.error) result = item.data proc initBackgroundLoader*[T]( - loadFn: proc(f: string): T {.gcsafe.} + loadFn: proc(path, package: string): T {.gcsafe.} ): ptr BackgroundLoader[T] = - result = cast[ptr BackgroundLoader[T]](allocShared0(sizeof(BackgroundLoader[T]))) + result = createShared(BackgroundLoader[T]) open(result.loadRequestCn) open(result.responseCn) createThread[LoaderThreadArgs[T]]( @@ -54,14 +60,3 @@ loader[T], (addr result.loadRequestCn, addr result.responseCn, loadFn), ) - -# threaded background loaders - -proc rawLoaderFunc(f: string): seq[byte] {.gcsafe.} = - cast[seq[byte]](toSeq(f.readFile())) - -proc audioLoaderFunc(f: string): seq[byte] {.gcsafe.} = - cast[seq[byte]](toSeq(f.readFile())) - -var rawLoader = initBackgroundLoader(rawLoaderFunc) -var rawLoader = initBackgroundLoader(rawLoaderFunc) diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/gltf.nim --- a/semicongine/gltf.nim Mon Nov 04 00:06:30 2024 +0700 +++ b/semicongine/gltf.nim Mon Nov 04 23:27:30 2024 +0700 @@ -410,28 +410,3 @@ for nodeId in items(scene["nodes"]): nodes.add nodeId.getInt() result.scenes.add nodes - -proc loadMeshes*[TMesh, TMaterial]( - path: string, - meshAttributesMapping: static MeshAttributeNames, - materialAttributesMapping: static MaterialAttributeNames, - package = DEFAULT_PACKAGE, -): GltfData[TMesh, TMaterial] = - ReadglTF[TMesh, TMaterial]( - stream = loadResource_intern(path, package = package), - meshAttributesMapping = meshAttributesMapping, - materialAttributesMapping = materialAttributesMapping, - ) - -# static version, for better checks -proc loadMeshes*[TMesh, TMaterial]( - path: static string, - meshAttributesMapping: static MeshAttributeNames, - materialAttributesMapping: static MaterialAttributeNames, - package: static string = DEFAULT_PACKAGE, -): GltfData[TMesh, TMaterial] = - ReadglTF[TMesh, TMaterial]( - stream = loadResource_intern(path, package = package), - meshAttributesMapping = meshAttributesMapping, - materialAttributesMapping = materialAttributesMapping, - ) diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/image.nim --- a/semicongine/image.nim Mon Nov 04 00:06:30 2024 +0700 +++ b/semicongine/image.nim Mon Nov 04 23:27:30 2024 +0700 @@ -93,18 +93,6 @@ swap(result.data[i][0], result.data[i][2]) # TODO: static versions to check for existing of files during compilation -proc loadImage*[T: PixelType](path: string, package = DEFAULT_PACKAGE): Image[T] = - assert path.splitFile().ext.toLowerAscii == ".png", - "Unsupported image type: " & path.splitFile().ext.toLowerAscii - when T is Gray: - let pngType = 0.cint - elif T is BGRA: - let pngType = 6.cint - - let (width, height, data) = - loadImageData[T](loadResource_intern(path, package = package).readAll()) - result = Image[T](width: width, height: height, data: data) - proc addImage*[T: PixelType](imageArray: var ImageArray[T], image: sink Image[T]) = assert image.width == imageArray.width, &"Image needs to have same dimension as ImageArray to be added (array has {imageArray.width}x{imageArray.height} but image has {image.width}x{image.height})" @@ -114,61 +102,6 @@ inc imageArray.nLayers imageArray.data.add image.data -proc loadImageArray*[T: PixelType]( - paths: openArray[string], package = DEFAULT_PACKAGE -): ImageArray[T] = - assert paths.len > 0, "Image array cannot contain 0 images" - for path in paths: - assert path.splitFile().ext.toLowerAscii == ".png", - "Unsupported image type: " & path.splitFile().ext.toLowerAscii - when T is Gray: - let pngType = 0.cint - elif T is BGRA: - let pngType = 6.cint - - let (width, height, data) = - loadImageData[T](loadResource_intern(paths[0], package = package).readAll()) - result = - ImageArray[T](width: width, height: height, data: data, nLayers: paths.len.uint32) - for path in paths[1 .. ^1]: - let (w, h, data) = - loadImageData[T](loadResource_intern(path, package = package).readAll()) - assert w == result.width, - "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}" - assert h == result.height, - "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}" - result.data.add data - -proc loadImageArray*[T: PixelType]( - path: string, tilesize: uint32, package = DEFAULT_PACKAGE -): ImageArray[T] = - assert path.splitFile().ext.toLowerAscii == ".png", - "Unsupported image type: " & path.splitFile().ext.toLowerAscii - when T is Gray: - let pngType = 0.cint - elif T is BGRA: - let pngType = 6.cint - - let (width, height, data) = - loadImageData[T](loadResource_intern(path, package = package).readAll()) - let - tilesX = width div tilesize - tilesY = height div tilesize - result = ImageArray[T](width: tilesize, height: tilesize) - var tile = newSeq[T](tilesize * tilesize) - for ty in 0 ..< tilesY: - for tx in 0 ..< tilesY: - var hasNonTransparent = when T is BGRA: false else: true - let baseI = ty * tilesize * width + tx * tilesize - for y in 0 ..< tilesize: - for x in 0 ..< tilesize: - tile[y * tilesize + x] = data[baseI + y * width + x] - when T is BGRA: - hasNonTransparent = hasNonTransparent or tile[y * tilesize + x].a > 0 - if hasNonTransparent: - result.data.add tile - result.nLayers.inc - proc `[]`*(image: Image, x, y: uint32): auto = assert x < image.width, &"{x} < {image.width} is not true" assert y < image.height, &"{y} < {image.height} is not true" diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/loaders.nim --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/semicongine/loaders.nim Mon Nov 04 23:27:30 2024 +0700 @@ -0,0 +1,171 @@ +import std/json +import std/parsecfg +import std/strutils +import std/sequtils +import std/os +import std/streams + +import ./audio +import ./background_loader +import ./core +import ./gltf +import ./image +import ./resources + +proc loadBytes*(path, package: string): seq[byte] {.gcsafe.} = + cast[seq[byte]](toSeq(path.loadResource_intern(package = package).readAll())) + +proc loadJson*(path: string, package = DEFAULT_PACKAGE): JsonNode {.gcsafe.} = + path.loadResource_intern(package = package).readAll().parseJson() + +proc loadConfig*(path: string, package = DEFAULT_PACKAGE): Config {.gcsafe.} = + path.loadResource_intern(package = package).loadConfig(filename = path) + +proc loadImage*[T: PixelType]( + path: string, package = DEFAULT_PACKAGE +): Image[T] {.gcsafe.} = + assert path.splitFile().ext.toLowerAscii == ".png", + "Unsupported image type: " & path.splitFile().ext.toLowerAscii + when T is Gray: + let pngType = 0.cint + elif T is BGRA: + let pngType = 6.cint + + let (width, height, data) = + loadImageData[T](loadResource_intern(path, package = package).readAll()) + result = Image[T](width: width, height: height, data: data) + +proc loadImageArray*[T: PixelType]( + paths: openArray[string], package = DEFAULT_PACKAGE +): ImageArray[T] {.gcsafe.} = + assert paths.len > 0, "Image array cannot contain 0 images" + for path in paths: + assert path.splitFile().ext.toLowerAscii == ".png", + "Unsupported image type: " & path.splitFile().ext.toLowerAscii + when T is Gray: + let pngType = 0.cint + elif T is BGRA: + let pngType = 6.cint + + let (width, height, data) = + loadImageData[T](loadResource_intern(paths[0], package = package).readAll()) + result = + ImageArray[T](width: width, height: height, data: data, nLayers: paths.len.uint32) + for path in paths[1 .. ^1]: + let (w, h, data) = + loadImageData[T](loadResource_intern(path, package = package).readAll()) + assert w == result.width, + "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}" + assert h == result.height, + "New image layer has dimension {(w, y)} but image has dimension {(result.width, result.height)}" + result.data.add data + +proc loadImageArray*[T: PixelType]( + path: string, tilesize: uint32, package = DEFAULT_PACKAGE +): ImageArray[T] {.gcsafe.} = + assert path.splitFile().ext.toLowerAscii == ".png", + "Unsupported image type: " & path.splitFile().ext.toLowerAscii + when T is Gray: + let pngType = 0.cint + elif T is BGRA: + let pngType = 6.cint + + let (width, height, data) = + loadImageData[T](loadResource_intern(path, package = package).readAll()) + let + tilesX = width div tilesize + tilesY = height div tilesize + result = ImageArray[T](width: tilesize, height: tilesize) + var tile = newSeq[T](tilesize * tilesize) + for ty in 0 ..< tilesY: + for tx in 0 ..< tilesY: + var hasNonTransparent = when T is BGRA: false else: true + let baseI = ty * tilesize * width + tx * tilesize + for y in 0 ..< tilesize: + for x in 0 ..< tilesize: + tile[y * tilesize + x] = data[baseI + y * width + x] + when T is BGRA: + hasNonTransparent = hasNonTransparent or tile[y * tilesize + x].a > 0 + if hasNonTransparent: + result.data.add tile + result.nLayers.inc + +proc loadAudio*(path: string, package = DEFAULT_PACKAGE): SoundData {.gcsafe.} = + if path.splitFile().ext.toLowerAscii == ".au": + loadResource_intern(path, package = package).readAU() + elif path.splitFile().ext.toLowerAscii == ".ogg": + loadResource_intern(path, package = package).readVorbis() + else: + raise newException(Exception, "Unsupported audio file type: " & path) + +proc loadMeshes*[TMesh, TMaterial]( + path: string, + meshAttributesMapping: static MeshAttributeNames, + materialAttributesMapping: static MaterialAttributeNames, + package = DEFAULT_PACKAGE, +): GltfData[TMesh, TMaterial] {.gcsafe.} = + ReadglTF[TMesh, TMaterial]( + stream = loadResource_intern(path, package = package), + meshAttributesMapping = meshAttributesMapping, + materialAttributesMapping = materialAttributesMapping, + ) + +# background loaders + +type ResourceType = + seq[byte] | JsonNode | Config | Image[Gray] | Image[BGRA] | SoundData + +var rawLoader = initBackgroundLoader(loadBytes) +var jsonLoader = initBackgroundLoader(loadJson) +var configLoader = initBackgroundLoader(loadConfig) +var grayImageLoader = initBackgroundLoader(loadImage[Gray]) +var imageLoader = initBackgroundLoader(loadImage[BGRA]) +var audioLoader = initBackgroundLoader(loadAudio) + +proc loadAsync*[T: ResourceType](path: string, package = DEFAULT_PACKAGE) = + when T is seq[byte]: + requestLoading(rawLoader[], path, package) + elif T is JsonNode: + requestLoading(jsonLoader[], path, package) + elif T is Config: + requestLoading(configLoader[], path, package) + elif T is Image[Gray]: + requestLoading(grayImageLoader[], path, package) + elif T is Image[BGRA]: + requestLoading(imageLoader[], path, package) + elif T is SoundData: + requestLoading(audioLoader[], path, package) + else: + {.error: "Unknown type".} + +proc isLoaded*[T: ResourceType](path: string, package = DEFAULT_PACKAGE): bool = + when T is seq[byte]: + isLoaded(rawLoader[], path, package) + elif T is JsonNode: + isLoaded(jsonLoader[], path, package) + elif T is Config: + isLoaded(configLoader[], path, package) + elif T is Image[Gray]: + isLoaded(grayImageLoader[], path, package) + elif T is Image[BGRA]: + isLoaded(imageLoader[], path, package) + elif T is SoundData: + isLoaded(audioLoader[], path, package) + else: + {.error: "Unknown type".} + +proc getLoaded*[T: ResourceType](path: string, package = DEFAULT_PACKAGE): T = + when T is seq[byte]: + getLoadedData(rawLoader[], path, package) + elif T is JsonNode: + getLoadedData(jsonLoader[], path, package) + elif T is Config: + getLoadedData(configLoader[], path, package) + elif T is Image[Gray]: + getLoadedData(grayImageLoader[], path, package) + elif T is Image[BGRA]: + getLoadedData(imageLoader[], path, package) + elif T is SoundData: + getLoadedData(audioLoader[], path, package) + else: + {.error: "Unknown type".} diff -r 48df70e8aeed -r 3dbf77ca78b9 semicongine/resources.nim --- a/semicongine/resources.nim Mon Nov 04 00:06:30 2024 +0700 +++ b/semicongine/resources.nim Mon Nov 04 23:27:30 2024 +0700 @@ -164,28 +164,6 @@ proc loadResource*(path: string, package = DEFAULT_PACKAGE): Stream = loadResource_intern(path, package = package) -proc loadJson*(path: string, package = DEFAULT_PACKAGE): JsonNode = - path.loadResource_intern(package = package).readAll().parseJson() - -proc loadConfig*(path: string, package = DEFAULT_PACKAGE): Config = - path.loadResource_intern(package = package).loadConfig(filename = path) - -# static versions of the above 3 calls -proc loadResource*( - path: static string, package: static string = DEFAULT_PACKAGE -): Stream = - loadResource_intern(path, package = package) - -proc loadJson*( - path: static string, package: static string = DEFAULT_PACKAGE -): JsonNode = - path.loadResource_intern(package = package).readAll().parseJson() - -proc loadConfig*( - path: static string, package: static string = DEFAULT_PACKAGE -): Config = - path.loadResource_intern(package = package).loadConfig(filename = path) - proc packages*(): seq[string] = modList_intern()