From 08337e38f56478f2526a83adfc8604bdbc7515a1 Mon Sep 17 00:00:00 2001 From: Morten Delenk Date: Sun, 15 May 2016 12:48:16 +0200 Subject: [PATCH] Initial Commit --- GNUmakefile | 48 ++++ audio/alsa.cpp | 212 ++++++++++++++ audio/ao.cpp | 73 +++++ audio/directsound.cpp | 184 ++++++++++++ audio/openal.cpp | 194 +++++++++++++ audio/oss.cpp | 115 ++++++++ audio/pulseaudio.cpp | 159 ++++++++++ audio/pulseaudiosimple.cpp | 95 ++++++ audio/wasapi.cpp | 169 +++++++++++ audio/xaudio2.cpp | 183 ++++++++++++ audio/xaudio2.hpp | 340 ++++++++++++++++++++++ input/carbon.cpp | 43 +++ input/joypad/directinput.cpp | 217 ++++++++++++++ input/joypad/sdl.cpp | 79 +++++ input/joypad/udev.cpp | 279 ++++++++++++++++++ input/joypad/xinput.cpp | 162 +++++++++++ input/keyboard/carbon.cpp | 158 ++++++++++ input/keyboard/quartz.cpp | 153 ++++++++++ input/keyboard/rawinput.cpp | 177 ++++++++++++ input/keyboard/xlib.cpp | 174 +++++++++++ input/mouse/rawinput.cpp | 122 ++++++++ input/mouse/xlib.cpp | 153 ++++++++++ input/quartz.cpp | 43 +++ input/sdl.cpp | 78 +++++ input/shared/rawinput.cpp | 158 ++++++++++ input/udev.cpp | 84 ++++++ input/windows.cpp | 105 +++++++ input/xlib.cpp | 73 +++++ ruby.cpp | 541 +++++++++++++++++++++++++++++++++++ ruby.hpp | 108 +++++++ video/cgl.cpp | 171 +++++++++++ video/direct3d.cpp | 443 ++++++++++++++++++++++++++++ video/directdraw.cpp | 171 +++++++++++ video/gdi.cpp | 89 ++++++ video/glx.cpp | 250 ++++++++++++++++ video/glx2.cpp | 245 ++++++++++++++++ video/opengl/bind.hpp | 101 +++++++ video/opengl/main.hpp | 212 ++++++++++++++ video/opengl/opengl.hpp | 93 ++++++ video/opengl/program.hpp | 108 +++++++ video/opengl/shaders.hpp | 91 ++++++ video/opengl/surface.hpp | 114 ++++++++ video/opengl/texture.hpp | 12 + video/opengl/utility.hpp | 106 +++++++ video/sdl.cpp | 135 +++++++++ video/wgl.cpp | 142 +++++++++ video/xshm.cpp | 205 +++++++++++++ video/xv.cpp | 489 +++++++++++++++++++++++++++++++ 48 files changed, 7856 insertions(+) create mode 100644 GNUmakefile create mode 100644 audio/alsa.cpp create mode 100644 audio/ao.cpp create mode 100644 audio/directsound.cpp create mode 100644 audio/openal.cpp create mode 100644 audio/oss.cpp create mode 100644 audio/pulseaudio.cpp create mode 100644 audio/pulseaudiosimple.cpp create mode 100644 audio/wasapi.cpp create mode 100644 audio/xaudio2.cpp create mode 100644 audio/xaudio2.hpp create mode 100644 input/carbon.cpp create mode 100644 input/joypad/directinput.cpp create mode 100644 input/joypad/sdl.cpp create mode 100644 input/joypad/udev.cpp create mode 100644 input/joypad/xinput.cpp create mode 100644 input/keyboard/carbon.cpp create mode 100644 input/keyboard/quartz.cpp create mode 100644 input/keyboard/rawinput.cpp create mode 100644 input/keyboard/xlib.cpp create mode 100644 input/mouse/rawinput.cpp create mode 100644 input/mouse/xlib.cpp create mode 100644 input/quartz.cpp create mode 100644 input/sdl.cpp create mode 100644 input/shared/rawinput.cpp create mode 100644 input/udev.cpp create mode 100644 input/windows.cpp create mode 100644 input/xlib.cpp create mode 100644 ruby.cpp create mode 100644 ruby.hpp create mode 100644 video/cgl.cpp create mode 100644 video/direct3d.cpp create mode 100644 video/directdraw.cpp create mode 100644 video/gdi.cpp create mode 100644 video/glx.cpp create mode 100644 video/glx2.cpp create mode 100644 video/opengl/bind.hpp create mode 100644 video/opengl/main.hpp create mode 100644 video/opengl/opengl.hpp create mode 100644 video/opengl/program.hpp create mode 100644 video/opengl/shaders.hpp create mode 100644 video/opengl/surface.hpp create mode 100644 video/opengl/texture.hpp create mode 100644 video/opengl/utility.hpp create mode 100644 video/sdl.cpp create mode 100644 video/wgl.cpp create mode 100644 video/xshm.cpp create mode 100644 video/xv.cpp diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..0c3d8a1 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,48 @@ +ifeq ($(platform),macosx) + rubyflags = $(objcppflags) $(flags) +else + rubyflags = $(cppflags) $(flags) +endif + +rubyflags += $(foreach c,$(subst .,_,$(call strupper,$(ruby))),-D$c) +rubyflags += $(if $(findstring .sdl,$(ruby)),$(shell sdl-config --cflags)) + +rubylink = + +rubylink += $(if $(findstring video.cgl,$(ruby)),-framework OpenGL) +rubylink += $(if $(findstring video.direct3d,$(ruby)),-ld3d9) +rubylink += $(if $(findstring video.directdraw,$(ruby)),-lddraw) +rubylink += $(if $(findstring video.glx,$(ruby)),-lGL) +rubylink += $(if $(findstring video.wgl,$(ruby)),-lopengl32) +rubylink += $(if $(findstring video.xv,$(ruby)),-lXv) + +rubylink += $(if $(findstring audio.alsa,$(ruby)),-lasound) +rubylink += $(if $(findstring audio.ao,$(ruby)),-lao) +rubylink += $(if $(findstring audio.directsound,$(ruby)),-ldsound) +rubylink += $(if $(findstring audio.pulseaudio,$(ruby)),-lpulse) +rubylink += $(if $(findstring audio.pulseaudiosimple,$(ruby)),-lpulse-simple) +rubylink += $(if $(findstring audio.wasapi,$(ruby)),-lavrt -luuid) +rubylink += $(if $(findstring audio.xaudio2,$(ruby)),-lole32) + +rubylink += $(if $(findstring input.udev,$(ruby)),-ludev) +rubylink += $(if $(findstring input.windows,$(ruby)),-ldinput8 -ldxguid) + +rubylink += $(if $(findstring .sdl,$(ruby)),$(shell sdl-config --libs)) + +ifeq ($(platform),windows) + rubylink += $(if $(findstring audio.openal,$(ruby)),-lopenal32) +endif + +ifeq ($(platform),macosx) + rubylink += $(if $(findstring audio.openal,$(ruby)),-framework OpenAL) +endif + +ifeq ($(platform),linux) + rubylink += -lX11 -lXext + rubylink += $(if $(findstring audio.openal,$(ruby)),-lopenal) +endif + +ifeq ($(platform),bsd) + rubylink += -lX11 -lXext + rubylink += $(if $(findstring audio.openal,$(ruby)),-lopenal) +endif diff --git a/audio/alsa.cpp b/audio/alsa.cpp new file mode 100644 index 0000000..d687720 --- /dev/null +++ b/audio/alsa.cpp @@ -0,0 +1,212 @@ +#include + +struct AudioALSA : Audio { + ~AudioALSA() { term(); } + + struct { + snd_pcm_t* handle = nullptr; + snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE; + snd_pcm_uframes_t buffer_size; + snd_pcm_uframes_t period_size; + int channels = 2; + const char* name = "default"; + } device; + + struct { + uint32_t* data = nullptr; + unsigned length = 0; + } buffer; + + struct { + bool synchronize = false; + unsigned frequency = 22050; + unsigned latency = 60; + } settings; + + auto cap(const string& name) -> bool { + if(name == Audio::Synchronize) return true; + if(name == Audio::Frequency) return true; + if(name == Audio::Latency) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Audio::Synchronize) return settings.synchronize; + if(name == Audio::Frequency) return settings.frequency; + if(name == Audio::Latency) return settings.latency; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Audio::Synchronize && value.is()) { + if(settings.synchronize != value.get()) { + settings.synchronize = value.get(); + if(device.handle) init(); + } + return true; + } + + if(name == Audio::Frequency && value.is()) { + if(settings.frequency != value.get()) { + settings.frequency = value.get(); + if(device.handle) init(); + } + return true; + } + + if(name == Audio::Latency && value.is()) { + if(settings.latency != value.get()) { + settings.latency = value.get(); + if(device.handle) init(); + } + return true; + } + + return false; + } + + auto sample(uint16_t left, uint16_t right) -> void { + if(!device.handle) return; + + buffer.data[buffer.length++] = left + (right << 16); + if(buffer.length < device.period_size) return; + + snd_pcm_sframes_t avail; + do { + avail = snd_pcm_avail_update(device.handle); + if(avail < 0) snd_pcm_recover(device.handle, avail, 1); + if(avail < buffer.length) { + if(settings.synchronize == false) { + buffer.length = 0; + return; + } + int error = snd_pcm_wait(device.handle, -1); + if(error < 0) snd_pcm_recover(device.handle, error, 1); + } + } while(avail < buffer.length); + + //below code has issues with PulseAudio sound server + #if 0 + if(settings.synchronize == false) { + snd_pcm_sframes_t avail = snd_pcm_avail_update(device.handle); + if(avail < device.period_size) { + buffer.length = 0; + return; + } + } + #endif + + uint32_t* buffer_ptr = buffer.data; + int i = 4; + + while((buffer.length > 0) && i--) { + snd_pcm_sframes_t written = snd_pcm_writei(device.handle, buffer_ptr, buffer.length); + if(written < 0) { + //no samples written + snd_pcm_recover(device.handle, written, 1); + } else if(written <= buffer.length) { + buffer.length -= written; + buffer_ptr += written; + } + } + + if(i < 0) { + if(buffer.data == buffer_ptr) { + buffer.length--; + buffer_ptr++; + } + memmove(buffer.data, buffer_ptr, buffer.length * sizeof(uint32_t)); + } + } + + auto clear() -> void { + } + + auto init() -> bool { + if(snd_pcm_open(&device.handle, device.name, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK) < 0) { + term(); + return false; + } + + //below code will not work with 24khz frequency rate (ALSA library bug) + #if 0 + if(snd_pcm_set_params(device.handle, device.format, SND_PCM_ACCESS_RW_INTERLEAVED, + device.channels, settings.frequency, 1, settings.latency * 1000) < 0) { + //failed to set device parameters + term(); + return false; + } + + if(snd_pcm_get_params(device.handle, &device.buffer_size, &device.period_size) < 0) { + device.period_size = settings.latency * 1000 * 1e-6 * settings.frequency / 4; + } + #endif + + snd_pcm_hw_params_t* hwparams; + snd_pcm_sw_params_t* swparams; + unsigned rate = settings.frequency; + unsigned buffer_time = settings.latency * 1000; + unsigned period_time = settings.latency * 1000 / 4; + + snd_pcm_hw_params_alloca(&hwparams); + if(snd_pcm_hw_params_any(device.handle, hwparams) < 0) { + term(); + return false; + } + + if(snd_pcm_hw_params_set_access(device.handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED) < 0 + || snd_pcm_hw_params_set_format(device.handle, hwparams, device.format) < 0 + || snd_pcm_hw_params_set_channels(device.handle, hwparams, device.channels) < 0 + || snd_pcm_hw_params_set_rate_near(device.handle, hwparams, &rate, 0) < 0 + || snd_pcm_hw_params_set_period_time_near(device.handle, hwparams, &period_time, 0) < 0 + || snd_pcm_hw_params_set_buffer_time_near(device.handle, hwparams, &buffer_time, 0) < 0 + ) { + term(); + return false; + } + + if(snd_pcm_hw_params(device.handle, hwparams) < 0) { + term(); + return false; + } + + if(snd_pcm_get_params(device.handle, &device.buffer_size, &device.period_size) < 0) { + term(); + return false; + } + + snd_pcm_sw_params_alloca(&swparams); + if(snd_pcm_sw_params_current(device.handle, swparams) < 0) { + term(); + return false; + } + + if(snd_pcm_sw_params_set_start_threshold(device.handle, swparams, + (device.buffer_size / device.period_size) * device.period_size) < 0 + ) { + term(); + return false; + } + + if(snd_pcm_sw_params(device.handle, swparams) < 0) { + term(); + return false; + } + + buffer.data = new uint32_t[device.period_size]; + return true; + } + + auto term() -> void { + if(device.handle) { + //snd_pcm_drain(device.handle); //prevents popping noise; but causes multi-second lag + snd_pcm_close(device.handle); + device.handle = 0; + } + + if(buffer.data) { + delete[] buffer.data; + buffer.data = 0; + } + } +}; diff --git a/audio/ao.cpp b/audio/ao.cpp new file mode 100644 index 0000000..bc45e5d --- /dev/null +++ b/audio/ao.cpp @@ -0,0 +1,73 @@ +#include + +struct AudioAO : Audio { + ~AudioAO() { term(); } + + int driver_id; + ao_sample_format driver_format; + ao_device* audio_device = nullptr; + + struct { + unsigned frequency = 22050; + } settings; + + auto cap(const string& name) -> bool { + if(name == Audio::Frequency) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Audio::Frequency) return settings.frequency; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Audio::Frequency && value.is()) { + settings.frequency = value.get(); + if(audio_device) init(); + return true; + } + + return false; + } + + auto sample(uint16_t l_sample, uint16_t r_sample) -> void { + uint32_t samp = (l_sample << 0) + (r_sample << 16); + ao_play(audio_device, (char*)&samp, 4); //This may need to be byte swapped for Big Endian + } + + auto clear() -> void { + } + + auto init() -> bool { + ao_initialize(); + + driver_id = ao_default_driver_id(); //ao_driver_id((const char*)driver) + if(driver_id < 0) return false; + + driver_format.bits = 16; + driver_format.channels = 2; + driver_format.rate = settings.frequency; + driver_format.byte_format = AO_FMT_LITTLE; + + ao_option* options = nullptr; + ao_info *di = ao_driver_info(driver_id); + if(!di) return false; + if(!strcmp(di->short_name, "alsa")) { + ao_append_option(&options, "buffer_time", "100000"); //100ms latency (default was 500ms) + } + + audio_device = ao_open_live(driver_id, &driver_format, options); + if(!audio_device) return false; + + return true; + } + + auto term() -> void { + if(audio_device) { + ao_close(audio_device); + audio_device = nullptr; + } + ao_shutdown(); + } +}; diff --git a/audio/directsound.cpp b/audio/directsound.cpp new file mode 100644 index 0000000..f3fb978 --- /dev/null +++ b/audio/directsound.cpp @@ -0,0 +1,184 @@ +#include + +struct AudioDS : Audio { + ~AudioDS() { term(); } + + LPDIRECTSOUND ds = nullptr; + LPDIRECTSOUNDBUFFER dsb_p = nullptr; + LPDIRECTSOUNDBUFFER dsb_b = nullptr; + DSBUFFERDESC dsbd; + WAVEFORMATEX wfx; + + struct { + uint rings = 0; + uint latency = 0; + + uint32_t* buffer = nullptr; + uint bufferoffset = 0; + + uint readring = 0; + uint writering = 0; + int distance = 0; + } device; + + struct { + HWND handle = nullptr; + bool synchronize = false; + uint frequency = 22050; + uint latency = 120; + } settings; + + auto cap(const string& name) -> bool { + if(name == Audio::Handle) return true; + if(name == Audio::Synchronize) return true; + if(name == Audio::Frequency) return true; + if(name == Audio::Latency) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Audio::Handle) return (uintptr_t)settings.handle; + if(name == Audio::Synchronize) return settings.synchronize; + if(name == Audio::Frequency) return settings.frequency; + if(name == Audio::Latency) return settings.latency; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Audio::Handle && value.is()) { + settings.handle = (HWND)value.get(); + return true; + } + + if(name == Audio::Synchronize && value.is()) { + settings.synchronize = value.get(); + if(ds) clear(); + return true; + } + + if(name == Audio::Frequency && value.is()) { + settings.frequency = value.get(); + if(ds) init(); + return true; + } + + if(name == Audio::Latency && value.is()) { + //latency settings below 40ms causes DirectSound to hang + settings.latency = max(40u, value.get()); + if(ds) init(); + return true; + } + + return false; + } + + auto sample(uint16_t left, uint16_t right) -> void { + device.buffer[device.bufferoffset++] = left + (right << 16); + if(device.bufferoffset < device.latency) return; + device.bufferoffset = 0; + + DWORD pos, size; + void* output; + + if(settings.synchronize) { + //wait until playback buffer has an empty ring to write new audio data to + while(device.distance >= device.rings - 1) { + dsb_b->GetCurrentPosition(&pos, 0); + uint activering = pos / (device.latency * 4); + if(activering == device.readring) continue; + + //subtract number of played rings from ring distance counter + device.distance -= (device.rings + activering - device.readring) % device.rings; + device.readring = activering; + + if(device.distance < 2) { + //buffer underflow; set max distance to recover quickly + device.distance = device.rings - 1; + device.writering = (device.rings + device.readring - 1) % device.rings; + break; + } + } + } + + device.writering = (device.writering + 1) % device.rings; + device.distance = (device.distance + 1) % device.rings; + + if(dsb_b->Lock(device.writering * device.latency * 4, device.latency * 4, &output, &size, 0, 0, 0) == DS_OK) { + memcpy(output, device.buffer, device.latency * 4); + dsb_b->Unlock(output, size, 0, 0); + } + } + + auto clear() -> void { + device.readring = 0; + device.writering = device.rings - 1; + device.distance = device.rings - 1; + + device.bufferoffset = 0; + if(device.buffer) memset(device.buffer, 0, device.latency * device.rings * 4); + + if(!dsb_b) return; + dsb_b->Stop(); + dsb_b->SetCurrentPosition(0); + + DWORD size; + void* output; + dsb_b->Lock(0, device.latency * device.rings * 4, &output, &size, 0, 0, 0); + memset(output, 0, size); + dsb_b->Unlock(output, size, 0, 0); + + dsb_b->Play(0, 0, DSBPLAY_LOOPING); + } + + auto init() -> bool { + settings.handle = GetDesktopWindow(); + + device.rings = 8; + device.latency = settings.frequency * settings.latency / device.rings / 1000.0 + 0.5; + device.buffer = new uint32_t[device.latency * device.rings]; + device.bufferoffset = 0; + + if(DirectSoundCreate(0, &ds, 0) != DS_OK) return term(), false; + ds->SetCooperativeLevel((HWND)settings.handle, DSSCL_PRIORITY); + + memory::fill(&dsbd, sizeof(dsbd)); + dsbd.dwSize = sizeof(dsbd); + dsbd.dwFlags = DSBCAPS_PRIMARYBUFFER; + dsbd.dwBufferBytes = 0; + dsbd.lpwfxFormat = 0; + ds->CreateSoundBuffer(&dsbd, &dsb_p, 0); + + memory::fill(&wfx, sizeof(wfx)); + wfx.wFormatTag = WAVE_FORMAT_PCM; + wfx.nChannels = 2; + wfx.nSamplesPerSec = settings.frequency; + wfx.wBitsPerSample = 16; + wfx.nBlockAlign = wfx.wBitsPerSample / 8 * wfx.nChannels; + wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; + dsb_p->SetFormat(&wfx); + + memory::fill(&dsbd, sizeof(dsbd)); + dsbd.dwSize = sizeof(dsbd); + dsbd.dwFlags = DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_CTRLFREQUENCY | DSBCAPS_GLOBALFOCUS | DSBCAPS_LOCSOFTWARE; + dsbd.dwBufferBytes = device.latency * device.rings * sizeof(uint32_t); + dsbd.guid3DAlgorithm = GUID_NULL; + dsbd.lpwfxFormat = &wfx; + ds->CreateSoundBuffer(&dsbd, &dsb_b, 0); + dsb_b->SetFrequency(settings.frequency); + dsb_b->SetCurrentPosition(0); + + clear(); + return true; + } + + auto term() -> void { + if(device.buffer) { + delete[] device.buffer; + device.buffer = nullptr; + } + + if(dsb_b) { dsb_b->Stop(); dsb_b->Release(); dsb_b = nullptr; } + if(dsb_p) { dsb_p->Stop(); dsb_p->Release(); dsb_p = nullptr; } + if(ds) { ds->Release(); ds = nullptr; } + } +}; diff --git a/audio/openal.cpp b/audio/openal.cpp new file mode 100644 index 0000000..8599e2d --- /dev/null +++ b/audio/openal.cpp @@ -0,0 +1,194 @@ +#if defined(PLATFORM_MACOSX) + #include + #include +#else + #include + #include +#endif + +struct AudioOpenAL : Audio { + ~AudioOpenAL() { term(); } + + struct { + ALCdevice* handle = nullptr; + ALCcontext* context = nullptr; + ALuint source = 0; + ALenum format = AL_FORMAT_STEREO16; + unsigned latency = 0; + unsigned queueLength = 0; + } device; + + struct { + uint32_t* data = nullptr; + unsigned length = 0; + unsigned size = 0; + } buffer; + + struct { + bool synchronize = true; + unsigned frequency = 22050; + unsigned latency = 40; + } settings; + + auto cap(const string& name) -> bool { + if(name == Audio::Synchronize) return true; + if(name == Audio::Frequency) return true; + if(name == Audio::Latency) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Audio::Synchronize) return settings.synchronize; + if(name == Audio::Frequency) return settings.frequency; + if(name == Audio::Latency) return settings.latency; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Audio::Synchronize && value.is()) { + settings.synchronize = value.get(); + return true; + } + + if(name == Audio::Frequency && value.is()) { + settings.frequency = value.get(); + return true; + } + + if(name == Audio::Latency && value.is()) { + if(settings.latency != value.get()) { + settings.latency = value.get(); + updateLatency(); + } + return true; + } + + return false; + } + + auto sample(uint16_t left, uint16_t right) -> void { + buffer.data[buffer.length++] = left << 0 | right << 16; + if(buffer.length < buffer.size) return; + + ALuint albuffer = 0; + int processed = 0; + while(true) { + alGetSourcei(device.source, AL_BUFFERS_PROCESSED, &processed); + while(processed--) { + alSourceUnqueueBuffers(device.source, 1, &albuffer); + alDeleteBuffers(1, &albuffer); + device.queueLength--; + } + //wait for buffer playback to catch up to sample generation if not synchronizing + if(settings.synchronize == false || device.queueLength < 3) break; + } + + if(device.queueLength < 3) { + alGenBuffers(1, &albuffer); + alBufferData(albuffer, device.format, buffer.data, buffer.size * 4, settings.frequency); + alSourceQueueBuffers(device.source, 1, &albuffer); + device.queueLength++; + } + + ALint playing; + alGetSourcei(device.source, AL_SOURCE_STATE, &playing); + if(playing != AL_PLAYING) alSourcePlay(device.source); + buffer.length = 0; + } + + auto clear() -> void { + } + + auto init() -> bool { + updateLatency(); + device.queueLength = 0; + + bool success = false; + if(device.handle = alcOpenDevice(nullptr)) { + if(device.context = alcCreateContext(device.handle, nullptr)) { + alcMakeContextCurrent(device.context); + alGenSources(1, &device.source); + + //alSourcef (device.source, AL_PITCH, 1.0); + //alSourcef (device.source, AL_GAIN, 1.0); + //alSource3f(device.source, AL_POSITION, 0.0, 0.0, 0.0); + //alSource3f(device.source, AL_VELOCITY, 0.0, 0.0, 0.0); + //alSource3f(device.source, AL_DIRECTION, 0.0, 0.0, 0.0); + //alSourcef (device.source, AL_ROLLOFF_FACTOR, 0.0); + //alSourcei (device.source, AL_SOURCE_RELATIVE, AL_TRUE); + + alListener3f(AL_POSITION, 0.0, 0.0, 0.0); + alListener3f(AL_VELOCITY, 0.0, 0.0, 0.0); + ALfloat listener_orientation[] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + alListenerfv(AL_ORIENTATION, listener_orientation); + + success = true; + } + } + + if(success == false) { + term(); + return false; + } + + return true; + } + + auto term() -> void { + if(alIsSource(device.source) == AL_TRUE) { + int playing = 0; + alGetSourcei(device.source, AL_SOURCE_STATE, &playing); + if(playing == AL_PLAYING) { + alSourceStop(device.source); + int queued = 0; + alGetSourcei(device.source, AL_BUFFERS_QUEUED, &queued); + while(queued--) { + ALuint albuffer = 0; + alSourceUnqueueBuffers(device.source, 1, &albuffer); + alDeleteBuffers(1, &albuffer); + device.queueLength--; + } + } + + alDeleteSources(1, &device.source); + device.source = 0; + } + + if(device.context) { + alcMakeContextCurrent(nullptr); + alcDestroyContext(device.context); + device.context = 0; + } + + if(device.handle) { + alcCloseDevice(device.handle); + device.handle = 0; + } + + if(buffer.data) { + delete[] buffer.data; + buffer.data = 0; + } + } + +private: + auto queryDevices() -> lstring { + lstring result; + + const char* buffer = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); + if(!buffer) return result; + + while(buffer[0] || buffer[1]) { + result.append(buffer); + while(buffer[0]) buffer++; + } + + return result; + } + + auto updateLatency() -> void { + if(buffer.data) delete[] buffer.data; + buffer.size = settings.frequency * settings.latency / 1000.0 + 0.5; + buffer.data = new uint32_t[buffer.size](); + } +}; diff --git a/audio/oss.cpp b/audio/oss.cpp new file mode 100644 index 0000000..4a539b7 --- /dev/null +++ b/audio/oss.cpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include + +//OSS4 soundcard.h includes below SNDCTL defines, but OSS3 does not +//However, OSS4 soundcard.h does not reside in +//Therefore, attempt to manually define SNDCTL values if using OSS3 header +//Note that if the defines below fail to work on any specific platform, one can point soundcard.h +//above to the correct location for OSS4 (usually /usr/lib/oss/include/sys/soundcard.h) +//Failing that, one can disable OSS4 ioctl calls inside init() and remove the below defines + +#ifndef SNDCTL_DSP_COOKEDMODE + #define SNDCTL_DSP_COOKEDMODE _IOW('P', 30, signed) +#endif + +#ifndef SNDCTL_DSP_POLICY + #define SNDCTL_DSP_POLICY _IOW('P', 45, signed) +#endif + +struct AudioOSS : Audio { + ~AudioOSS() { term(); } + + struct { + signed fd = -1; + signed format = AFMT_S16_LE; + signed channels = 2; + } device; + + struct { + string device = "/dev/dsp"; + bool synchronize = true; + unsigned frequency = 22050; + } settings; + + auto cap(const string& name) -> bool { + if(name == Audio::Device) return true; + if(name == Audio::Synchronize) return true; + if(name == Audio::Frequency) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Audio::Device) return settings.device; + if(name == Audio::Synchronize) return settings.synchronize; + if(name == Audio::Frequency) return settings.frequency; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Audio::Device && value.is()) { + settings.device = value.get(); + if(!settings.device) settings.device = "/dev/dsp"; + return true; + } + + if(name == Audio::Synchronize && value.is()) { + settings.synchronize = value.get(); + updateSynchronization(); + return true; + } + + if(name == Audio::Frequency && value.is()) { + settings.frequency = value.get(); + if(device.fd >= 0) init(); + return true; + } + + return false; + } + + auto sample(uint16_t left, uint16_t right) -> void { + uint32_t sample = left << 0 | right << 16; + auto unused = write(device.fd, &sample, 4); + } + + auto clear() -> void { + } + + auto init() -> bool { + device.fd = open(settings.device, O_WRONLY, O_NONBLOCK); + if(device.fd < 0) return false; + + #if 1 //SOUND_VERSION >= 0x040000 + //attempt to enable OSS4-specific features regardless of version + //OSS3 ioctl calls will silently fail, but sound will still work + signed cooked = 1, policy = 4; //policy should be 0 - 10, lower = less latency, more CPU usage + ioctl(device.fd, SNDCTL_DSP_COOKEDMODE, &cooked); + ioctl(device.fd, SNDCTL_DSP_POLICY, &policy); + #endif + signed freq = settings.frequency; + ioctl(device.fd, SNDCTL_DSP_CHANNELS, &device.channels); + ioctl(device.fd, SNDCTL_DSP_SETFMT, &device.format); + ioctl(device.fd, SNDCTL_DSP_SPEED, &freq); + + updateSynchronization(); + return true; + } + + auto term() -> void { + if(device.fd >= 0) { + close(device.fd); + device.fd = -1; + } + } + +private: + auto updateSynchronization() -> void { + if(device.fd < 0) return; + auto flags = fcntl(device.fd, F_GETFL); + if(flags < 0) return; + settings.synchronize ? flags &=~ O_NONBLOCK : flags |= O_NONBLOCK; + fcntl(device.fd, F_SETFL, flags); + } +}; diff --git a/audio/pulseaudio.cpp b/audio/pulseaudio.cpp new file mode 100644 index 0000000..895a315 --- /dev/null +++ b/audio/pulseaudio.cpp @@ -0,0 +1,159 @@ +#include + +struct AudioPulseAudio : Audio { + ~AudioPulseAudio() { term(); } + + struct { + pa_mainloop* mainloop = nullptr; + pa_context* context = nullptr; + pa_stream* stream = nullptr; + pa_sample_spec spec; + pa_buffer_attr buffer_attr; + bool first; + } device; + + struct { + uint32_t* data = nullptr; + size_t size; + unsigned offset; + } buffer; + + struct { + bool synchronize = false; + unsigned frequency = 22050; + unsigned latency = 60; + } settings; + + auto cap(const string& name) -> bool { + if(name == Audio::Synchronize) return true; + if(name == Audio::Frequency) return true; + if(name == Audio::Latency) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Audio::Synchronize) return settings.synchronize; + if(name == Audio::Frequency) return settings.frequency; + if(name == Audio::Latency) return settings.latency; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Audio::Synchronize && value.is()) { + settings.synchronize = value.get(); + return true; + } + + if(name == Audio::Frequency && value.is()) { + settings.frequency = value.get(); + if(device.stream) { + pa_operation_unref(pa_stream_update_sample_rate(device.stream, settings.frequency, NULL, NULL)); + } + return true; + } + + if(name == Audio::Latency && value.is()) { + settings.latency = value.get(); + if(device.stream) { + device.buffer_attr.tlength = pa_usec_to_bytes(settings.latency * PA_USEC_PER_MSEC, &device.spec); + pa_stream_set_buffer_attr(device.stream, &device.buffer_attr, NULL, NULL); + } + return true; + } + + return false; + } + + auto sample(uint16_t left, uint16_t right) -> void { + pa_stream_begin_write(device.stream, (void**)&buffer.data, &buffer.size); + buffer.data[buffer.offset++] = left + (right << 16); + if((buffer.offset + 1) * pa_frame_size(&device.spec) <= buffer.size) return; + + while(true) { + if(device.first) { + device.first = false; + pa_mainloop_iterate(device.mainloop, 0, NULL); + } else { + pa_mainloop_iterate(device.mainloop, 1, NULL); + } + unsigned length = pa_stream_writable_size(device.stream); + if(length >= buffer.offset * pa_frame_size(&device.spec)) break; + if(settings.synchronize == false) { + buffer.offset = 0; + return; + } + } + + pa_stream_write(device.stream, (const void*)buffer.data, buffer.offset * pa_frame_size(&device.spec), NULL, 0LL, PA_SEEK_RELATIVE); + buffer.data = 0; + buffer.offset = 0; + } + + auto clear() -> void { + } + + auto init() -> bool { + device.mainloop = pa_mainloop_new(); + + device.context = pa_context_new(pa_mainloop_get_api(device.mainloop), "ruby::pulseaudio"); + pa_context_connect(device.context, NULL, PA_CONTEXT_NOFLAGS, NULL); + + pa_context_state_t cstate; + do { + pa_mainloop_iterate(device.mainloop, 1, NULL); + cstate = pa_context_get_state(device.context); + if(!PA_CONTEXT_IS_GOOD(cstate)) return false; + } while(cstate != PA_CONTEXT_READY); + + device.spec.format = PA_SAMPLE_S16LE; + device.spec.channels = 2; + device.spec.rate = settings.frequency; + device.stream = pa_stream_new(device.context, "audio", &device.spec, NULL); + + device.buffer_attr.maxlength = -1; + device.buffer_attr.tlength = pa_usec_to_bytes(settings.latency * PA_USEC_PER_MSEC, &device.spec); + device.buffer_attr.prebuf = -1; + device.buffer_attr.minreq = -1; + device.buffer_attr.fragsize = -1; + + pa_stream_flags_t flags = (pa_stream_flags_t)(PA_STREAM_ADJUST_LATENCY | PA_STREAM_VARIABLE_RATE); + pa_stream_connect_playback(device.stream, NULL, &device.buffer_attr, flags, NULL, NULL); + + pa_stream_state_t sstate; + do { + pa_mainloop_iterate(device.mainloop, 1, NULL); + sstate = pa_stream_get_state(device.stream); + if(!PA_STREAM_IS_GOOD(sstate)) return false; + } while(sstate != PA_STREAM_READY); + + buffer.size = 960; + buffer.offset = 0; + device.first = true; + + return true; + } + + auto term() -> void { + if(buffer.data) { + pa_stream_cancel_write(device.stream); + buffer.data = nullptr; + } + + if(device.stream) { + pa_stream_disconnect(device.stream); + pa_stream_unref(device.stream); + device.stream = nullptr; + } + + if(device.context) { + pa_context_disconnect(device.context); + pa_context_unref(device.context); + device.context = nullptr; + } + + if(device.mainloop) { + pa_mainloop_free(device.mainloop); + device.mainloop = nullptr; + } + } +}; diff --git a/audio/pulseaudiosimple.cpp b/audio/pulseaudiosimple.cpp new file mode 100644 index 0000000..9f7c400 --- /dev/null +++ b/audio/pulseaudiosimple.cpp @@ -0,0 +1,95 @@ +#include +#include + +struct AudioPulseAudioSimple : Audio { + ~AudioPulseAudioSimple() { term(); } + + struct { + pa_simple* handle = nullptr; + pa_sample_spec spec; + } device; + + struct { + uint32_t* data = nullptr; + unsigned offset = 0; + } buffer; + + struct { + unsigned frequency = 22050; + } settings; + + auto cap(const string& name) -> bool { + if(name == Audio::Frequency) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Audio::Frequency) return settings.frequency; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Audio::Frequency && value.is()) { + settings.frequency = value.get(); + if(device.handle) init(); + return true; + } + + return false; + } + + auto sample(uint16_t left, uint16_t right) -> void { + if(!device.handle) return; + + buffer.data[buffer.offset++] = left + (right << 16); + if(buffer.offset >= 64) { + int error; + pa_simple_write(device.handle, (const void*)buffer.data, buffer.offset * sizeof(uint32_t), &error); + buffer.offset = 0; + } + } + + auto clear() -> void { + } + + auto init() -> bool { + device.spec.format = PA_SAMPLE_S16LE; + device.spec.channels = 2; + device.spec.rate = settings.frequency; + + int error = 0; + device.handle = pa_simple_new( + 0, //default server + "ruby::pulseaudiosimple", //application name + PA_STREAM_PLAYBACK, //direction + 0, //default device + "audio", //stream description + &device.spec, //sample format + 0, //default channel map + 0, //default buffering attributes + &error //error code + ); + if(!device.handle) { + fprintf(stderr, "ruby::pulseaudiosimple failed to initialize - %s\n", pa_strerror(error)); + return false; + } + + buffer.data = new uint32_t[64]; + buffer.offset = 0; + return true; + } + + auto term() -> void { + if(device.handle) { + int error; + pa_simple_flush(device.handle, &error); + pa_simple_free(device.handle); + device.handle = nullptr; + } + + if(buffer.data) { + delete[] buffer.data; + buffer.data = nullptr; + } + } +}; diff --git a/audio/wasapi.cpp b/audio/wasapi.cpp new file mode 100644 index 0000000..adc2865 --- /dev/null +++ b/audio/wasapi.cpp @@ -0,0 +1,169 @@ +#include +#include +#include +#include +#include +#include + +#include + +struct AudioWASAPI : Audio { + ~AudioWASAPI() { term(); } + + struct { + bool exclusive = false; + bool synchronize = false; + uint frequency = 44100; + } settings; + + auto cap(const string& name) -> bool { + if(name == Audio::Exclusive) return true; + if(name == Audio::Synchronize) return true; + if(name == Audio::Frequency) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Audio::Exclusive) return settings.exclusive; + if(name == Audio::Synchronize) return settings.synchronize; + if(name == Audio::Frequency) return settings.frequency; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Audio::Exclusive && value.get()) { + settings.exclusive = value.get(); + return true; + } + + if(name == Audio::Synchronize && value.is()) { + settings.synchronize = value.get(); + return true; + } + + if(name == Audio::Frequency && value.is()) { + settings.frequency = value.get(); + dsp.setFrequency(settings.frequency); + return true; + } + + return false; + } + + auto sample(uint16 left, uint16 right) -> void { + int samples[] = {(int16)left, (int16)right}; + dsp.sample(samples); + while(dsp.pending()) { + dsp.read(samples); + write(samples); + } + } + + auto clear() -> void { + audioClient->Stop(); + renderClient->GetBuffer(bufferFrameCount, &bufferData); + + renderClient->ReleaseBuffer(bufferFrameCount, 0); + audioClient->Start(); + } + + auto init() -> bool { + if(CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&enumerator) != S_OK) return false; + if(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device) != S_OK) return false; + if(device->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, (void**)&audioClient) != S_OK) return false; + + if(settings.exclusive) { + if(device->OpenPropertyStore(STGM_READ, &propertyStore) != S_OK) return false; + if(propertyStore->GetValue(PKEY_AudioEngine_DeviceFormat, &propVariant) != S_OK) return false; + waveFormat = (WAVEFORMATEX*)propVariant.blob.pBlobData; + if(audioClient->GetDevicePeriod(nullptr, &devicePeriod) != S_OK) return false; + if(audioClient->Initialize(AUDCLNT_SHAREMODE_EXCLUSIVE, 0, devicePeriod, devicePeriod, waveFormat, nullptr) != S_OK) return false; + taskHandle = AvSetMmThreadCharacteristics(L"Pro Audio", &taskIndex); + } else { + if(audioClient->GetMixFormat(&waveFormat) != S_OK) return false; + if(audioClient->GetDevicePeriod(&devicePeriod, nullptr)) return false; + if(audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, devicePeriod, 0, waveFormat, nullptr) != S_OK) return false; + } + + if(audioClient->GetService(IID_IAudioRenderClient, (void**)&renderClient) != S_OK) return false; + if(audioClient->GetBufferSize(&bufferFrameCount) != S_OK) return false; + + switch(((WAVEFORMATEXTENSIBLE*)waveFormat)->SubFormat.Data1) { + case 1: ieee = false; break; //fixed point + case 3: ieee = true; break; //floating point + default: return false; //unknown format; abort + } + + dsp.setChannels(2); + dsp.setPrecision(16); + dsp.setFrequency(settings.frequency); + + dsp.setResampler(DSP::ResampleEngine::Linear); + dsp.setResamplerFrequency(waveFormat->nSamplesPerSec); + dsp.setChannels(waveFormat->nChannels); + dsp.setPrecision(waveFormat->wBitsPerSample); + + print("[WASAPI]\n"); + print("Channels: ", waveFormat->nChannels, "\n"); + print("Precision: ", waveFormat->wBitsPerSample, "\n"); + print("Frequency: ", waveFormat->nSamplesPerSec, "\n"); + print("IEEE-754: ", ieee, "\n"); + print("Exclusive: ", settings.exclusive, "\n\n"); + + audioClient->Start(); + return true; + } + + auto term() -> void { + if(audioClient) { + audioClient->Stop(); + } + + if(taskHandle) { + AvRevertMmThreadCharacteristics(taskHandle); + taskHandle = nullptr; + } + } + +private: + auto write(int samples[]) -> void { + while(true) { + uint32 padding = 0; + audioClient->GetCurrentPadding(&padding); + if(bufferFrameCount - padding < 1) { + if(!settings.synchronize) return; + continue; + } + break; + } + + renderClient->GetBuffer(1, &bufferData); + + if(ieee) { + auto buffer = (float*)bufferData; + buffer[0] = (int16)samples[0] / 32768.0; + buffer[1] = (int16)samples[1] / 32768.0; + } else { + auto buffer = (int16*)bufferData; + buffer[0] = (int16)samples[0]; + buffer[1] = (int16)samples[1]; + } + + renderClient->ReleaseBuffer(1, 0); + } + + DSP dsp; + IMMDeviceEnumerator* enumerator = nullptr; + IMMDevice* device = nullptr; + IPropertyStore* propertyStore = nullptr; + IAudioClient* audioClient = nullptr; + IAudioRenderClient* renderClient = nullptr; + WAVEFORMATEX* waveFormat = nullptr; + PROPVARIANT propVariant; + HANDLE taskHandle = nullptr; + DWORD taskIndex = 0; + REFERENCE_TIME devicePeriod = 0; + uint32 bufferFrameCount = 0; + uint8* bufferData = nullptr; + bool ieee = false; +}; diff --git a/audio/xaudio2.cpp b/audio/xaudio2.cpp new file mode 100644 index 0000000..72fbdb4 --- /dev/null +++ b/audio/xaudio2.cpp @@ -0,0 +1,183 @@ +#include "xaudio2.hpp" +#include + +struct AudioXAudio2 : Audio, public IXAudio2VoiceCallback { + ~AudioXAudio2() { term(); } + + IXAudio2* pXAudio2 = nullptr; + IXAudio2MasteringVoice* pMasterVoice = nullptr; + IXAudio2SourceVoice* pSourceVoice = nullptr; + + //inherited from IXAudio2VoiceCallback + STDMETHODIMP_(void) OnBufferStart(void* pBufferContext){} + STDMETHODIMP_(void) OnLoopEnd(void* pBufferContext){} + STDMETHODIMP_(void) OnStreamEnd() {} + STDMETHODIMP_(void) OnVoiceError(void* pBufferContext, HRESULT Error) {} + STDMETHODIMP_(void) OnVoiceProcessingPassEnd() {} + STDMETHODIMP_(void) OnVoiceProcessingPassStart(UINT32 BytesRequired) {} + + struct { + unsigned buffers = 0; + unsigned latency = 0; + + uint32_t* buffer = nullptr; + unsigned bufferoffset = 0; + + volatile long submitbuffers = 0; + unsigned writebuffer = 0; + } device; + + struct { + bool synchronize = false; + unsigned frequency = 22050; + unsigned latency = 120; + } settings; + + auto cap(const string& name) -> bool { + if(name == Audio::Synchronize) return true; + if(name == Audio::Frequency) return true; + if(name == Audio::Latency) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Audio::Synchronize) return settings.synchronize; + if(name == Audio::Frequency) return settings.frequency; + if(name == Audio::Latency) return settings.latency; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Audio::Synchronize && value.is()) { + settings.synchronize = value.get(); + if(pXAudio2) clear(); + return true; + } + + if(name == Audio::Frequency && value.is()) { + settings.frequency = value.get(); + if(pXAudio2) init(); + return true; + } + + if(name == Audio::Latency && value.is()) { + settings.latency = value.get(); + if(pXAudio2) init(); + return true; + } + + return false; + } + + auto pushbuffer(unsigned bytes, uint32_t* pAudioData) -> void { + XAUDIO2_BUFFER xa2buffer = {0}; + xa2buffer.AudioBytes = bytes; + xa2buffer.pAudioData = reinterpret_cast(pAudioData); + xa2buffer.pContext = 0; + InterlockedIncrement(&device.submitbuffers); + pSourceVoice->SubmitSourceBuffer(&xa2buffer); + } + + auto sample(uint16_t left, uint16_t right) -> void { + device.buffer[device.writebuffer * device.latency + device.bufferoffset++] = left + (right << 16); + if(device.bufferoffset < device.latency) return; + device.bufferoffset = 0; + + if(device.submitbuffers == device.buffers - 1) { + if(settings.synchronize == true) { + //wait until there is at least one other free buffer for the next sample + while(device.submitbuffers == device.buffers - 1) { + //Sleep(0); + } + } else { //we need one free buffer for the next sample, so ignore the current contents + return; + } + } + + pushbuffer(device.latency * 4,device.buffer + device.writebuffer * device.latency); + + device.writebuffer = (device.writebuffer + 1) % device.buffers; + } + + auto clear() -> void { + if(!pSourceVoice) return; + pSourceVoice->Stop(0); + pSourceVoice->FlushSourceBuffers(); //calls OnBufferEnd for all currently submitted buffers + + device.writebuffer = 0; + + device.bufferoffset = 0; + if(device.buffer) memset(device.buffer, 0, device.latency * device.buffers * 4); + + pSourceVoice->Start(0); + } + + auto init() -> bool { + device.buffers = 8; + device.latency = settings.frequency * settings.latency / device.buffers / 1000.0 + 0.5; + device.buffer = new uint32_t[device.latency * device.buffers]; + device.bufferoffset = 0; + device.submitbuffers = 0; + + HRESULT hr; + if(FAILED(hr = XAudio2Create(&pXAudio2, 0 , XAUDIO2_DEFAULT_PROCESSOR))) { + return false; + } + + unsigned deviceCount = 0; + pXAudio2->GetDeviceCount(&deviceCount); + if(deviceCount == 0) { term(); return false; } + + unsigned deviceID = 0; + for(unsigned deviceIndex = 0; deviceIndex < deviceCount; deviceIndex++) { + XAUDIO2_DEVICE_DETAILS deviceDetails; + memset(&deviceDetails, 0, sizeof(XAUDIO2_DEVICE_DETAILS)); + pXAudio2->GetDeviceDetails(deviceIndex, &deviceDetails); + if(deviceDetails.Role & DefaultGameDevice) deviceID = deviceIndex; + } + + if(FAILED(hr = pXAudio2->CreateMasteringVoice(&pMasterVoice, 2, settings.frequency, 0, deviceID, NULL))) { + return false; + } + + WAVEFORMATEX wfx; + wfx.wFormatTag = WAVE_FORMAT_PCM; + wfx.nChannels = 2; + wfx.nSamplesPerSec = settings.frequency; + wfx.nBlockAlign = 4; + wfx.wBitsPerSample = 16; + wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; + wfx.cbSize = 0; + + if(FAILED(hr = pXAudio2->CreateSourceVoice(&pSourceVoice, (WAVEFORMATEX*)&wfx, XAUDIO2_VOICE_NOSRC, XAUDIO2_DEFAULT_FREQ_RATIO, this, NULL, NULL))) { + return false; + } + + clear(); + return true; + } + + auto term() -> void { + if(pSourceVoice) { + pSourceVoice->Stop(0); + pSourceVoice->DestroyVoice(); + pSourceVoice = nullptr; + } + if(pMasterVoice) { + pMasterVoice->DestroyVoice(); + pMasterVoice = nullptr; + } + if(pXAudio2) { + pXAudio2->Release(); + pXAudio2 = nullptr; + } + if(device.buffer) { + delete[] device.buffer; + device.buffer = nullptr; + } + } + + STDMETHODIMP_(void) OnBufferEnd(void* pBufferContext) { + InterlockedDecrement(&device.submitbuffers); + } +}; diff --git a/audio/xaudio2.hpp b/audio/xaudio2.hpp new file mode 100644 index 0000000..e283f50 --- /dev/null +++ b/audio/xaudio2.hpp @@ -0,0 +1,340 @@ +/* + xaudio2.hpp (2010-08-14) + author: OV2 + + ruby-specific header to provide mingw-friendly xaudio2 interfaces +*/ + +#ifndef XAUDIO2_RUBY_H +#define XAUDIO2_RUBY_H + +//64-bit GCC fix +#define GUID_EXT EXTERN_C +#define GUID_SECT + +#include + +#define DEFINE_GUID_X(n,l,w1,w2,b1,b2,b3,b4,b5,b6,b7,b8) GUID_EXT const GUID n GUID_SECT = {l,w1,w2,{b1,b2,b3,b4,b5,b6,b7,b8}} +#define DEFINE_CLSID_X(className, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + DEFINE_GUID_X(CLSID_##className, 0x##l, 0x##w1, 0x##w2, 0x##b1, 0x##b2, 0x##b3, 0x##b4, 0x##b5, 0x##b6, 0x##b7, 0x##b8) +#define DEFINE_IID_X(interfaceName, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + DEFINE_GUID_X(IID_##interfaceName, 0x##l, 0x##w1, 0x##w2, 0x##b1, 0x##b2, 0x##b3, 0x##b4, 0x##b5, 0x##b6, 0x##b7, 0x##b8) +#define X2DEFAULT(x) =x + +DEFINE_CLSID_X(XAudio2, e21a7345, eb21, 468e, be, 50, 80, 4d, b9, 7c, f7, 08); +DEFINE_CLSID_X(XAudio2_Debug, f7a76c21, 53d4, 46bb, ac, 53, 8b, 45, 9c, ae, 46, bd); +DEFINE_IID_X(IXAudio2, 8bcf1f58, 9fe7, 4583, 8a, c6, e2, ad, c4, 65, c8, bb); + +DECLARE_INTERFACE(IXAudio2Voice); + +#define XAUDIO2_COMMIT_NOW 0 +#define XAUDIO2_DEFAULT_CHANNELS 0 +#define XAUDIO2_DEFAULT_SAMPLERATE 0 +#define XAUDIO2_DEFAULT_FREQ_RATIO 4.0f +#define XAUDIO2_DEBUG_ENGINE 0x0001 +#define XAUDIO2_VOICE_NOSRC 0x0004 + +typedef struct +{ + WAVEFORMATEX Format; + union + { + WORD wValidBitsPerSample; + WORD wSamplesPerBlock; + WORD wReserved; + } Samples; + DWORD dwChannelMask; + GUID SubFormat; +} WAVEFORMATEXTENSIBLE, *PWAVEFORMATEXTENSIBLE, *LPPWAVEFORMATEXTENSIBLE; +typedef const WAVEFORMATEXTENSIBLE* LPCWAVEFORMATEXTENSIBLE; + +typedef enum XAUDIO2_DEVICE_ROLE +{ + NotDefaultDevice = 0x0, + DefaultConsoleDevice = 0x1, + DefaultMultimediaDevice = 0x2, + DefaultCommunicationsDevice = 0x4, + DefaultGameDevice = 0x8, + GlobalDefaultDevice = 0xf, + InvalidDeviceRole = ~GlobalDefaultDevice +} XAUDIO2_DEVICE_ROLE; + +typedef struct XAUDIO2_DEVICE_DETAILS +{ + WCHAR DeviceID[256]; + WCHAR DisplayName[256]; + XAUDIO2_DEVICE_ROLE Role; + WAVEFORMATEXTENSIBLE OutputFormat; +} XAUDIO2_DEVICE_DETAILS; + +typedef struct XAUDIO2_VOICE_DETAILS +{ + UINT32 CreationFlags; + UINT32 InputChannels; + UINT32 InputSampleRate; +} XAUDIO2_VOICE_DETAILS; + +typedef enum XAUDIO2_WINDOWS_PROCESSOR_SPECIFIER +{ + Processor1 = 0x00000001, + Processor2 = 0x00000002, + Processor3 = 0x00000004, + Processor4 = 0x00000008, + Processor5 = 0x00000010, + Processor6 = 0x00000020, + Processor7 = 0x00000040, + Processor8 = 0x00000080, + Processor9 = 0x00000100, + Processor10 = 0x00000200, + Processor11 = 0x00000400, + Processor12 = 0x00000800, + Processor13 = 0x00001000, + Processor14 = 0x00002000, + Processor15 = 0x00004000, + Processor16 = 0x00008000, + Processor17 = 0x00010000, + Processor18 = 0x00020000, + Processor19 = 0x00040000, + Processor20 = 0x00080000, + Processor21 = 0x00100000, + Processor22 = 0x00200000, + Processor23 = 0x00400000, + Processor24 = 0x00800000, + Processor25 = 0x01000000, + Processor26 = 0x02000000, + Processor27 = 0x04000000, + Processor28 = 0x08000000, + Processor29 = 0x10000000, + Processor30 = 0x20000000, + Processor31 = 0x40000000, + Processor32 = 0x80000000, + XAUDIO2_ANY_PROCESSOR = 0xffffffff, + XAUDIO2_DEFAULT_PROCESSOR = XAUDIO2_ANY_PROCESSOR +} XAUDIO2_WINDOWS_PROCESSOR_SPECIFIER, XAUDIO2_PROCESSOR; + +typedef struct XAUDIO2_VOICE_SENDS +{ + UINT32 OutputCount; + IXAudio2Voice** pOutputVoices; +} XAUDIO2_VOICE_SENDS; + +typedef struct XAUDIO2_EFFECT_DESCRIPTOR +{ + IUnknown* pEffect; + BOOL InitialState; + UINT32 OutputChannels; +} XAUDIO2_EFFECT_DESCRIPTOR; + +typedef struct XAUDIO2_EFFECT_CHAIN +{ + UINT32 EffectCount; + const XAUDIO2_EFFECT_DESCRIPTOR* pEffectDescriptors; +} XAUDIO2_EFFECT_CHAIN; + +typedef enum XAUDIO2_FILTER_TYPE +{ + LowPassFilter, + BandPassFilter, + HighPassFilter +} XAUDIO2_FILTER_TYPE; + +typedef struct XAUDIO2_FILTER_PARAMETERS +{ + XAUDIO2_FILTER_TYPE Type; + float Frequency; + float OneOverQ; + +} XAUDIO2_FILTER_PARAMETERS; + +typedef struct XAUDIO2_BUFFER +{ + UINT32 Flags; + UINT32 AudioBytes; + const BYTE* pAudioData; + UINT32 PlayBegin; + UINT32 PlayLength; + UINT32 LoopBegin; + UINT32 LoopLength; + UINT32 LoopCount; + void* pContext; +} XAUDIO2_BUFFER; + +typedef struct XAUDIO2_BUFFER_WMA +{ + const UINT32* pDecodedPacketCumulativeBytes; + UINT32 PacketCount; +} XAUDIO2_BUFFER_WMA; + +typedef struct XAUDIO2_VOICE_STATE +{ + void* pCurrentBufferContext; + UINT32 BuffersQueued; + UINT64 SamplesPlayed; +} XAUDIO2_VOICE_STATE; + +typedef struct XAUDIO2_PERFORMANCE_DATA +{ + UINT64 AudioCyclesSinceLastQuery; + UINT64 TotalCyclesSinceLastQuery; + UINT32 MinimumCyclesPerQuantum; + UINT32 MaximumCyclesPerQuantum; + UINT32 MemoryUsageInBytes; + UINT32 CurrentLatencyInSamples; + UINT32 GlitchesSinceEngineStarted; + UINT32 ActiveSourceVoiceCount; + UINT32 TotalSourceVoiceCount; + UINT32 ActiveSubmixVoiceCount; + UINT32 TotalSubmixVoiceCount; + UINT32 ActiveXmaSourceVoices; + UINT32 ActiveXmaStreams; +} XAUDIO2_PERFORMANCE_DATA; + +typedef struct XAUDIO2_DEBUG_CONFIGURATION +{ + UINT32 TraceMask; + UINT32 BreakMask; + BOOL LogThreadID; + BOOL LogFileline; + BOOL LogFunctionName; + BOOL LogTiming; +} XAUDIO2_DEBUG_CONFIGURATION; + +DECLARE_INTERFACE(IXAudio2EngineCallback) +{ + STDMETHOD_(void, OnProcessingPassStart) (THIS) PURE; + STDMETHOD_(void, OnProcessingPassEnd) (THIS) PURE; + STDMETHOD_(void, OnCriticalError) (THIS_ HRESULT Error) PURE; +}; + +DECLARE_INTERFACE(IXAudio2VoiceCallback) +{ + STDMETHOD_(void, OnVoiceProcessingPassStart) (THIS_ UINT32 BytesRequired) PURE; + STDMETHOD_(void, OnVoiceProcessingPassEnd) (THIS) PURE; + STDMETHOD_(void, OnStreamEnd) (THIS) PURE; + STDMETHOD_(void, OnBufferStart) (THIS_ void* pBufferContext) PURE; + STDMETHOD_(void, OnBufferEnd) (THIS_ void* pBufferContext) PURE; + STDMETHOD_(void, OnLoopEnd) (THIS_ void* pBufferContext) PURE; + STDMETHOD_(void, OnVoiceError) (THIS_ void* pBufferContext, HRESULT Error) PURE; +}; + +DECLARE_INTERFACE(IXAudio2Voice) +{ + #define Declare_IXAudio2Voice_Methods() \ + STDMETHOD_(void, GetVoiceDetails) (THIS_ XAUDIO2_VOICE_DETAILS* pVoiceDetails) PURE; \ + STDMETHOD(SetOutputVoices) (THIS_ const XAUDIO2_VOICE_SENDS* pSendList) PURE; \ + STDMETHOD(SetEffectChain) (THIS_ const XAUDIO2_EFFECT_CHAIN* pEffectChain) PURE; \ + STDMETHOD(EnableEffect) (THIS_ UINT32 EffectIndex, \ + UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; \ + STDMETHOD(DisableEffect) (THIS_ UINT32 EffectIndex, \ + UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; \ + STDMETHOD_(void, GetEffectState) (THIS_ UINT32 EffectIndex, BOOL* pEnabled) PURE; \ + STDMETHOD(SetEffectParameters) (THIS_ UINT32 EffectIndex, \ + const void* pParameters, \ + UINT32 ParametersByteSize, \ + UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; \ + STDMETHOD(GetEffectParameters) (THIS_ UINT32 EffectIndex, void* pParameters, \ + UINT32 ParametersByteSize) PURE; \ + STDMETHOD(SetFilterParameters) (THIS_ const XAUDIO2_FILTER_PARAMETERS* pParameters, \ + UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; \ + STDMETHOD_(void, GetFilterParameters) (THIS_ XAUDIO2_FILTER_PARAMETERS* pParameters) PURE; \ + STDMETHOD(SetVolume) (THIS_ float Volume, \ + UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; \ + STDMETHOD_(void, GetVolume) (THIS_ float* pVolume) PURE; \ + STDMETHOD(SetChannelVolumes) (THIS_ UINT32 Channels, const float* pVolumes, \ + UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; \ + STDMETHOD_(void, GetChannelVolumes) (THIS_ UINT32 Channels, float* pVolumes) PURE; \ + STDMETHOD(SetOutputMatrix) (THIS_ IXAudio2Voice* pDestinationVoice, \ + UINT32 SourceChannels, UINT32 DestinationChannels, \ + const float* pLevelMatrix, \ + UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; \ + STDMETHOD_(void, GetOutputMatrix) (THIS_ IXAudio2Voice* pDestinationVoice, \ + UINT32 SourceChannels, UINT32 DestinationChannels, \ + float* pLevelMatrix) PURE; \ + STDMETHOD_(void, DestroyVoice) (THIS) PURE + + Declare_IXAudio2Voice_Methods(); +}; + + +DECLARE_INTERFACE_(IXAudio2MasteringVoice, IXAudio2Voice) +{ + Declare_IXAudio2Voice_Methods(); +}; + +DECLARE_INTERFACE_(IXAudio2SubmixVoice, IXAudio2Voice) +{ + Declare_IXAudio2Voice_Methods(); +}; + +DECLARE_INTERFACE_(IXAudio2SourceVoice, IXAudio2Voice) +{ + Declare_IXAudio2Voice_Methods(); + STDMETHOD(Start) (THIS_ UINT32 Flags, UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; + STDMETHOD(Stop) (THIS_ UINT32 Flags, UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; + STDMETHOD(SubmitSourceBuffer) (THIS_ const XAUDIO2_BUFFER* pBuffer, const XAUDIO2_BUFFER_WMA* pBufferWMA X2DEFAULT(NULL)) PURE; + STDMETHOD(FlushSourceBuffers) (THIS) PURE; + STDMETHOD(Discontinuity) (THIS) PURE; + STDMETHOD(ExitLoop) (THIS_ UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; + STDMETHOD_(void, GetState) (THIS_ XAUDIO2_VOICE_STATE* pVoiceState) PURE; + STDMETHOD(SetFrequencyRatio) (THIS_ float Ratio, + UINT32 OperationSet X2DEFAULT(XAUDIO2_COMMIT_NOW)) PURE; + STDMETHOD_(void, GetFrequencyRatio) (THIS_ float* pRatio) PURE; +}; + +DECLARE_INTERFACE_(IXAudio2, IUnknown) +{ + STDMETHOD(QueryInterface) (THIS_ REFIID riid, void** ppvInterface) PURE; + STDMETHOD_(ULONG, AddRef) (THIS) PURE; + STDMETHOD_(ULONG, Release) (THIS) PURE; + STDMETHOD(GetDeviceCount) (THIS_ UINT32* pCount) PURE; + STDMETHOD(GetDeviceDetails) (THIS_ UINT32 Index, XAUDIO2_DEVICE_DETAILS* pDeviceDetails) PURE; + STDMETHOD(Initialize) (THIS_ UINT32 Flags X2DEFAULT(0), + XAUDIO2_PROCESSOR XAudio2Processor X2DEFAULT(XAUDIO2_DEFAULT_PROCESSOR)) PURE; + STDMETHOD(RegisterForCallbacks) (IXAudio2EngineCallback* pCallback) PURE; + STDMETHOD_(void, UnregisterForCallbacks) (IXAudio2EngineCallback* pCallback) PURE; + STDMETHOD(CreateSourceVoice) (THIS_ IXAudio2SourceVoice** ppSourceVoice, + const WAVEFORMATEX* pSourceFormat, + UINT32 Flags X2DEFAULT(0), + float MaxFrequencyRatio X2DEFAULT(XAUDIO2_DEFAULT_FREQ_RATIO), + IXAudio2VoiceCallback* pCallback X2DEFAULT(NULL), + const XAUDIO2_VOICE_SENDS* pSendList X2DEFAULT(NULL), + const XAUDIO2_EFFECT_CHAIN* pEffectChain X2DEFAULT(NULL)) PURE; + STDMETHOD(CreateSubmixVoice) (THIS_ IXAudio2SubmixVoice** ppSubmixVoice, + UINT32 InputChannels, UINT32 InputSampleRate, + UINT32 Flags X2DEFAULT(0), UINT32 ProcessingStage X2DEFAULT(0), + const XAUDIO2_VOICE_SENDS* pSendList X2DEFAULT(NULL), + const XAUDIO2_EFFECT_CHAIN* pEffectChain X2DEFAULT(NULL)) PURE; + STDMETHOD(CreateMasteringVoice) (THIS_ IXAudio2MasteringVoice** ppMasteringVoice, + UINT32 InputChannels X2DEFAULT(XAUDIO2_DEFAULT_CHANNELS), + UINT32 InputSampleRate X2DEFAULT(XAUDIO2_DEFAULT_SAMPLERATE), + UINT32 Flags X2DEFAULT(0), UINT32 DeviceIndex X2DEFAULT(0), + const XAUDIO2_EFFECT_CHAIN* pEffectChain X2DEFAULT(NULL)) PURE; + STDMETHOD(StartEngine) (THIS) PURE; + STDMETHOD_(void, StopEngine) (THIS) PURE; + STDMETHOD(CommitChanges) (THIS_ UINT32 OperationSet) PURE; + STDMETHOD_(void, GetPerformanceData) (THIS_ XAUDIO2_PERFORMANCE_DATA* pPerfData) PURE; + STDMETHOD_(void, SetDebugConfiguration) (THIS_ const XAUDIO2_DEBUG_CONFIGURATION* pDebugConfiguration, + void* pReserved X2DEFAULT(NULL)) PURE; +}; + +__inline HRESULT XAudio2Create(IXAudio2** ppXAudio2, UINT32 Flags X2DEFAULT(0), + XAUDIO2_PROCESSOR XAudio2Processor X2DEFAULT(XAUDIO2_DEFAULT_PROCESSOR)) +{ + IXAudio2* pXAudio2; + HRESULT hr = CoCreateInstance((Flags & XAUDIO2_DEBUG_ENGINE) ? CLSID_XAudio2_Debug : CLSID_XAudio2, + NULL, CLSCTX_INPROC_SERVER, IID_IXAudio2, (void**)&pXAudio2); + if (SUCCEEDED(hr)) + { + hr = pXAudio2->Initialize(Flags, XAudio2Processor); + if (SUCCEEDED(hr)) + { + *ppXAudio2 = pXAudio2; + } + else + { + pXAudio2->Release(); + } + } + return hr; +} +#endif diff --git a/input/carbon.cpp b/input/carbon.cpp new file mode 100644 index 0000000..42728fe --- /dev/null +++ b/input/carbon.cpp @@ -0,0 +1,43 @@ +#include "keyboard/carbon.cpp" + +struct InputCarbon : Input { + InputKeyboardCarbon carbonKeyboard; + InputCarbon() : carbonKeyboard(*this) {} + ~InputCarbon() { term(); } + + auto cap(const string& name) -> bool { + if(name == Input::KeyboardSupport) return true; + return false; + } + + auto get(const string& name) -> any { + return {}; + } + + auto set(const string& name, const any& value) -> bool { + return false; + } + + auto acquire() -> bool { return false; } + auto release() -> bool { return false; } + auto acquired() -> bool { return false; } + + auto poll() -> vector> { + vector> devices; + carbonKeyboard.poll(devices); + return devices; + } + + auto rumble(uint64 id, bool enable) -> bool { + return false; + } + + auto init() -> bool { + if(!carbonKeyboard.init()) return false; + return true; + } + + auto term() -> void { + carbonKeyboard.term(); + } +}; diff --git a/input/joypad/directinput.cpp b/input/joypad/directinput.cpp new file mode 100644 index 0000000..e8eeaeb --- /dev/null +++ b/input/joypad/directinput.cpp @@ -0,0 +1,217 @@ +#ifndef RUBY_INPUT_JOYPAD_DIRECTINPUT +#define RUBY_INPUT_JOYPAD_DIRECTINPUT + +auto CALLBACK DirectInput_EnumJoypadsCallback(const DIDEVICEINSTANCE* instance, void* p) -> BOOL; +auto CALLBACK DirectInput_EnumJoypadAxesCallback(const DIDEVICEOBJECTINSTANCE* instance, void* p) -> BOOL; +auto CALLBACK DirectInput_EnumJoypadEffectsCallback(const DIDEVICEOBJECTINSTANCE* instance, void* p) -> BOOL; + +struct InputJoypadDirectInput { + Input& input; + InputJoypadDirectInput(Input& input) : input(input) {} + + struct Joypad { + shared_pointer hid{new HID::Joypad}; + + LPDIRECTINPUTDEVICE8 device = nullptr; + LPDIRECTINPUTEFFECT effect = nullptr; + + uint32_t pathID = 0; + uint16_t vendorID = 0; + uint16_t productID = 0; + bool isXInputDevice = false; + }; + vector joypads; + + uintptr_t handle = 0; + LPDIRECTINPUT8 context = nullptr; + LPDIRECTINPUTDEVICE8 device = nullptr; + bool xinputAvailable = false; + unsigned effects = 0; + + auto assign(shared_pointer hid, unsigned groupID, unsigned inputID, int16_t value) -> void { + auto& group = hid->group(groupID); + if(group.input(inputID).value() == value) return; + input.doChange(hid, groupID, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + for(auto& jp : joypads) { + if(FAILED(jp.device->Poll())) jp.device->Acquire(); + + DIJOYSTATE2 state; + if(FAILED(jp.device->GetDeviceState(sizeof(DIJOYSTATE2), &state))) continue; + + for(unsigned n = 0; n < 4; n++) { + assign(jp.hid, HID::Joypad::GroupID::Axis, 0, state.lX); + assign(jp.hid, HID::Joypad::GroupID::Axis, 1, state.lY); + assign(jp.hid, HID::Joypad::GroupID::Axis, 2, state.lZ); + assign(jp.hid, HID::Joypad::GroupID::Axis, 3, state.lRx); + assign(jp.hid, HID::Joypad::GroupID::Axis, 4, state.lRy); + assign(jp.hid, HID::Joypad::GroupID::Axis, 5, state.lRz); + + unsigned pov = state.rgdwPOV[n]; + int16_t xaxis = 0; + int16_t yaxis = 0; + + if(pov < 36000) { + if(pov >= 31500 || pov <= 4500) yaxis = -32768; + if(pov >= 4500 && pov <= 13500) xaxis = +32767; + if(pov >= 13500 && pov <= 22500) yaxis = +32767; + if(pov >= 22500 && pov <= 31500) xaxis = -32768; + } + + assign(jp.hid, HID::Joypad::GroupID::Hat, n * 2 + 0, xaxis); + assign(jp.hid, HID::Joypad::GroupID::Hat, n * 2 + 1, yaxis); + } + + for(unsigned n = 0; n < 128; n++) { + assign(jp.hid, HID::Joypad::GroupID::Button, n, (bool)state.rgbButtons[n]); + } + + devices.append(jp.hid); + } + } + + auto rumble(uint64_t id, bool enable) -> bool { + for(auto& jp : joypads) { + if(jp.hid->id() != id) continue; + if(jp.effect == nullptr) continue; + + if(enable) jp.effect->Start(1, 0); + else jp.effect->Stop(); + return true; + } + + return false; + } + + auto init(uintptr_t handle, LPDIRECTINPUT8 context, bool xinputAvailable) -> bool { + this->handle = handle; + this->context = context; + this->xinputAvailable = xinputAvailable; + context->EnumDevices(DI8DEVCLASS_GAMECTRL, DirectInput_EnumJoypadsCallback, (void*)this, DIEDFL_ATTACHEDONLY); + return true; + } + + auto term() -> void { + for(auto& jp : joypads) { + jp.device->Unacquire(); + if(jp.effect) jp.effect->Release(); + jp.device->Release(); + } + joypads.reset(); + context = nullptr; + } + + auto initJoypad(const DIDEVICEINSTANCE* instance) -> bool { + Joypad jp; + jp.vendorID = instance->guidProduct.Data1 >> 0; + jp.productID = instance->guidProduct.Data1 >> 16; + jp.isXInputDevice = false; + if(auto device = rawinput.find(jp.vendorID, jp.productID)) { + jp.isXInputDevice = device().isXInputDevice; + } + + //Microsoft has intentionally imposed artificial restrictions on XInput devices when used with DirectInput + //a) the two triggers are merged into a single axis, making uniquely distinguishing them impossible + //b) rumble support is not exposed + //thus, it's always preferred to let the XInput driver handle these joypads + //but if the driver is not available (XInput 1.3 does not ship with stock Windows XP), fall back on DirectInput + if(jp.isXInputDevice && xinputAvailable) return DIENUM_CONTINUE; + + if(FAILED(context->CreateDevice(instance->guidInstance, &device, 0))) return DIENUM_CONTINUE; + jp.device = device; + device->SetDataFormat(&c_dfDIJoystick2); + device->SetCooperativeLevel((HWND)handle, DISCL_NONEXCLUSIVE | DISCL_BACKGROUND); + + effects = 0; + device->EnumObjects(DirectInput_EnumJoypadAxesCallback, (void*)this, DIDFT_ABSAXIS); + device->EnumObjects(DirectInput_EnumJoypadEffectsCallback, (void*)this, DIDFT_FFACTUATOR); + jp.hid->setRumble(effects > 0); + + DIPROPGUIDANDPATH property; + memset(&property, 0, sizeof(DIPROPGUIDANDPATH)); + property.diph.dwSize = sizeof(DIPROPGUIDANDPATH); + property.diph.dwHeaderSize = sizeof(DIPROPHEADER); + property.diph.dwObj = 0; + property.diph.dwHow = DIPH_DEVICE; + device->GetProperty(DIPROP_GUIDANDPATH, &property.diph); + string devicePath = (const char*)utf8_t(property.wszPath); + jp.pathID = Hash::CRC32(devicePath.data(), devicePath.size()).value(); + jp.hid->setID((uint64_t)jp.pathID << 32 | jp.vendorID << 16 | jp.productID << 0); + + if(jp.hid->rumble()) { + //disable auto-centering spring for rumble support + DIPROPDWORD property; + memset(&property, 0, sizeof(DIPROPDWORD)); + property.diph.dwSize = sizeof(DIPROPDWORD); + property.diph.dwHeaderSize = sizeof(DIPROPHEADER); + property.diph.dwObj = 0; + property.diph.dwHow = DIPH_DEVICE; + property.dwData = false; + device->SetProperty(DIPROP_AUTOCENTER, &property.diph); + + DWORD dwAxes[2] = {(DWORD)DIJOFS_X, (DWORD)DIJOFS_Y}; + LONG lDirection[2] = {0, 0}; + DICONSTANTFORCE force; + force.lMagnitude = DI_FFNOMINALMAX; //full force + DIEFFECT effect; + memset(&effect, 0, sizeof(DIEFFECT)); + effect.dwSize = sizeof(DIEFFECT); + effect.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS; + effect.dwDuration = INFINITE; + effect.dwSamplePeriod = 0; + effect.dwGain = DI_FFNOMINALMAX; + effect.dwTriggerButton = DIEB_NOTRIGGER; + effect.dwTriggerRepeatInterval = 0; + effect.cAxes = 2; + effect.rgdwAxes = dwAxes; + effect.rglDirection = lDirection; + effect.lpEnvelope = 0; + effect.cbTypeSpecificParams = sizeof(DICONSTANTFORCE); + effect.lpvTypeSpecificParams = &force; + effect.dwStartDelay = 0; + device->CreateEffect(GUID_ConstantForce, &effect, &jp.effect, NULL); + } + + for(unsigned n = 0; n < 6; n++) jp.hid->axes().append(n); + for(unsigned n = 0; n < 8; n++) jp.hid->hats().append(n); + for(unsigned n = 0; n < 128; n++) jp.hid->buttons().append(n); + joypads.append(jp); + + return DIENUM_CONTINUE; + } + + auto initAxis(const DIDEVICEOBJECTINSTANCE* instance) -> bool { + DIPROPRANGE range; + memset(&range, 0, sizeof(DIPROPRANGE)); + range.diph.dwSize = sizeof(DIPROPRANGE); + range.diph.dwHeaderSize = sizeof(DIPROPHEADER); + range.diph.dwHow = DIPH_BYID; + range.diph.dwObj = instance->dwType; + range.lMin = -32768; + range.lMax = +32767; + device->SetProperty(DIPROP_RANGE, &range.diph); + return DIENUM_CONTINUE; + } + + auto initEffect(const DIDEVICEOBJECTINSTANCE* instance) -> bool { + effects++; + return DIENUM_CONTINUE; + } +}; + +auto CALLBACK DirectInput_EnumJoypadsCallback(const DIDEVICEINSTANCE* instance, void* p) -> BOOL { + return ((InputJoypadDirectInput*)p)->initJoypad(instance); +} + +auto CALLBACK DirectInput_EnumJoypadAxesCallback(const DIDEVICEOBJECTINSTANCE* instance, void* p) -> BOOL { + return ((InputJoypadDirectInput*)p)->initAxis(instance); +} + +auto CALLBACK DirectInput_EnumJoypadEffectsCallback(const DIDEVICEOBJECTINSTANCE* instance, void* p) -> BOOL { + return ((InputJoypadDirectInput*)p)->initEffect(instance); +} + +#endif diff --git a/input/joypad/sdl.cpp b/input/joypad/sdl.cpp new file mode 100644 index 0000000..a41273f --- /dev/null +++ b/input/joypad/sdl.cpp @@ -0,0 +1,79 @@ +#ifndef RUBY_INPUT_JOYPAD_SDL +#define RUBY_INPUT_JOYPAD_SDL + +struct InputJoypadSDL { + Input& input; + InputJoypadSDL(Input& input) : input(input) {} + + struct Joypad { + shared_pointer hid{new HID::Joypad}; + + unsigned id = 0; + SDL_Joystick* handle = nullptr; + }; + vector joypads; + + auto assign(shared_pointer hid, unsigned groupID, unsigned inputID, int16_t value) -> void { + auto& group = hid->group(groupID); + if(group.input(inputID).value() == value) return; + input.doChange(hid, groupID, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + SDL_JoystickUpdate(); + + for(auto& jp : joypads) { + for(auto n : range(jp.hid->axes())) { + assign(jp.hid, HID::Joypad::GroupID::Axis, n, (int16_t)SDL_JoystickGetAxis(jp.handle, n)); + } + + for(signed n = 0; n < (signed)jp.hid->hats().size() - 1; n += 2) { + uint8_t state = SDL_JoystickGetHat(jp.handle, n >> 1); + assign(jp.hid, HID::Joypad::GroupID::Hat, n + 0, state & SDL_HAT_LEFT ? -32768 : state & SDL_HAT_RIGHT ? +32767 : 0); + assign(jp.hid, HID::Joypad::GroupID::Hat, n + 1, state & SDL_HAT_UP ? -32768 : state & SDL_HAT_DOWN ? +32767 : 0); + } + + for(auto n : range(jp.hid->buttons())) { + assign(jp.hid, HID::Joypad::GroupID::Button, n, (bool)SDL_JoystickGetButton(jp.handle, n)); + } + + devices.append(jp.hid); + } + } + + auto init() -> bool { + SDL_InitSubSystem(SDL_INIT_JOYSTICK); + SDL_JoystickEventState(SDL_IGNORE); + + for(auto id : range(SDL_NumJoysticks())) { + Joypad jp; + jp.id = id; + jp.handle = SDL_JoystickOpen(id); + + unsigned axes = SDL_JoystickNumAxes(jp.handle); + unsigned hats = SDL_JoystickNumHats(jp.handle) * 2; + unsigned buttons = 32; //there is no SDL_JoystickNumButtons() + + jp.hid->setID(3 + jp.id); + for(auto n : range(axes)) jp.hid->axes().append(n); + for(auto n : range(hats)) jp.hid->hats().append(n); + for(auto n : range(buttons)) jp.hid->buttons().append(n); + jp.hid->setRumble(false); + + joypads.append(jp); + } + + return true; + } + + auto term() -> void { + for(auto& jp : joypads) { + SDL_JoystickClose(jp.handle); + } + joypads.reset(); + SDL_QuitSubSystem(SDL_INIT_JOYSTICK); + } +}; + +#endif diff --git a/input/joypad/udev.cpp b/input/joypad/udev.cpp new file mode 100644 index 0000000..941175c --- /dev/null +++ b/input/joypad/udev.cpp @@ -0,0 +1,279 @@ +#ifndef RUBY_INPUT_JOYPAD_UDEV +#define RUBY_INPUT_JOYPAD_UDEV + +struct InputJoypadUdev { + Input& input; + InputJoypadUdev(Input& input) : input(input) {} + + udev* context = nullptr; + udev_monitor* monitor = nullptr; + udev_enumerate* enumerator = nullptr; + udev_list_entry* devices = nullptr; + udev_list_entry* item = nullptr; + + struct JoypadInput { + signed code = 0; + unsigned id = 0; + int16_t value = 0; + input_absinfo info; + + JoypadInput() {} + JoypadInput(signed code) : code(code) {} + JoypadInput(signed code, unsigned id) : code(code), id(id) {} + bool operator< (const JoypadInput& source) const { return code < source.code; } + bool operator==(const JoypadInput& source) const { return code == source.code; } + }; + + struct Joypad { + shared_pointer hid{new HID::Joypad}; + + int fd = -1; + dev_t device = 0; + string deviceName; + string deviceNode; + + uint8_t evbit[(EV_MAX + 7) / 8] = {0}; + uint8_t keybit[(KEY_MAX + 7) / 8] = {0}; + uint8_t absbit[(ABS_MAX + 7) / 8] = {0}; + uint8_t ffbit[(FF_MAX + 7) / 8] = {0}; + unsigned effects = 0; + + string name; + string manufacturer; + string product; + string serial; + string vendorID; + string productID; + + set axes; + set hats; + set buttons; + bool rumble = false; + unsigned effectID = 0; + }; + vector joypads; + + auto assign(shared_pointer hid, unsigned groupID, unsigned inputID, int16_t value) -> void { + auto& group = hid->group(groupID); + if(group.input(inputID).value() == value) return; + input.doChange(hid, groupID, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + while(hotplugDevicesAvailable()) hotplugDevice(); + + for(auto& jp : joypads) { + input_event events[32]; + signed length = 0; + while((length = read(jp.fd, events, sizeof(events))) > 0) { + length /= sizeof(input_event); + for(unsigned i = 0; i < length; i++) { + signed code = events[i].code; + signed type = events[i].type; + signed value = events[i].value; + + if(type == EV_ABS) { + if(auto input = jp.axes.find({code})) { + signed range = input().info.maximum - input().info.minimum; + value = (value - input().info.minimum) * 65535ll / range - 32767; + assign(jp.hid, HID::Joypad::GroupID::Axis, input().id, sclamp<16>(value)); + } else if(auto input = jp.hats.find({code})) { + signed range = input().info.maximum - input().info.minimum; + value = (value - input().info.minimum) * 65535ll / range - 32767; + assign(jp.hid, HID::Joypad::GroupID::Hat, input().id, sclamp<16>(value)); + } + } else if(type == EV_KEY) { + if(code >= BTN_MISC) { + if(auto input = jp.buttons.find({code})) { + assign(jp.hid, HID::Joypad::GroupID::Button, input().id, (bool)value); + } + } + } + } + } + + devices.append(jp.hid); + } + } + + auto rumble(uint64_t id, bool enable) -> bool { + for(auto& jp : joypads) { + if(jp.hid->id() != id) continue; + if(!jp.hid->rumble()) continue; + + input_event play; + memset(&play, 0, sizeof(input_event)); + play.type = EV_FF; + play.code = jp.effectID; + play.value = enable; + auto unused = write(jp.fd, &play, sizeof(input_event)); + return true; + } + + return false; + } + + auto init() -> bool { + context = udev_new(); + if(context == nullptr) return false; + + monitor = udev_monitor_new_from_netlink(context, "udev"); + if(monitor) { + udev_monitor_filter_add_match_subsystem_devtype(monitor, "input", nullptr); + udev_monitor_enable_receiving(monitor); + } + + enumerator = udev_enumerate_new(context); + if(enumerator) { + udev_enumerate_add_match_property(enumerator, "ID_INPUT_JOYSTICK", "1"); + udev_enumerate_scan_devices(enumerator); + devices = udev_enumerate_get_list_entry(enumerator); + for(udev_list_entry* item = devices; item != nullptr; item = udev_list_entry_get_next(item)) { + string name = udev_list_entry_get_name(item); + udev_device* device = udev_device_new_from_syspath(context, name); + string deviceNode = udev_device_get_devnode(device); + if(deviceNode) createJoypad(device, deviceNode); + udev_device_unref(device); + } + } + + return true; + } + + auto term() -> void { + if(enumerator) { udev_enumerate_unref(enumerator); enumerator = nullptr; } + } + +private: + auto hotplugDevicesAvailable() -> bool { + pollfd fd = {0}; + fd.fd = udev_monitor_get_fd(monitor); + fd.events = POLLIN; + return (::poll(&fd, 1, 0) == 1) && (fd.revents & POLLIN); + } + + auto hotplugDevice() -> void { + udev_device* device = udev_monitor_receive_device(monitor); + if(device == nullptr) return; + + string value = udev_device_get_property_value(device, "ID_INPUT_JOYSTICK"); + string action = udev_device_get_action(device); + string deviceNode = udev_device_get_devnode(device); + if(value == "1") { + if(action == "add") { + createJoypad(device, deviceNode); + } + if(action == "remove") { + removeJoypad(device, deviceNode); + } + } + } + + auto createJoypad(udev_device* device, const string& deviceNode) -> void { + Joypad jp; + jp.deviceNode = deviceNode; + + struct stat st; + if(stat(deviceNode, &st) < 0) return; + jp.device = st.st_rdev; + + jp.fd = open(deviceNode, O_RDWR | O_NONBLOCK); + if(jp.fd < 0) return; + + uint8_t evbit[(EV_MAX + 7) / 8] = {0}; + uint8_t keybit[(KEY_MAX + 7) / 8] = {0}; + uint8_t absbit[(ABS_MAX + 7) / 8] = {0}; + + ioctl(jp.fd, EVIOCGBIT(0, sizeof(jp.evbit)), jp.evbit); + ioctl(jp.fd, EVIOCGBIT(EV_KEY, sizeof(jp.keybit)), jp.keybit); + ioctl(jp.fd, EVIOCGBIT(EV_ABS, sizeof(jp.absbit)), jp.absbit); + ioctl(jp.fd, EVIOCGBIT(EV_FF, sizeof(jp.ffbit)), jp.ffbit); + ioctl(jp.fd, EVIOCGEFFECTS, &jp.effects); + + #define testBit(buffer, bit) (buffer[(bit) >> 3] & 1 << ((bit) & 7)) + + if(testBit(jp.evbit, EV_KEY)) { + if(udev_device* parent = udev_device_get_parent_with_subsystem_devtype(device, "input", nullptr)) { + jp.name = udev_device_get_sysattr_value(parent, "name"); + jp.vendorID = udev_device_get_sysattr_value(parent, "id/vendor"); + jp.productID = udev_device_get_sysattr_value(parent, "id/product"); + if(udev_device* root = udev_device_get_parent_with_subsystem_devtype(parent, "usb", "usb_device")) { + if(jp.vendorID == udev_device_get_sysattr_value(root, "idVendor") + && jp.productID == udev_device_get_sysattr_value(root, "idProduct") + ) { + jp.deviceName = udev_device_get_devpath(root); + jp.manufacturer = udev_device_get_sysattr_value(root, "manufacturer"); + jp.product = udev_device_get_sysattr_value(root, "product"); + jp.serial = udev_device_get_sysattr_value(root, "serial"); + } + } + } + + unsigned axes = 0; + unsigned hats = 0; + unsigned buttons = 0; + for(signed i = 0; i < ABS_MISC; i++) { + if(testBit(jp.absbit, i)) { + if(i >= ABS_HAT0X && i <= ABS_HAT3Y) { + if(auto hat = jp.hats.insert({i, hats++})) { + ioctl(jp.fd, EVIOCGABS(i), &hat().info); + } + } else { + if(auto axis = jp.axes.insert({i, axes++})) { + ioctl(jp.fd, EVIOCGABS(i), &axis().info); + } + } + } + } + for(signed i = BTN_JOYSTICK; i < KEY_MAX; i++) { + if(testBit(jp.keybit, i)) { + jp.buttons.insert({i, buttons++}); + } + } + for(signed i = BTN_MISC; i < BTN_JOYSTICK; i++) { + if(testBit(jp.keybit, i)) { + jp.buttons.insert({i, buttons++}); + } + } + jp.rumble = jp.effects >= 2 && testBit(jp.ffbit, FF_RUMBLE); + if(jp.rumble) { + ff_effect effect; + memset(&effect, 0, sizeof(ff_effect)); + effect.type = FF_RUMBLE; + effect.id = -1; + effect.u.rumble.strong_magnitude = 65535; + effect.u.rumble.weak_magnitude = 65535; + ioctl(jp.fd, EVIOCSFF, &effect); + jp.effectID = effect.id; + } + + createJoypadHID(jp); + joypads.append(jp); + } + + #undef testBit + } + + auto createJoypadHID(Joypad& jp) -> void { + uint64_t pathID = Hash::CRC32(jp.deviceName.data(), jp.deviceName.size()).value(); + jp.hid->setID(pathID << 32 | hex(jp.vendorID) << 16 | hex(jp.productID) << 0); + + for(unsigned n = 0; n < jp.axes.size(); n++) jp.hid->axes().append(n); + for(unsigned n = 0; n < jp.hats.size(); n++) jp.hid->hats().append(n); + for(unsigned n = 0; n < jp.buttons.size(); n++) jp.hid->buttons().append(n); + jp.hid->setRumble(jp.rumble); + } + + auto removeJoypad(udev_device* device, const string& deviceNode) -> void { + for(unsigned n = 0; n < joypads.size(); n++) { + if(joypads[n].deviceNode == deviceNode) { + close(joypads[n].fd); + joypads.remove(n); + return; + } + } + } +}; + +#endif diff --git a/input/joypad/xinput.cpp b/input/joypad/xinput.cpp new file mode 100644 index 0000000..a5b9bf8 --- /dev/null +++ b/input/joypad/xinput.cpp @@ -0,0 +1,162 @@ +#ifndef RUBY_INPUT_JOYPAD_XINPUT +#define RUBY_INPUT_JOYPAD_XINPUT + +//documented functionality +#define oXInputGetState "XInputGetState" +#define oXInputSetState "XInputSetState" +typedef DWORD WINAPI (*pXInputGetState)(DWORD dwUserIndex, XINPUT_STATE* pState); +typedef DWORD WINAPI (*pXInputSetState)(DWORD dwUserIndex, XINPUT_VIBRATION* pVibration); + +//undocumented functionality +#define oXInputGetStateEx (LPCSTR)100 +#define oXInputWaitForGuideButton (LPCSTR)101 +#define oXInputCancelGuideButtonWait (LPCSTR)102 +#define oXInputPowerOffController (LPCSTR)103 +typedef DWORD WINAPI (*pXInputGetStateEx)(DWORD dwUserIndex, XINPUT_STATE* pState); +typedef DWORD WINAPI (*pXInputWaitForGuideButton)(DWORD dwUserIndex, DWORD dwFlag, void* pUnknown); +typedef DWORD WINAPI (*pXInputCancelGuideButtonWait)(DWORD dwUserIndex); +typedef DWORD WINAPI (*pXInputPowerOffController)(DWORD dwUserIndex); + +#define XINPUT_GAMEPAD_GUIDE 0x0400 + +struct InputJoypadXInput { + Input& input; + InputJoypadXInput(Input& input) : input(input) {} + + HMODULE libxinput = nullptr; + pXInputGetStateEx XInputGetStateEx = nullptr; + pXInputSetState XInputSetState = nullptr; + + struct Joypad { + shared_pointer hid{new HID::Joypad}; + unsigned id = 0; + }; + vector joypads; + + auto assign(shared_pointer hid, unsigned groupID, unsigned inputID, int16_t value) -> void { + auto& group = hid->group(groupID); + if(group.input(inputID).value() == value) return; + input.doChange(hid, groupID, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + for(auto& jp : joypads) { + XINPUT_STATE state; + if(XInputGetStateEx(jp.id, &state) != ERROR_SUCCESS) continue; + + //flip vertical axes so that -32768 = up, +32767 = down + uint16_t axisLY = 32768 + state.Gamepad.sThumbLY; + uint16_t axisRY = 32768 + state.Gamepad.sThumbRY; + assign(jp.hid, HID::Joypad::GroupID::Axis, 0, (int16_t)state.Gamepad.sThumbLX); + assign(jp.hid, HID::Joypad::GroupID::Axis, 1, (int16_t)(~axisLY - 32768)); + assign(jp.hid, HID::Joypad::GroupID::Axis, 2, (int16_t)state.Gamepad.sThumbRX); + assign(jp.hid, HID::Joypad::GroupID::Axis, 3, (int16_t)(~axisRY - 32768)); + + int16_t hatX = 0; + int16_t hatY = 0; + if(state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_UP ) hatY = -32768; + if(state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN ) hatY = +32767; + if(state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_LEFT ) hatX = -32768; + if(state.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) hatX = +32767; + + assign(jp.hid, HID::Joypad::GroupID::Hat, 0, hatX); + assign(jp.hid, HID::Joypad::GroupID::Hat, 1, hatY); + + //scale trigger ranges for up to down from (0 to 255) to (-32768 to +32767) + uint16_t triggerL = state.Gamepad.bLeftTrigger; + uint16_t triggerR = state.Gamepad.bRightTrigger; + triggerL = triggerL << 8 | triggerL << 0; + triggerR = triggerR << 8 | triggerR << 0; + + assign(jp.hid, HID::Joypad::GroupID::Trigger, 0, (int16_t)(triggerL - 32768)); + assign(jp.hid, HID::Joypad::GroupID::Trigger, 1, (int16_t)(triggerR - 32768)); + + assign(jp.hid, HID::Joypad::GroupID::Button, 0, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_A)); + assign(jp.hid, HID::Joypad::GroupID::Button, 1, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_B)); + assign(jp.hid, HID::Joypad::GroupID::Button, 2, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_X)); + assign(jp.hid, HID::Joypad::GroupID::Button, 3, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_Y)); + assign(jp.hid, HID::Joypad::GroupID::Button, 4, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_BACK)); + assign(jp.hid, HID::Joypad::GroupID::Button, 5, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_START)); + assign(jp.hid, HID::Joypad::GroupID::Button, 6, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER)); + assign(jp.hid, HID::Joypad::GroupID::Button, 7, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER)); + assign(jp.hid, HID::Joypad::GroupID::Button, 8, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_THUMB)); + assign(jp.hid, HID::Joypad::GroupID::Button, 9, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB)); + assign(jp.hid, HID::Joypad::GroupID::Button, 10, (bool)(state.Gamepad.wButtons & XINPUT_GAMEPAD_GUIDE)); + + devices.append(jp.hid); + } + } + + auto rumble(uint64_t id, bool enable) -> bool { + for(auto& jp : joypads) { + if(jp.hid->id() != id) continue; + + XINPUT_VIBRATION vibration; + memset(&vibration, 0, sizeof(XINPUT_VIBRATION)); + vibration.wLeftMotorSpeed = enable ? 65535 : 0; //low-frequency motor (0 = off, 65535 = max) + vibration.wRightMotorSpeed = enable ? 65535 : 0; //high-frequency motor (0 = off, 65535 = max) + XInputSetState(jp.id, &vibration); + return true; + } + + return false; + } + + auto init() -> bool { + if(!libxinput) libxinput = LoadLibraryA("xinput1_3.dll"); + if(!libxinput) return false; + + //XInputGetStateEx is an undocumented function; but is required to get the state of the guide button + //if for some reason it is not available, fall back on XInputGetState, which takes the same parameters + XInputGetStateEx = (pXInputGetStateEx)GetProcAddress(libxinput, oXInputGetStateEx); + XInputSetState = (pXInputSetState)GetProcAddress(libxinput, oXInputSetState); + if(!XInputGetStateEx) XInputGetStateEx = (pXInputGetStateEx)GetProcAddress(libxinput, oXInputGetState); + if(!XInputGetStateEx || !XInputSetState) return term(), false; + + //XInput supports a maximum of four controllers + //add all four to devices list now. If they are not connected, they will not show up in poll() results + for(unsigned id = 0; id < 4; id++) { + Joypad jp; + jp.id = id; + jp.hid->setID((uint64_t)(1 + id) << 32 | 0x045e << 16 | 0x028e << 0); //Xbox 360 Player# + VendorID + ProductID + jp.hid->setRumble(true); + + jp.hid->axes().append("LeftThumbX"); + jp.hid->axes().append("LeftThumbY"); + jp.hid->axes().append("RightThumbX"); + jp.hid->axes().append("RightThumbY"); + + jp.hid->hats().append("HatX"); + jp.hid->hats().append("HatY"); + + jp.hid->triggers().append("LeftTrigger"); + jp.hid->triggers().append("RightTrigger"); + + jp.hid->buttons().append("A"); + jp.hid->buttons().append("B"); + jp.hid->buttons().append("X"); + jp.hid->buttons().append("Y"); + jp.hid->buttons().append("Back"); + jp.hid->buttons().append("Start"); + jp.hid->buttons().append("LeftShoulder"); + jp.hid->buttons().append("RightShoulder"); + jp.hid->buttons().append("LeftThumb"); + jp.hid->buttons().append("RightThumb"); + jp.hid->buttons().append("Guide"); + + joypads.append(jp); + } + + return true; + } + + auto term() -> void { + if(!libxinput) return; + + FreeLibrary(libxinput); + libxinput = nullptr; + } +}; + +#endif diff --git a/input/keyboard/carbon.cpp b/input/keyboard/carbon.cpp new file mode 100644 index 0000000..d2beb90 --- /dev/null +++ b/input/keyboard/carbon.cpp @@ -0,0 +1,158 @@ +struct InputKeyboardCarbon { + Input& input; + InputKeyboardCarbon(Input& input) : input(input) {} + + shared_pointer hid{new HID::Keyboard}; + + struct Key { + uint8 id; + string name; + }; + vector keys; + + auto assign(uint inputID, bool value) -> void { + auto& group = hid->buttons(); + if(group.input(inputID).value() == value) return; + input.doChange(hid, HID::Keyboard::GroupID::Button, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + KeyMap keymap; + GetKeys(keymap); + auto buffer = (const uint8*)keymap; + + uint inputID = 0; + for(auto& key : keys) { + bool value = buffer[key.id >> 3] & (1 << (key.id & 7)); + assign(inputID++, value); + } + + devices.append(hid); + } + + auto init() -> bool { + keys.append({0x35, "Escape"}); + keys.append({0x7a, "F1"}); + keys.append({0x78, "F2"}); + keys.append({0x63, "F3"}); + keys.append({0x76, "F4"}); + keys.append({0x60, "F5"}); + keys.append({0x61, "F6"}); + keys.append({0x62, "F7"}); + keys.append({0x64, "F8"}); + keys.append({0x65, "F9"}); + keys.append({0x6d, "F10"}); + keys.append({0x67, "F11"}); + //keys.append({0x??, "F12"}); + + keys.append({0x69, "PrintScreen"}); + //keys.append({0x??, "ScrollLock"}); + keys.append({0x71, "Pause"}); + + keys.append({0x32, "Tilde"}); + keys.append({0x12, "Num1"}); + keys.append({0x13, "Num2"}); + keys.append({0x14, "Num3"}); + keys.append({0x15, "Num4"}); + keys.append({0x17, "Num5"}); + keys.append({0x16, "Num6"}); + keys.append({0x1a, "Num7"}); + keys.append({0x1c, "Num8"}); + keys.append({0x19, "Num9"}); + keys.append({0x1d, "Num0"}); + + keys.append({0x1b, "Dash"}); + keys.append({0x18, "Equal"}); + keys.append({0x33, "Backspace"}); + + keys.append({0x72, "Insert"}); + keys.append({0x75, "Delete"}); + keys.append({0x73, "Home"}); + keys.append({0x77, "End"}); + keys.append({0x74, "PageUp"}); + keys.append({0x79, "PageDown"}); + + keys.append({0x00, "A"}); + keys.append({0x0b, "B"}); + keys.append({0x08, "C"}); + keys.append({0x02, "D"}); + keys.append({0x0e, "E"}); + keys.append({0x03, "F"}); + keys.append({0x05, "G"}); + keys.append({0x04, "H"}); + keys.append({0x22, "I"}); + keys.append({0x26, "J"}); + keys.append({0x28, "K"}); + keys.append({0x25, "L"}); + keys.append({0x2e, "M"}); + keys.append({0x2d, "N"}); + keys.append({0x1f, "O"}); + keys.append({0x23, "P"}); + keys.append({0x0c, "Q"}); + keys.append({0x0f, "R"}); + keys.append({0x01, "S"}); + keys.append({0x11, "T"}); + keys.append({0x20, "U"}); + keys.append({0x09, "V"}); + keys.append({0x0d, "W"}); + keys.append({0x07, "X"}); + keys.append({0x10, "Y"}); + keys.append({0x06, "Z"}); + + keys.append({0x21, "LeftBracket"}); + keys.append({0x1e, "RightBracket"}); + keys.append({0x2a, "Backslash"}); + keys.append({0x29, "Semicolon"}); + keys.append({0x27, "Apostrophe"}); + keys.append({0x2b, "Comma"}); + keys.append({0x2f, "Period"}); + keys.append({0x2c, "Slash"}); + + keys.append({0x53, "Keypad1"}); + keys.append({0x54, "Keypad2"}); + keys.append({0x55, "Keypad3"}); + keys.append({0x56, "Keypad4"}); + keys.append({0x57, "Keypad5"}); + keys.append({0x58, "Keypad6"}); + keys.append({0x59, "Keypad7"}); + keys.append({0x5b, "Keypad8"}); + keys.append({0x5c, "Keypad9"}); + keys.append({0x52, "Keypad0"}); + + //keys.append({0x??, "Point"}); + keys.append({0x45, "Add"}); + keys.append({0x4e, "Subtract"}); + keys.append({0x43, "Multiply"}); + keys.append({0x4b, "Divide"}); + keys.append({0x4c, "Enter"}); + + keys.append({0x47, "NumLock"}); + //keys.append({0x39, "CapsLock"}); + + keys.append({0x7e, "Up"}); + keys.append({0x7d, "Down"}); + keys.append({0x7b, "Left"}); + keys.append({0x7c, "Right"}); + + keys.append({0x30, "Tab"}); + keys.append({0x24, "Return"}); + keys.append({0x31, "Spacebar"}); + //keys.append({0x??, "Menu"}); + + keys.append({0x38, "Shift"}); + keys.append({0x3b, "Control"}); + keys.append({0x3a, "Alt"}); + keys.append({0x37, "Super"}); + + hid->setID(1); + for(auto& key : keys) { + hid->buttons().append(key.name); + } + + return true; + } + + auto term() -> void { + } +}; diff --git a/input/keyboard/quartz.cpp b/input/keyboard/quartz.cpp new file mode 100644 index 0000000..7757fa6 --- /dev/null +++ b/input/keyboard/quartz.cpp @@ -0,0 +1,153 @@ +struct InputKeyboardQuartz { + Input& input; + InputKeyboardQuartz(Input& input) : input(input) {} + + shared_pointer hid{new HID::Keyboard}; + + struct Key { + string name; + uint id; + }; + vector keys; + + auto assign(uint inputID, bool value) -> void { + auto& group = hid->buttons(); + if(group.input(inputID).value() == value) return; + input.doChange(hid, HID::Keyboard::GroupID::Button, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + uint inputID = 0; + for(auto& key : keys) { + bool value = CGEventSourceKeyState(kCGEventSourceStateCombinedSessionState, key.id); + assign(inputID++, value); + } + devices.append(hid); + } + + auto init() -> bool { + keys.append({"Escape", kVK_Escape}); + keys.append({"F1", kVK_F1}); + keys.append({"F2", kVK_F2}); + keys.append({"F3", kVK_F3}); + keys.append({"F4", kVK_F4}); + keys.append({"F5", kVK_F5}); + keys.append({"F6", kVK_F6}); + keys.append({"F7", kVK_F7}); + keys.append({"F8", kVK_F8}); + keys.append({"F9", kVK_F9}); + keys.append({"F10", kVK_F10}); + keys.append({"F11", kVK_F11}); + keys.append({"F12", kVK_F12}); + keys.append({"F13", kVK_F13}); + keys.append({"F14", kVK_F14}); + keys.append({"F15", kVK_F15}); + keys.append({"F16", kVK_F16}); + keys.append({"F17", kVK_F17}); + keys.append({"F18", kVK_F18}); + keys.append({"F19", kVK_F19}); + keys.append({"F20", kVK_F20}); + + keys.append({"Tilde", kVK_ANSI_Grave}); + keys.append({"Num1", kVK_ANSI_1}); + keys.append({"Num2", kVK_ANSI_2}); + keys.append({"Num3", kVK_ANSI_3}); + keys.append({"Num4", kVK_ANSI_4}); + keys.append({"Num5", kVK_ANSI_5}); + keys.append({"Num6", kVK_ANSI_6}); + keys.append({"Num7", kVK_ANSI_7}); + keys.append({"Num8", kVK_ANSI_8}); + keys.append({"Num9", kVK_ANSI_9}); + keys.append({"Num0", kVK_ANSI_0}); + + keys.append({"Dash", kVK_ANSI_Minus}); + keys.append({"Equal", kVK_ANSI_Equal}); + keys.append({"Delete", kVK_Delete}); + + keys.append({"Erase", kVK_ForwardDelete}); + keys.append({"Home", kVK_Home}); + keys.append({"End", kVK_End}); + keys.append({"PageUp", kVK_PageUp}); + keys.append({"PageDown", kVK_PageDown}); + + keys.append({"A", kVK_ANSI_A}); + keys.append({"B", kVK_ANSI_B}); + keys.append({"C", kVK_ANSI_C}); + keys.append({"D", kVK_ANSI_D}); + keys.append({"E", kVK_ANSI_E}); + keys.append({"F", kVK_ANSI_F}); + keys.append({"G", kVK_ANSI_G}); + keys.append({"H", kVK_ANSI_H}); + keys.append({"I", kVK_ANSI_I}); + keys.append({"J", kVK_ANSI_J}); + keys.append({"K", kVK_ANSI_K}); + keys.append({"L", kVK_ANSI_L}); + keys.append({"M", kVK_ANSI_M}); + keys.append({"N", kVK_ANSI_N}); + keys.append({"O", kVK_ANSI_O}); + keys.append({"P", kVK_ANSI_P}); + keys.append({"Q", kVK_ANSI_Q}); + keys.append({"R", kVK_ANSI_R}); + keys.append({"S", kVK_ANSI_S}); + keys.append({"T", kVK_ANSI_T}); + keys.append({"U", kVK_ANSI_U}); + keys.append({"V", kVK_ANSI_V}); + keys.append({"W", kVK_ANSI_W}); + keys.append({"X", kVK_ANSI_X}); + keys.append({"Y", kVK_ANSI_Y}); + keys.append({"Z", kVK_ANSI_Z}); + + keys.append({"LeftBracket", kVK_ANSI_LeftBracket}); + keys.append({"RightBracket", kVK_ANSI_RightBracket}); + keys.append({"Backslash", kVK_ANSI_Backslash}); + keys.append({"Semicolon", kVK_ANSI_Semicolon}); + keys.append({"Apostrophe", kVK_ANSI_Quote}); + keys.append({"Comma", kVK_ANSI_Comma}); + keys.append({"Period", kVK_ANSI_Period}); + keys.append({"Slash", kVK_ANSI_Slash}); + + keys.append({"Keypad1", kVK_ANSI_Keypad1}); + keys.append({"Keypad2", kVK_ANSI_Keypad2}); + keys.append({"Keypad3", kVK_ANSI_Keypad3}); + keys.append({"Keypad4", kVK_ANSI_Keypad4}); + keys.append({"Keypad5", kVK_ANSI_Keypad5}); + keys.append({"Keypad6", kVK_ANSI_Keypad6}); + keys.append({"Keypad7", kVK_ANSI_Keypad7}); + keys.append({"Keypad8", kVK_ANSI_Keypad8}); + keys.append({"Keypad9", kVK_ANSI_Keypad9}); + keys.append({"Keypad0", kVK_ANSI_Keypad0}); + + keys.append({"Clear", kVK_ANSI_KeypadClear}); + keys.append({"Equals", kVK_ANSI_KeypadEquals}); + keys.append({"Divide", kVK_ANSI_KeypadDivide}); + keys.append({"Multiply", kVK_ANSI_KeypadMultiply}); + keys.append({"Subtract", kVK_ANSI_KeypadMinus}); + keys.append({"Add", kVK_ANSI_KeypadPlus}); + keys.append({"Enter", kVK_ANSI_KeypadEnter}); + keys.append({"Decimal", kVK_ANSI_KeypadDecimal}); + + keys.append({"Up", kVK_UpArrow}); + keys.append({"Down", kVK_DownArrow}); + keys.append({"Left", kVK_LeftArrow}); + keys.append({"Right", kVK_RightArrow}); + + keys.append({"Tab", kVK_Tab}); + keys.append({"Return", kVK_Return}); + keys.append({"Spacebar", kVK_Space}); + keys.append({"Shift", kVK_Shift}); + keys.append({"Control", kVK_Control}); + keys.append({"Option", kVK_Option}); + keys.append({"Command", kVK_Command}); + + hid->setID(1); + for(auto& key : keys) { + hid->buttons().append(key.name); + } + + return true; + } + + auto term() -> void { + } +}; diff --git a/input/keyboard/rawinput.cpp b/input/keyboard/rawinput.cpp new file mode 100644 index 0000000..2f77b50 --- /dev/null +++ b/input/keyboard/rawinput.cpp @@ -0,0 +1,177 @@ +#ifndef RUBY_INPUT_KEYBOARD_RAWINPUT +#define RUBY_INPUT_KEYBOARD_RAWINPUT + +struct InputKeyboardRawInput { + Input& input; + InputKeyboardRawInput(Input& input) : input(input) {} + + struct Key { + uint16_t code; + uint16_t flag; + string name; + bool value; + }; + vector keys; + + struct Keyboard { + shared_pointer hid{new HID::Keyboard}; + } kb; + + auto update(RAWINPUT* input) -> void { + unsigned code = input->data.keyboard.MakeCode; + unsigned flag = input->data.keyboard.Flags; + + for(auto& key : keys) { + if(key.code != code) continue; + key.value = (key.flag == flag); + } + } + + auto assign(unsigned inputID, bool value) -> void { + auto& group = kb.hid->buttons(); + if(group.input(inputID).value() == value) return; + input.doChange(kb.hid, HID::Keyboard::GroupID::Button, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + for(unsigned n = 0; n < keys.size(); n++) assign(n, keys[n].value); + devices.append(kb.hid); + } + + auto init() -> bool { + rawinput.updateKeyboard = {&InputKeyboardRawInput::update, this}; + + //Pause sends 0x001d,4 + 0x0045,0; NumLock sends only 0x0045,0 + //pressing Pause will falsely trigger NumLock + //further, pause sends its key release even while button is held down + //because of this, we cannot map either reliably + + keys.append({0x0001, 0, "Escape"}); + keys.append({0x003b, 0, "F1"}); + keys.append({0x003c, 0, "F2"}); + keys.append({0x003d, 0, "F3"}); + keys.append({0x003e, 0, "F4"}); + keys.append({0x003f, 0, "F5"}); + keys.append({0x0040, 0, "F6"}); + keys.append({0x0041, 0, "F7"}); + keys.append({0x0042, 0, "F8"}); + keys.append({0x0043, 0, "F9"}); + keys.append({0x0044, 0, "F10"}); + keys.append({0x0057, 0, "F11"}); + keys.append({0x0058, 0, "F12"}); + + keys.append({0x0037, 2, "PrintScreen"}); + keys.append({0x0046, 0, "ScrollLock"}); + //keys.append({0x001d, 4, "Pause"}); + keys.append({0x0029, 0, "Tilde"}); + + keys.append({0x0002, 0, "Num1"}); + keys.append({0x0003, 0, "Num2"}); + keys.append({0x0004, 0, "Num3"}); + keys.append({0x0005, 0, "Num4"}); + keys.append({0x0006, 0, "Num5"}); + keys.append({0x0007, 0, "Num6"}); + keys.append({0x0008, 0, "Num7"}); + keys.append({0x0009, 0, "Num8"}); + keys.append({0x000a, 0, "Num9"}); + keys.append({0x000b, 0, "Num0"}); + + keys.append({0x000c, 0, "Dash"}); + keys.append({0x000d, 0, "Equal"}); + keys.append({0x000e, 0, "Backspace"}); + + keys.append({0x0052, 2, "Insert"}); + keys.append({0x0053, 2, "Delete"}); + keys.append({0x0047, 2, "Home"}); + keys.append({0x004f, 2, "End"}); + keys.append({0x0049, 2, "PageUp"}); + keys.append({0x0051, 2, "PageDown"}); + + keys.append({0x001e, 0, "A"}); + keys.append({0x0030, 0, "B"}); + keys.append({0x002e, 0, "C"}); + keys.append({0x0020, 0, "D"}); + keys.append({0x0012, 0, "E"}); + keys.append({0x0021, 0, "F"}); + keys.append({0x0022, 0, "G"}); + keys.append({0x0023, 0, "H"}); + keys.append({0x0017, 0, "I"}); + keys.append({0x0024, 0, "J"}); + keys.append({0x0025, 0, "K"}); + keys.append({0x0026, 0, "L"}); + keys.append({0x0032, 0, "M"}); + keys.append({0x0031, 0, "N"}); + keys.append({0x0018, 0, "O"}); + keys.append({0x0019, 0, "P"}); + keys.append({0x0010, 0, "Q"}); + keys.append({0x0013, 0, "R"}); + keys.append({0x001f, 0, "S"}); + keys.append({0x0014, 0, "T"}); + keys.append({0x0016, 0, "U"}); + keys.append({0x002f, 0, "V"}); + keys.append({0x0011, 0, "W"}); + keys.append({0x002d, 0, "X"}); + keys.append({0x0015, 0, "Y"}); + keys.append({0x002c, 0, "Z"}); + + keys.append({0x001a, 0, "LeftBracket"}); + keys.append({0x001b, 0, "RightBracket"}); + keys.append({0x002b, 0, "Backslash"}); + keys.append({0x0027, 0, "Semicolon"}); + keys.append({0x0028, 0, "Apostrophe"}); + keys.append({0x0033, 0, "Comma"}); + keys.append({0x0034, 0, "Period"}); + keys.append({0x0035, 0, "Slash"}); + + keys.append({0x004f, 0, "Keypad1"}); + keys.append({0x0050, 0, "Keypad2"}); + keys.append({0x0051, 0, "Keypad3"}); + keys.append({0x004b, 0, "Keypad4"}); + keys.append({0x004c, 0, "Keypad5"}); + keys.append({0x004d, 0, "Keypad6"}); + keys.append({0x0047, 0, "Keypad7"}); + keys.append({0x0048, 0, "Keypad8"}); + keys.append({0x0049, 0, "Keypad9"}); + keys.append({0x0052, 0, "Keypad0"}); + + keys.append({0x0053, 0, "Point"}); + keys.append({0x001c, 2, "Enter"}); + keys.append({0x004e, 0, "Add"}); + keys.append({0x004a, 0, "Subtract"}); + keys.append({0x0037, 0, "Multiply"}); + keys.append({0x0035, 2, "Divide"}); + + //keys.append({0x0045, 0, "NumLock"}); + keys.append({0x003a, 0, "CapsLock"}); + + keys.append({0x0048, 2, "Up"}); + keys.append({0x0050, 2, "Down"}); + keys.append({0x004b, 2, "Left"}); + keys.append({0x004d, 2, "Right"}); + + keys.append({0x000f, 0, "Tab"}); + keys.append({0x001c, 0, "Return"}); + keys.append({0x0039, 0, "Spacebar"}); + + keys.append({0x002a, 0, "LeftShift"}); + keys.append({0x0036, 0, "RightShift"}); + keys.append({0x001d, 0, "LeftControl"}); + keys.append({0x001d, 2, "RightControl"}); + keys.append({0x0038, 0, "LeftAlt"}); + keys.append({0x0038, 2, "RightAlt"}); + keys.append({0x005b, 2, "LeftSuper"}); + keys.append({0x005c, 2, "RightSuper"}); + keys.append({0x005d, 2, "Menu"}); + + kb.hid->setID(1); + for(auto& key : keys) kb.hid->buttons().append(key.name); + + return true; + } + + void term() { + } +}; + +#endif diff --git a/input/keyboard/xlib.cpp b/input/keyboard/xlib.cpp new file mode 100644 index 0000000..547ed43 --- /dev/null +++ b/input/keyboard/xlib.cpp @@ -0,0 +1,174 @@ +#ifndef RUBY_INPUT_KEYBOARD_XLIB +#define RUBY_INPUT_KEYBOARD_XLIB + +struct InputKeyboardXlib { + Input& input; + InputKeyboardXlib(Input& input) : input(input) {} + + shared_pointer hid{new HID::Keyboard}; + + Display* display = nullptr; + + struct Key { + string name; + uint keysym; + uint keycode; + }; + vector keys; + + auto assign(uint inputID, bool value) -> void { + auto& group = hid->buttons(); + if(group.input(inputID).value() == value) return; + input.doChange(hid, HID::Keyboard::GroupID::Button, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + char state[32]; + XQueryKeymap(display, state); + + uint inputID = 0; + for(auto& key : keys) { + bool value = state[key.keycode >> 3] & (1 << (key.keycode & 7)); + assign(inputID++, value); + } + + devices.append(hid); + } + + auto init() -> bool { + display = XOpenDisplay(0); + + keys.append({"Escape", XK_Escape}); + + keys.append({"F1", XK_F1}); + keys.append({"F2", XK_F2}); + keys.append({"F3", XK_F3}); + keys.append({"F4", XK_F4}); + keys.append({"F5", XK_F5}); + keys.append({"F6", XK_F6}); + keys.append({"F7", XK_F7}); + keys.append({"F8", XK_F8}); + keys.append({"F9", XK_F9}); + keys.append({"F10", XK_F10}); + keys.append({"F11", XK_F11}); + keys.append({"F12", XK_F12}); + + keys.append({"ScrollLock", XK_Scroll_Lock}); + keys.append({"Pause", XK_Pause}); + + keys.append({"Tilde", XK_asciitilde}); + + keys.append({"Num0", XK_0}); + keys.append({"Num1", XK_1}); + keys.append({"Num2", XK_2}); + keys.append({"Num3", XK_3}); + keys.append({"Num4", XK_4}); + keys.append({"Num5", XK_5}); + keys.append({"Num6", XK_6}); + keys.append({"Num7", XK_7}); + keys.append({"Num8", XK_8}); + keys.append({"Num9", XK_9}); + + keys.append({"Dash", XK_minus}); + keys.append({"Equal", XK_equal}); + keys.append({"Backspace", XK_BackSpace}); + + keys.append({"Insert", XK_Insert}); + keys.append({"Delete", XK_Delete}); + keys.append({"Home", XK_Home}); + keys.append({"End", XK_End}); + keys.append({"PageUp", XK_Prior}); + keys.append({"PageDown", XK_Next}); + + keys.append({"A", XK_A}); + keys.append({"B", XK_B}); + keys.append({"C", XK_C}); + keys.append({"D", XK_D}); + keys.append({"E", XK_E}); + keys.append({"F", XK_F}); + keys.append({"G", XK_G}); + keys.append({"H", XK_H}); + keys.append({"I", XK_I}); + keys.append({"J", XK_J}); + keys.append({"K", XK_K}); + keys.append({"L", XK_L}); + keys.append({"M", XK_M}); + keys.append({"N", XK_N}); + keys.append({"O", XK_O}); + keys.append({"P", XK_P}); + keys.append({"Q", XK_Q}); + keys.append({"R", XK_R}); + keys.append({"S", XK_S}); + keys.append({"T", XK_T}); + keys.append({"U", XK_U}); + keys.append({"V", XK_V}); + keys.append({"W", XK_W}); + keys.append({"X", XK_X}); + keys.append({"Y", XK_Y}); + keys.append({"Z", XK_Z}); + + keys.append({"LeftBracket", XK_bracketleft}); + keys.append({"RightBracket", XK_bracketright}); + keys.append({"Backslash", XK_backslash}); + keys.append({"Semicolon", XK_semicolon}); + keys.append({"Apostrophe", XK_apostrophe}); + keys.append({"Comma", XK_comma}); + keys.append({"Period", XK_period}); + keys.append({"Slash", XK_slash}); + + keys.append({"Keypad0", XK_KP_0}); + keys.append({"Keypad1", XK_KP_1}); + keys.append({"Keypad2", XK_KP_2}); + keys.append({"Keypad3", XK_KP_3}); + keys.append({"Keypad4", XK_KP_4}); + keys.append({"Keypad5", XK_KP_5}); + keys.append({"Keypad6", XK_KP_6}); + keys.append({"Keypad7", XK_KP_7}); + keys.append({"Keypad8", XK_KP_8}); + keys.append({"Keypad9", XK_KP_9}); + + keys.append({"Add", XK_KP_Add}); + keys.append({"Subtract", XK_KP_Subtract}); + keys.append({"Multiply", XK_KP_Multiply}); + keys.append({"Divide", XK_KP_Divide}); + keys.append({"Enter", XK_KP_Enter}); + + keys.append({"Up", XK_Up}); + keys.append({"Down", XK_Down}); + keys.append({"Left", XK_Left}); + keys.append({"Right", XK_Right}); + + keys.append({"Tab", XK_Tab}); + keys.append({"Return", XK_Return}); + keys.append({"Spacebar", XK_space}); + + keys.append({"LeftControl", XK_Control_L}); + keys.append({"RightControl", XK_Control_R}); + keys.append({"LeftAlt", XK_Alt_L}); + keys.append({"RightAlt", XK_Alt_R}); + keys.append({"LeftShift", XK_Shift_L}); + keys.append({"RightShift", XK_Shift_R}); + keys.append({"LeftSuper", XK_Super_L}); + keys.append({"RightSuper", XK_Super_R}); + keys.append({"Menu", XK_Menu}); + + hid->setID(1); + + for(auto& key : keys) { + hid->buttons().append(key.name); + key.keycode = XKeysymToKeycode(display, key.keysym); + } + + return true; + } + + auto term() -> void { + if(display) { + XCloseDisplay(display); + display = nullptr; + } + } +}; + +#endif diff --git a/input/mouse/rawinput.cpp b/input/mouse/rawinput.cpp new file mode 100644 index 0000000..bd25c3f --- /dev/null +++ b/input/mouse/rawinput.cpp @@ -0,0 +1,122 @@ +#ifndef RUBY_INPUT_MOUSE_RAWINPUT +#define RUBY_INPUT_MOUSE_RAWINPUT + +struct InputMouseRawInput { + Input& input; + InputMouseRawInput(Input& input) : input(input) {} + + uintptr_t handle = 0; + bool mouseAcquired = false; + + struct Mouse { + shared_pointer hid{new HID::Mouse}; + + signed relativeX = 0; + signed relativeY = 0; + signed relativeZ = 0; + bool buttons[5] = {0}; + } ms; + + auto acquire() -> bool { + if(!mouseAcquired) { + mouseAcquired = true; + ShowCursor(false); + } + return acquired(); + } + + auto release() -> bool { + if(mouseAcquired) { + mouseAcquired = false; + ReleaseCapture(); + ClipCursor(nullptr); + ShowCursor(true); + } + return true; + } + + auto acquired() -> bool { + if(mouseAcquired) { + SetFocus((HWND)handle); + SetCapture((HWND)handle); + RECT rc; + GetWindowRect((HWND)handle, &rc); + ClipCursor(&rc); + } + return GetCapture() == (HWND)handle; + } + + auto update(RAWINPUT* input) -> void { + if((input->data.mouse.usFlags & 1) == MOUSE_MOVE_RELATIVE) { + ms.relativeX += input->data.mouse.lLastX; + ms.relativeY += input->data.mouse.lLastY; + } + + if(input->data.mouse.usButtonFlags & RI_MOUSE_WHEEL) { + ms.relativeZ += (int16_t)input->data.mouse.usButtonData; + } + + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_1_DOWN) ms.buttons[0] = 1; + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_1_UP ) ms.buttons[0] = 0; + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_2_DOWN) ms.buttons[1] = 1; + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_2_UP ) ms.buttons[1] = 0; + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_3_DOWN) ms.buttons[2] = 1; + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_3_UP ) ms.buttons[2] = 0; + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_DOWN) ms.buttons[3] = 1; + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_UP ) ms.buttons[3] = 0; + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_DOWN) ms.buttons[4] = 1; + if(input->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_UP ) ms.buttons[4] = 0; + } + + auto assign(unsigned groupID, unsigned inputID, int16_t value) -> void { + auto& group = ms.hid->group(groupID); + if(group.input(inputID).value() == value) return; + input.doChange(ms.hid, groupID, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + assign(HID::Mouse::GroupID::Axis, 0, ms.relativeX); + assign(HID::Mouse::GroupID::Axis, 1, ms.relativeY); + assign(HID::Mouse::GroupID::Axis, 2, ms.relativeZ); + + //keys are intentionally reordered below: + //in ruby, button order is {left, middle, right, up, down} + assign(HID::Mouse::GroupID::Button, 0, ms.buttons[0]); + assign(HID::Mouse::GroupID::Button, 2, ms.buttons[1]); + assign(HID::Mouse::GroupID::Button, 1, ms.buttons[2]); + assign(HID::Mouse::GroupID::Button, 4, ms.buttons[3]); + assign(HID::Mouse::GroupID::Button, 3, ms.buttons[4]); + + ms.relativeX = 0; + ms.relativeY = 0; + ms.relativeZ = 0; + + devices.append(ms.hid); + } + + auto init(uintptr_t handle) -> bool { + this->handle = handle; + + ms.hid->setID(2); + + ms.hid->axes().append("X"); + ms.hid->axes().append("Y"); + ms.hid->axes().append("Z"); + + ms.hid->buttons().append("Left"); + ms.hid->buttons().append("Middle"); + ms.hid->buttons().append("Right"); + ms.hid->buttons().append("Up"); + ms.hid->buttons().append("Down"); + + rawinput.updateMouse = {&InputMouseRawInput::update, this}; + return true; + } + + auto term() -> void { + release(); + } +}; + +#endif diff --git a/input/mouse/xlib.cpp b/input/mouse/xlib.cpp new file mode 100644 index 0000000..75f151a --- /dev/null +++ b/input/mouse/xlib.cpp @@ -0,0 +1,153 @@ +#ifndef RUBY_INPUT_MOUSE_XLIB +#define RUBY_INPUT_MOUSE_XLIB + +struct InputMouseXlib { + Input& input; + InputMouseXlib(Input& input) : input(input) {} + + shared_pointer hid{new HID::Mouse}; + + uintptr_t handle = 0; + + Display* display = nullptr; + Window rootWindow; + Cursor invisibleCursor; + unsigned screenWidth = 0; + unsigned screenHeight = 0; + + struct Mouse { + bool acquired = false; + signed numerator = 0; + signed denominator = 0; + signed threshold = 0; + unsigned relativeX = 0; + unsigned relativeY = 0; + } ms; + + auto acquire() -> bool { + if(acquired()) return true; + + if(XGrabPointer(display, handle, True, 0, GrabModeAsync, GrabModeAsync, rootWindow, invisibleCursor, CurrentTime) == GrabSuccess) { + //backup existing cursor acceleration settings + XGetPointerControl(display, &ms.numerator, &ms.denominator, &ms.threshold); + + //disable cursor acceleration + XChangePointerControl(display, True, False, 1, 1, 0); + + //center cursor (so that first relative poll returns 0, 0 if mouse has not moved) + XWarpPointer(display, None, rootWindow, 0, 0, 0, 0, screenWidth / 2, screenHeight / 2); + + return ms.acquired = true; + } else { + return ms.acquired = false; + } + } + + auto release() -> bool { + if(acquired()) { + //restore cursor acceleration and release cursor + XChangePointerControl(display, True, True, ms.numerator, ms.denominator, ms.threshold); + XUngrabPointer(display, CurrentTime); + ms.acquired = false; + } + return true; + } + + auto acquired() -> bool { + return ms.acquired; + } + + auto assign(unsigned groupID, unsigned inputID, int16_t value) -> void { + auto& group = hid->group(groupID); + if(group.input(inputID).value() == value) return; + input.doChange(hid, groupID, inputID, group.input(inputID).value(), value); + group.input(inputID).setValue(value); + } + + auto poll(vector>& devices) -> void { + Window rootReturn; + Window childReturn; + signed rootXReturn = 0; + signed rootYReturn = 0; + signed windowXReturn = 0; + signed windowYReturn = 0; + unsigned maskReturn = 0; + XQueryPointer(display, handle, &rootReturn, &childReturn, &rootXReturn, &rootYReturn, &windowXReturn, &windowYReturn, &maskReturn); + + if(acquired()) { + XWindowAttributes attributes; + XGetWindowAttributes(display, handle, &attributes); + + //absolute -> relative conversion + assign(HID::Mouse::GroupID::Axis, 0, (int16_t)(rootXReturn - screenWidth / 2)); + assign(HID::Mouse::GroupID::Axis, 1, (int16_t)(rootYReturn - screenHeight / 2)); + + if(hid->axes().input(0).value() != 0 || hid->axes().input(1).value() != 0) { + //if mouse moved, re-center mouse for next poll + XWarpPointer(display, None, rootWindow, 0, 0, 0, 0, screenWidth / 2, screenHeight / 2); + } + } else { + assign(HID::Mouse::GroupID::Axis, 0, (int16_t)(rootXReturn - ms.relativeX)); + assign(HID::Mouse::GroupID::Axis, 1, (int16_t)(rootYReturn - ms.relativeY)); + + ms.relativeX = rootXReturn; + ms.relativeY = rootYReturn; + } + + assign(HID::Mouse::GroupID::Button, 0, (bool)(maskReturn & Button1Mask)); + assign(HID::Mouse::GroupID::Button, 1, (bool)(maskReturn & Button2Mask)); + assign(HID::Mouse::GroupID::Button, 2, (bool)(maskReturn & Button3Mask)); + assign(HID::Mouse::GroupID::Button, 3, (bool)(maskReturn & Button4Mask)); + assign(HID::Mouse::GroupID::Button, 4, (bool)(maskReturn & Button5Mask)); + + devices.append(hid); + } + + auto init(uintptr_t handle) -> bool { + this->handle = handle; + display = XOpenDisplay(0); + rootWindow = DefaultRootWindow(display); + + XWindowAttributes attributes; + XGetWindowAttributes(display, rootWindow, &attributes); + screenWidth = attributes.width; + screenHeight = attributes.height; + + //Xlib: because XShowCursor(display, false) would just be too easy + //create invisible cursor for use when mouse is acquired + Pixmap pixmap; + XColor black, unused; + static char invisibleData[8] = {0}; + Colormap colormap = DefaultColormap(display, DefaultScreen(display)); + XAllocNamedColor(display, colormap, "black", &black, &unused); + pixmap = XCreateBitmapFromData(display, handle, invisibleData, 8, 8); + invisibleCursor = XCreatePixmapCursor(display, pixmap, pixmap, &black, &black, 0, 0); + XFreePixmap(display, pixmap); + XFreeColors(display, colormap, &black.pixel, 1, 0); + + ms.acquired = false; + ms.relativeX = 0; + ms.relativeY = 0; + + hid->setID(2); + + hid->axes().append("X"); + hid->axes().append("Y"); + + hid->buttons().append("Left"); + hid->buttons().append("Middle"); + hid->buttons().append("Right"); + hid->buttons().append("Up"); + hid->buttons().append("Down"); + + return true; + } + + auto term() -> void { + release(); + XFreeCursor(display, invisibleCursor); + XCloseDisplay(display); + } +}; + +#endif diff --git a/input/quartz.cpp b/input/quartz.cpp new file mode 100644 index 0000000..8232004 --- /dev/null +++ b/input/quartz.cpp @@ -0,0 +1,43 @@ +#include "keyboard/quartz.cpp" + +struct InputQuartz : Input { + InputKeyboardQuartz quartzKeyboard; + InputQuartz() : quartzKeyboard(*this) {} + ~InputQuartz() { term(); } + + auto cap(const string& name) -> bool { + if(name == Input::KeyboardSupport) return true; + return false; + } + + auto get(const string& name) -> any { + return {}; + } + + auto set(const string& name, const any& value) -> bool { + return false; + } + + auto acquire() -> bool { return false; } + auto release() -> bool { return false; } + auto acquired() -> bool { return false; } + + auto poll() -> vector> { + vector> devices; + quartzKeyboard.poll(devices); + return devices; + } + + auto rumble(uint64 id, bool enable) -> bool { + return false; + } + + auto init() -> bool { + if(!quartzKeyboard.init()) return false; + return true; + } + + auto term() -> void { + quartzKeyboard.term(); + } +}; diff --git a/input/sdl.cpp b/input/sdl.cpp new file mode 100644 index 0000000..ea0aba1 --- /dev/null +++ b/input/sdl.cpp @@ -0,0 +1,78 @@ +#include +#include +#include + +#include "keyboard/xlib.cpp" +#include "mouse/xlib.cpp" +#include "joypad/sdl.cpp" + +struct InputSDL : Input { + InputKeyboardXlib xlibKeyboard; + InputMouseXlib xlibMouse; + InputJoypadSDL sdl; + InputSDL() : xlibKeyboard(*this), xlibMouse(*this), sdl(*this) {} + ~InputSDL() { term(); } + + struct Settings { + uintptr_t handle = 0; + } settings; + + auto cap(const string& name) -> bool { + if(name == Input::Handle) return true; + if(name == Input::KeyboardSupport) return true; + if(name == Input::MouseSupport) return true; + if(name == Input::JoypadSupport) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Input::Handle) return (uintptr_t)settings.handle; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Input::Handle && value.is()) { + settings.handle = value.get(); + return true; + } + + return false; + } + + auto acquire() -> bool { + return xlibMouse.acquire(); + } + + auto release() -> bool { + return xlibMouse.release(); + } + + auto acquired() -> bool { + return xlibMouse.acquired(); + } + + auto poll() -> vector> { + vector> devices; + xlibKeyboard.poll(devices); + xlibMouse.poll(devices); + sdl.poll(devices); + return devices; + } + + auto rumble(uint64_t id, bool enable) -> bool { + return false; + } + + auto init() -> bool { + if(!xlibKeyboard.init()) return false; + if(!xlibMouse.init(settings.handle)) return false; + if(!sdl.init()) return false; + return true; + } + + auto term() -> void { + xlibKeyboard.term(); + xlibMouse.term(); + sdl.term(); + } +}; diff --git a/input/shared/rawinput.cpp b/input/shared/rawinput.cpp new file mode 100644 index 0000000..9cc9de1 --- /dev/null +++ b/input/shared/rawinput.cpp @@ -0,0 +1,158 @@ +#ifndef RUBY_INPUT_SHARED_RAWINPUT +#define RUBY_INPUT_SHARED_RAWINPUT + +auto CALLBACK RawInputWindowProc(HWND, UINT, WPARAM, LPARAM) -> LRESULT; + +struct RawInput { + HANDLE mutex = nullptr; + HWND hwnd = nullptr; + bool ready = false; + bool initialized = false; + function updateKeyboard; + function updateMouse; + + struct Device { + HANDLE handle = nullptr; + string path; + enum class Type : unsigned { Keyboard, Mouse, Joypad } type; + uint16_t vendorID = 0; + uint16_t productID = 0; + bool isXInputDevice = false; + }; + vector devices; + + auto find(uint16_t vendorID, uint16_t productID) -> maybe { + for(auto& device : devices) { + if(device.vendorID == vendorID && device.productID == productID) return device; + } + return nothing; + } + + auto scanDevices() -> void { + devices.reset(); + + unsigned deviceCount = 0; + GetRawInputDeviceList(NULL, &deviceCount, sizeof(RAWINPUTDEVICELIST)); + RAWINPUTDEVICELIST* list = new RAWINPUTDEVICELIST[deviceCount]; + GetRawInputDeviceList(list, &deviceCount, sizeof(RAWINPUTDEVICELIST)); + + for(unsigned n = 0; n < deviceCount; n++) { + wchar_t path[4096]; + unsigned size = sizeof(path) - 1; + GetRawInputDeviceInfo(list[n].hDevice, RIDI_DEVICENAME, &path, &size); + + RID_DEVICE_INFO info; + info.cbSize = size = sizeof(RID_DEVICE_INFO); + GetRawInputDeviceInfo(list[n].hDevice, RIDI_DEVICEINFO, &info, &size); + + Device device; + device.path = (const char*)utf8_t(path); + device.handle = list[n].hDevice; + + if(info.dwType == RIM_TYPEKEYBOARD) { + device.type = Device::Type::Keyboard; + device.vendorID = 0; + device.productID = 1; + } + + if(info.dwType == RIM_TYPEMOUSE) { + device.type = Device::Type::Mouse; + device.vendorID = 0; + device.productID = 2; + } + + if(info.dwType == RIM_TYPEHID) { + //verify that this is a joypad device + if(info.hid.usUsagePage != 1 || (info.hid.usUsage != 4 && info.hid.usUsage != 5)) continue; + + device.type = Device::Type::Joypad; + device.vendorID = info.hid.dwVendorId; + device.productID = info.hid.dwProductId; + if(device.path.find("IG_")) device.isXInputDevice = true; //"IG_" is only found inside XInput device paths + } + + devices.append(device); + } + + delete[] list; + } + + auto windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { + if(msg != WM_INPUT) return DefWindowProc(hwnd, msg, wparam, lparam); + + unsigned size = 0; + GetRawInputData((HRAWINPUT)lparam, RID_INPUT, NULL, &size, sizeof(RAWINPUTHEADER)); + RAWINPUT* input = new RAWINPUT[size]; + GetRawInputData((HRAWINPUT)lparam, RID_INPUT, input, &size, sizeof(RAWINPUTHEADER)); + WaitForSingleObject(mutex, INFINITE); + + if(input->header.dwType == RIM_TYPEKEYBOARD) { + if(updateKeyboard) updateKeyboard(input); + } + + if(input->header.dwType == RIM_TYPEMOUSE) { + if(updateMouse) updateMouse(input); + } + + ReleaseMutex(mutex); + LRESULT result = DefRawInputProc(&input, size, sizeof(RAWINPUTHEADER)); + delete[] input; + return result; + } + + auto main() -> void { + WNDCLASS wc; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + wc.hbrBackground = (HBRUSH)COLOR_WINDOW; + wc.hCursor = LoadCursor(0, IDC_ARROW); + wc.hIcon = LoadIcon(0, IDI_APPLICATION); + wc.hInstance = GetModuleHandle(0); + wc.lpfnWndProc = RawInputWindowProc; + wc.lpszClassName = L"RawInputClass"; + wc.lpszMenuName = 0; + wc.style = CS_VREDRAW | CS_HREDRAW; + RegisterClass(&wc); + + hwnd = CreateWindow(L"RawInputClass", L"RawInputClass", WS_POPUP, 0, 0, 64, 64, 0, 0, GetModuleHandle(0), 0); + + scanDevices(); + + RAWINPUTDEVICE device[2]; + //capture all keyboard input + device[0].usUsagePage = 1; + device[0].usUsage = 6; + device[0].dwFlags = RIDEV_INPUTSINK; + device[0].hwndTarget = hwnd; + //capture all mouse input + device[1].usUsagePage = 1; + device[1].usUsage = 2; + device[1].dwFlags = RIDEV_INPUTSINK; + device[1].hwndTarget = hwnd; + RegisterRawInputDevices(device, 2, sizeof(RAWINPUTDEVICE)); + + WaitForSingleObject(mutex, INFINITE); + ready = true; + ReleaseMutex(mutex); + + while(true) { + MSG msg; + GetMessage(&msg, hwnd, 0, 0); + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } +}; + +static RawInput rawinput; + +auto WINAPI RawInputThreadProc(void*) -> DWORD { + rawinput.main(); + return 0; +} + +auto CALLBACK RawInputWindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { + return rawinput.windowProc(hwnd, msg, wparam, lparam); +} + +#endif diff --git a/input/udev.cpp b/input/udev.cpp new file mode 100644 index 0000000..26f54d6 --- /dev/null +++ b/input/udev.cpp @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "keyboard/xlib.cpp" +#include "mouse/xlib.cpp" +#include "joypad/udev.cpp" + +struct InputUdev : Input { + InputKeyboardXlib xlibKeyboard; + InputMouseXlib xlibMouse; + InputJoypadUdev udev; + InputUdev() : xlibKeyboard(*this), xlibMouse(*this), udev(*this) {} + ~InputUdev() { term(); } + + struct Settings { + uintptr_t handle = 0; + } settings; + + auto cap(const string& name) -> bool { + if(name == Input::Handle) return true; + if(name == Input::KeyboardSupport) return true; + if(name == Input::MouseSupport) return true; + if(name == Input::JoypadSupport) return true; + if(name == Input::JoypadRumbleSupport) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Input::Handle) return settings.handle; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Input::Handle && value.is()) { + settings.handle = value.get(); + return true; + } + return false; + } + + auto acquire() -> bool { + return xlibMouse.acquire(); + } + + auto release() -> bool { + return xlibMouse.release(); + } + + auto acquired() -> bool { + return xlibMouse.acquired(); + } + + auto poll() -> vector> { + vector> devices; + xlibKeyboard.poll(devices); + xlibMouse.poll(devices); + udev.poll(devices); + return devices; + } + + auto rumble(uint64_t id, bool enable) -> bool { + return udev.rumble(id, enable); + } + + auto init() -> bool { + if(xlibKeyboard.init() == false) return false; + if(xlibMouse.init(settings.handle) == false) return false; + if(udev.init() == false) return false; + return true; + } + + auto term() -> void { + xlibKeyboard.term(); + xlibMouse.term(); + udev.term(); + } +}; diff --git a/input/windows.cpp b/input/windows.cpp new file mode 100644 index 0000000..9d181f1 --- /dev/null +++ b/input/windows.cpp @@ -0,0 +1,105 @@ +#include +#define DIRECTINPUT_VERSION 0x0800 +#include + +#include "shared/rawinput.cpp" +#include "keyboard/rawinput.cpp" +#include "mouse/rawinput.cpp" +#include "joypad/xinput.cpp" +#include "joypad/directinput.cpp" + +struct InputWindows : Input { + InputKeyboardRawInput rawinputKeyboard; + InputMouseRawInput rawinputMouse; + InputJoypadXInput xinput; + InputJoypadDirectInput directinput; + InputWindows() : rawinputKeyboard(*this), rawinputMouse(*this), xinput(*this), directinput(*this) {} + ~InputWindows() { term(); } + + LPDIRECTINPUT8 directinputContext = nullptr; + + struct Settings { + uintptr_t handle = 0; + } settings; + + auto cap(const string& name) -> bool { + if(name == Input::Handle) return true; + if(name == Input::KeyboardSupport) return true; + if(name == Input::MouseSupport) return true; + if(name == Input::JoypadSupport) return true; + if(name == Input::JoypadRumbleSupport) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Input::Handle) return (uintptr_t)settings.handle; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Input::Handle && value.is()) { + settings.handle = value.get(); + return true; + } + return false; + } + + auto acquire() -> bool { + return rawinputMouse.acquire(); + } + + auto release() -> bool { + return rawinputMouse.release(); + } + + auto acquired() -> bool { + return rawinputMouse.acquired(); + } + + auto poll() -> vector> { + vector> devices; + rawinputKeyboard.poll(devices); + rawinputMouse.poll(devices); + xinput.poll(devices); + directinput.poll(devices); + return devices; + } + + auto rumble(uint64_t id, bool enable) -> bool { + if(xinput.rumble(id, enable)) return true; + if(directinput.rumble(id, enable)) return true; + return false; + } + + auto init() -> bool { + if(rawinput.initialized == false) { + rawinput.initialized = true; + rawinput.mutex = CreateMutex(NULL, FALSE, NULL); + CreateThread(NULL, 0, RawInputThreadProc, 0, 0, NULL); + + do { + Sleep(1); + WaitForSingleObject(rawinput.mutex, INFINITE); + ReleaseMutex(rawinput.mutex); + } while(rawinput.ready == false); + } + + DirectInput8Create(GetModuleHandle(0), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&directinputContext, 0); + if(directinputContext == nullptr) return false; + + if(rawinputKeyboard.init() == false) return false; + if(rawinputMouse.init(settings.handle) == false) return false; + bool xinputAvailable = xinput.init(); + if(directinput.init(settings.handle, directinputContext, xinputAvailable) == false) return false; + return true; + } + + auto term() -> void { + rawinputKeyboard.term(); + rawinputMouse.term(); + xinput.term(); + directinput.term(); + + if(directinputContext) { directinputContext->Release(); directinputContext = nullptr; } + } +}; diff --git a/input/xlib.cpp b/input/xlib.cpp new file mode 100644 index 0000000..b84c24e --- /dev/null +++ b/input/xlib.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include +#include + +#include "keyboard/xlib.cpp" +#include "mouse/xlib.cpp" + +struct InputXlib : Input { + InputKeyboardXlib xlibKeyboard; + InputMouseXlib xlibMouse; + InputXlib() : xlibKeyboard(*this), xlibMouse(*this) {} + ~InputXlib() { term(); } + + struct Settings { + uintptr handle = 0; + } settings; + + auto cap(const string& name) -> bool { + if(name == Input::KeyboardSupport) return true; + if(name == Input::MouseSupport) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Input::Handle) return (uintptr_t)settings.handle; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Input::Handle && value.is()) { + settings.handle = value.get(); + return true; + } + + return false; + } + + auto acquire() -> bool { + return xlibMouse.acquire(); + } + + auto release() -> bool { + return xlibMouse.release(); + } + + auto acquired() -> bool { + return xlibMouse.acquired(); + } + + auto poll() -> vector> { + vector> devices; + xlibKeyboard.poll(devices); + xlibMouse.poll(devices); + return devices; + } + + auto rumble(uint64_t id, bool enable) -> bool { + return false; + } + + auto init() -> bool { + if(!xlibKeyboard.init()) return false; + if(!xlibMouse.init(settings.handle)) return false; + return true; + } + + auto term() -> void { + xlibKeyboard.term(); + xlibMouse.term(); + } +}; diff --git a/ruby.cpp b/ruby.cpp new file mode 100644 index 0000000..6eeaf06 --- /dev/null +++ b/ruby.cpp @@ -0,0 +1,541 @@ +#ifdef _WIN32 + #include + #include +#endif + +#include +using namespace nall; +using namespace ruby; + +/* Shared */ + +#undef deprecated +#undef mkdir +#undef usleep + +#if defined(DISPLAY_XORG) + #include + #include + #include +#elif defined(DISPLAY_QUARTZ) + #define Boolean CocoaBoolean + #define decimal CocoaDecimal + #include + #include + #undef Boolean + #undef decimal +#elif defined(DISPLAY_WINDOWS) + #include +#endif + +/* Video */ + +#if defined(VIDEO_CGL) + #include +#endif + +#if defined(VIDEO_DIRECT3D) + #include +#endif + +#if defined(VIDEO_DIRECTDRAW) + #include +#endif + +#if defined(VIDEO_GDI) + #include +#endif + +#if defined(VIDEO_GLX) + #include +#endif + +#if defined(VIDEO_GLX2) + #include +#endif + +#if defined(VIDEO_SDL) + #include +#endif + +#if defined(VIDEO_WGL) + #include +#endif + +#if defined(VIDEO_XSHM) + #include +#endif + +#if defined(VIDEO_XV) + #include +#endif + +namespace ruby { + +const string Video::Handle = "Handle"; +const string Video::Synchronize = "Synchronize"; +const string Video::Depth = "Depth"; +const string Video::Filter = "Filter"; +const string Video::Shader = "Shader"; + +const unsigned Video::FilterNearest = 0; +const unsigned Video::FilterLinear = 1; + +auto Video::create(const string& driver) -> Video* { + if(!driver) return create(optimalDriver()); + + #if defined(VIDEO_CGL) + if(driver == "OpenGL") return new VideoCGL; + #endif + + #if defined(VIDEO_DIRECT3D) + if(driver == "Direct3D") return new VideoD3D; + #endif + + #if defined(VIDEO_DIRECTDRAW) + if(driver == "DirectDraw") return new VideoDD; + #endif + + #if defined(VIDEO_GDI) + if(driver == "GDI") return new VideoGDI; + #endif + + #if defined(VIDEO_GLX) + if(driver == "OpenGL") return new VideoGLX; + #endif + + #if defined(VIDEO_GLX2) + if(driver == "OpenGL2") return new VideoGLX2; + #endif + + #if defined(VIDEO_SDL) + if(driver == "SDL") return new VideoSDL; + #endif + + #if defined(VIDEO_WGL) + if(driver == "OpenGL") return new VideoWGL; + #endif + + #if defined(VIDEO_XSHM) + if(driver == "XShm") return new VideoXShm; + #endif + + #if defined(VIDEO_XV) + if(driver == "X-Video") return new VideoXv; + #endif + + return new Video; +} + +auto Video::optimalDriver() -> string { + #if defined(VIDEO_WGL) + return "OpenGL"; + #elif defined(VIDEO_DIRECT3D) + return "Direct3D"; + #elif defined(VIDEO_DIRECTDRAW) + return "DirectDraw"; + #elif defined(VIDEO_GDI) + return "GDI"; + #elif defined(VIDEO_CGL) + return "OpenGL"; + #elif defined(VIDEO_GLX) + return "OpenGL"; + #elif defined(VIDEO_GLX2) + return "OpenGL2"; + #elif defined(VIDEO_XV) + return "X-Video"; + #elif defined(VIDEO_XSHM) + return "XShm"; + #elif defined(VIDEO_SDL) + return "SDL"; + #else + return "None"; + #endif +} + +auto Video::safestDriver() -> string { + #if defined(VIDEO_DIRECT3D) + return "Direct3D"; + #elif defined(VIDEO_WGL) + return "OpenGL"; + #elif defined(VIDEO_DIRECTDRAW) + return "DirectDraw"; + #elif defined(VIDEO_GDI) + return "GDI"; + #elif defined(VIDEO_CGL) + return "OpenGL"; + #elif defined(VIDEO_XSHM) + return "XShm"; + #elif defined(VIDEO_SDL) + return "SDL"; + #elif defined(VIDEO_XV) + return "X-Video"; + #elif defined(VIDEO_GLX2) + return "OpenGL2"; + #elif defined(VIDEO_GLX) + return "OpenGL"; + #else + return "None"; + #endif +} + +auto Video::availableDrivers() -> lstring { + return { + + #if defined(VIDEO_WGL) + "OpenGL", + #endif + + #if defined(VIDEO_DIRECT3D) + "Direct3D", + #endif + + #if defined(VIDEO_DIRECTDRAW) + "DirectDraw", + #endif + + #if defined(VIDEO_GDI) + "GDI", + #endif + + #if defined(VIDEO_CGL) + "OpenGL", + #endif + + #if defined(VIDEO_GLX) + "OpenGL", + #endif + + #if defined(VIDEO_GLX2) + "OpenGL2", + #endif + + #if defined(VIDEO_XV) + "X-Video", + #endif + + #if defined(VIDEO_XSHM) + "XShm", + #endif + + #if defined(VIDEO_SDL) + "SDL", + #endif + + "None"}; +} + +} + +/* Audio */ + +#if defined(AUDIO_ALSA) + #include +#endif + +#if defined(AUDIO_AO) + #include +#endif + +#if defined(AUDIO_DIRECTSOUND) + #include +#endif + +#if defined(AUDIO_OPENAL) + #include +#endif + +#if defined(AUDIO_OSS) + #include +#endif + +#if defined(AUDIO_PULSEAUDIO) + #include +#endif + +#if defined(AUDIO_PULSEAUDIOSIMPLE) + #include +#endif + +#if defined(AUDIO_WASAPI) + #include +#endif + +#if defined(AUDIO_XAUDIO2) + #include +#endif + +namespace ruby { + +const string Audio::Device = "Device"; +const string Audio::Exclusive = "Exclusive"; +const string Audio::Handle = "Handle"; +const string Audio::Synchronize = "Synchronize"; +const string Audio::Frequency = "Frequency"; +const string Audio::Latency = "Latency"; + +auto Audio::create(const string& driver) -> Audio* { + if(!driver) return create(optimalDriver()); + + #if defined(AUDIO_ALSA) + if(driver == "ALSA") return new AudioALSA; + #endif + + #if defined(AUDIO_AO) + if(driver == "libao") return new AudioAO; + #endif + + #if defined(AUDIO_DIRECTSOUND) + if(driver == "DirectSound") return new AudioDS; + #endif + + #if defined(AUDIO_OPENAL) + if(driver == "OpenAL") return new AudioOpenAL; + #endif + + #if defined(AUDIO_OSS) + if(driver == "OSS") return new AudioOSS; + #endif + + #if defined(AUDIO_PULSEAUDIO) + if(driver == "PulseAudio") return new AudioPulseAudio; + #endif + + #if defined(AUDIO_PULSEAUDIOSIMPLE) + if(driver == "PulseAudioSimple") return new AudioPulseAudioSimple; + #endif + + #if defined(AUDIO_WASAPI) + if(driver == "WASAPI") return new AudioWASAPI; + #endif + + #if defined(AUDIO_XAUDIO2) + if(driver == "XAudio2") return new AudioXAudio2; + #endif + + return new Audio; +} + +auto Audio::optimalDriver() -> string { + #if defined(AUDIO_WASAPI) + return "WASAPI"; + #elif defined(AUDIO_XAUDIO2) + return "XAudio2"; + #elif defined(AUDIO_DIRECTSOUND) + return "DirectSound"; + #elif defined(AUDIO_ALSA) + return "ALSA"; + #elif defined(AUDIO_OSS) + return "OSS"; + #elif defined(AUDIO_OPENAL) + return "OpenAL"; + #elif defined(AUDIO_PULSEAUDIO) + return "PulseAudio"; + #elif defined(AUDIO_PULSEAUDIOSIMPLE) + return "PulseAudioSimple"; + #elif defined(AUDIO_AO) + return "libao"; + #else + return "None"; + #endif +} + +auto Audio::safestDriver() -> string { + #if defined(AUDIO_DIRECTSOUND) + return "DirectSound"; + #elif defined(AUDIO_WASAPI) + return "WASAPI"; + #elif defined(AUDIO_XAUDIO2) + return "XAudio2"; + #elif defined(AUDIO_ALSA) + return "ALSA"; + #elif defined(AUDIO_OSS) + return "OSS"; + #elif defined(AUDIO_OPENAL) + return "OpenAL"; + #elif defined(AUDIO_PULSEAUDIO) + return "PulseAudio"; + #elif defined(AUDIO_PULSEAUDIOSIMPLE) + return "PulseAudioSimple"; + #elif defined(AUDIO_AO) + return "libao"; + #else + return "None"; + #endif +} + +auto Audio::availableDrivers() -> lstring { + return { + + #if defined(AUDIO_WASAPI) + "WASAPI", + #endif + + #if defined(AUDIO_XAUDIO2) + "XAudio2", + #endif + + #if defined(AUDIO_DIRECTSOUND) + "DirectSound", + #endif + + #if defined(AUDIO_ALSA) + "ALSA", + #endif + + #if defined(AUDIO_OSS) + "OSS", + #endif + + #if defined(AUDIO_OPENAL) + "OpenAL", + #endif + + #if defined(AUDIO_PULSEAUDIO) + "PulseAudio", + #endif + + #if defined(AUDIO_PULSEAUDIOSIMPLE) + "PulseAudioSimple", + #endif + + #if defined(AUDIO_AO) + "libao", + #endif + + "None"}; +} + +} + +/* Input */ + +#if defined(INPUT_CARBON) + #include +#endif + +#if defined(INPUT_QUARTZ) + #include +#endif + +#if defined(INPUT_SDL) + #include +#endif + +#if defined(INPUT_UDEV) + #include +#endif + +#if defined(INPUT_WINDOWS) + #include +#endif + +#if defined(INPUT_XLIB) + #include +#endif + +namespace ruby { + +const string Input::Handle = "Handle"; +const string Input::KeyboardSupport = "KeyboardSupport"; +const string Input::MouseSupport = "MouseSupport"; +const string Input::JoypadSupport = "JoypadSupport"; +const string Input::JoypadRumbleSupport = "JoypadRumbleSupport"; + +auto Input::create(const string& driver) -> Input* { + if(!driver) return create(optimalDriver()); + + #if defined(INPUT_WINDOWS) + if(driver == "Windows") return new InputWindows; + #endif + + #if defined(INPUT_QUARTZ) + if(driver == "Quartz") return new InputQuartz; + #endif + + #if defined(INPUT_CARBON) + if(driver == "Carbon") return new InputCarbon; + #endif + + #if defined(INPUT_UDEV) + if(driver == "udev") return new InputUdev; + #endif + + #if defined(INPUT_SDL) + if(driver == "SDL") return new InputSDL; + #endif + + #if defined(INPUT_XLIB) + if(driver == "Xlib") return new InputXlib; + #endif + + return new Input; +} + +auto Input::optimalDriver() -> string { + #if defined(INPUT_WINDOWS) + return "Windows"; + #elif defined(INPUT_QUARTZ) + return "Quartz"; + #elif defined(INPUT_CARBON) + return "Carbon"; + #elif defined(INPUT_UDEV) + return "udev"; + #elif defined(INPUT_SDL) + return "SDL"; + #elif defined(INPUT_XLIB) + return "Xlib"; + #else + return "None"; + #endif +} + +auto Input::safestDriver() -> string { + #if defined(INPUT_WINDOWS) + return "Windows"; + #elif defined(INPUT_QUARTZ) + return "Quartz"; + #elif defined(INPUT_CARBON) + return "Carbon"; + #elif defined(INPUT_UDEV) + return "udev"; + #elif defined(INPUT_SDL) + return "SDL"; + #elif defined(INPUT_XLIB) + return "Xlib"; + #else + return "none"; + #endif +} + +auto Input::availableDrivers() -> lstring { + return { + + #if defined(INPUT_WINDOWS) + "Windows", + #endif + + #if defined(INPUT_QUARTZ) + "Quartz", + #endif + + #if defined(INPUT_CARBON) + "Carbon", + #endif + + #if defined(INPUT_UDEV) + "udev", + #endif + + #if defined(INPUT_SDL) + "SDL", + #endif + + #if defined(INPUT_XLIB) + "Xlib", + #endif + + "None"}; +} + +} diff --git a/ruby.hpp b/ruby.hpp new file mode 100644 index 0000000..51a5200 --- /dev/null +++ b/ruby.hpp @@ -0,0 +1,108 @@ +#pragma once + +/* ruby + * author: byuu + * license: ISC + * version: 0.14 (2015-11-19) + * + * ruby is a cross-platform hardware abstraction layer + * it provides a common interface to video, audio and input devices + */ + +#include + +namespace ruby { + +struct Video { + static const nall::string Handle; + static const nall::string Synchronize; + static const nall::string Depth; + static const nall::string Filter; + static const nall::string Shader; + + static const unsigned FilterNearest; + static const unsigned FilterLinear; + + static auto create(const nall::string& driver = "") -> Video*; + static auto optimalDriver() -> nall::string; + static auto safestDriver() -> nall::string; + static auto availableDrivers() -> nall::lstring; + + virtual ~Video() = default; + + virtual auto cap(const nall::string& name) -> bool { return false; } + virtual auto get(const nall::string& name) -> nall::any { return false; } + virtual auto set(const nall::string& name, const nall::any& value) -> bool { return false; } + + virtual auto lock(uint32_t*& data, unsigned& pitch, unsigned width, unsigned height) -> bool { return false; } + virtual auto unlock() -> void {} + virtual auto clear() -> void {} + virtual auto refresh() -> void {} + + virtual auto init() -> bool { return true; } + virtual auto term() -> void {} +}; + +struct Audio { + static const nall::string Device; + static const nall::string Exclusive; + static const nall::string Handle; + static const nall::string Synchronize; + static const nall::string Frequency; + static const nall::string Latency; + + static auto create(const nall::string& driver = "") -> Audio*; + static auto optimalDriver() -> nall::string; + static auto safestDriver() -> nall::string; + static auto availableDrivers() -> nall::lstring; + + virtual ~Audio() = default; + + virtual auto cap(const nall::string& name) -> bool { return false; } + virtual auto get(const nall::string& name) -> nall::any { return false; } + virtual auto set(const nall::string& name, const nall::any& value) -> bool { return false; } + + virtual auto sample(uint16_t left, uint16_t right) -> void {} + virtual auto clear() -> void {} + + virtual auto init() -> bool { return true; } + virtual auto term() -> void {} +}; + +struct Input { + static const nall::string Handle; + static const nall::string KeyboardSupport; + static const nall::string MouseSupport; + static const nall::string JoypadSupport; + static const nall::string JoypadRumbleSupport; + + static auto create(const nall::string& driver = "") -> Input*; + static auto optimalDriver() -> nall::string; + static auto safestDriver() -> nall::string; + static auto availableDrivers() -> nall::lstring; + + virtual ~Input() = default; + + virtual auto cap(const nall::string& name) -> bool { return false; } + virtual auto get(const nall::string& name) -> nall::any { return false; } + virtual auto set(const nall::string& name, const nall::any& value) -> bool { return false; } + + virtual auto acquire() -> bool { return false; } + virtual auto release() -> bool { return false; } + virtual auto acquired() -> bool { return false; } + virtual auto poll() -> nall::vector> { return {}; } + virtual auto rumble(uint64_t id, bool enable) -> bool { return false; } + + virtual auto init() -> bool { return true; } + virtual auto term() -> void {} + + auto onChange(const nall::function, unsigned, unsigned, int16_t, int16_t)>& callback) { _onChange = callback; } + auto doChange(nall::shared_pointer device, unsigned group, unsigned input, int16_t oldValue, int16_t newValue) -> void { + if(_onChange) _onChange(device, group, input, oldValue, newValue); + } + +private: + nall::function device, unsigned group, unsigned input, int16_t oldValue, int16_t newValue)> _onChange; +}; + +} diff --git a/video/cgl.cpp b/video/cgl.cpp new file mode 100644 index 0000000..eed6cce --- /dev/null +++ b/video/cgl.cpp @@ -0,0 +1,171 @@ +#define GL_ALPHA_TEST 0x0bc0 +#include "opengl/opengl.hpp" + +struct VideoCGL; + +@interface RubyVideoCGL : NSOpenGLView { +@public + VideoCGL* video; +} +-(id) initWith:(VideoCGL*)video pixelFormat:(NSOpenGLPixelFormat*)pixelFormat; +-(void) reshape; +@end + +struct VideoCGL : Video, OpenGL { + ~VideoCGL() { term(); } + + RubyVideoCGL* view = nullptr; + + struct { + NSView* handle = nullptr; + bool synchronize = false; + uint filter = Video::FilterNearest; + string shader; + } settings; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + if(name == Video::Synchronize) return true; + if(name == Video::Filter) return true; + if(name == Video::Shader) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return (uintptr)settings.handle; + if(name == Video::Synchronize) return settings.synchronize; + if(name == Video::Filter) return settings.filter; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = (NSView*)value.get(); + return true; + } + + if(name == Video::Synchronize && value.is()) { + if(settings.synchronize != value.get()) { + settings.synchronize = value.get(); + + if(view) { + @autoreleasepool { + [[view openGLContext] makeCurrentContext]; + int synchronize = settings.synchronize; + [[view openGLContext] setValues:&synchronize forParameter:NSOpenGLCPSwapInterval]; + } + } + } + return true; + } + + if(name == Video::Filter && value.is()) { + settings.filter = value.get(); + if(!settings.shader) OpenGL::filter = settings.filter ? GL_LINEAR : GL_NEAREST; + return true; + } + + if(name == Video::Shader && value.is()) { + settings.shader = value.get(); + @autoreleasepool { + [[view openGLContext] makeCurrentContext]; + } + OpenGL::shader(settings.shader); + if(!settings.shader) OpenGL::filter = settings.filter ? GL_LINEAR : GL_NEAREST; + return true; + } + + return false; + } + + auto lock(uint32*& data, uint& pitch, uint width, uint height) -> bool { + OpenGL::size(width, height); + return OpenGL::lock(data, pitch); + } + + auto unlock() -> void { + } + + auto clear() -> void { + @autoreleasepool { + [view lockFocus]; + OpenGL::clear(); + [[view openGLContext] flushBuffer]; + [view unlockFocus]; + } + } + + auto refresh() -> void { + @autoreleasepool { + if([view lockFocusIfCanDraw]) { + auto area = [view frame]; + outputWidth = area.size.width; + outputHeight = area.size.height; + OpenGL::refresh(); + [[view openGLContext] flushBuffer]; + [view unlockFocus]; + } + } + } + + auto init() -> bool { + @autoreleasepool { + NSOpenGLPixelFormatAttribute attributes[] = { + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFAAlphaSize, 8, + NSOpenGLPFADoubleBuffer, + 0 + }; + + auto size = [settings.handle frame].size; + auto format = [[[NSOpenGLPixelFormat alloc] initWithAttributes:attributes] autorelease]; + auto context = [[[NSOpenGLContext alloc] initWithFormat:format shareContext:nil] autorelease]; + + view = [[RubyVideoCGL alloc] initWith:this pixelFormat:format]; + [view setOpenGLContext:context]; + [view setFrame:NSMakeRect(0, 0, size.width, size.height)]; + [view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [settings.handle addSubview:view]; + [context setView:view]; + + [view lockFocus]; + + OpenGL::init(); + //print((const char*)glGetString(GL_VERSION), "\n"); + + int synchronize = settings.synchronize; + [[view openGLContext] setValues:&synchronize forParameter:NSOpenGLCPSwapInterval]; + + [view unlockFocus]; + } + + clear(); + return true; + } + + auto term() -> void { + OpenGL::term(); + + @autoreleasepool { + [view removeFromSuperview]; + [view release]; + view = nil; + } + } +}; + +@implementation RubyVideoCGL : NSOpenGLView + +-(id) initWith:(VideoCGL*)videoPointer pixelFormat:(NSOpenGLPixelFormat*)pixelFormat { + if(self = [super initWithFrame:NSMakeRect(0, 0, 0, 0) pixelFormat:pixelFormat]) { + video = videoPointer; + } + return self; +} + +-(void) reshape { + video->refresh(); +} + +@end diff --git a/video/direct3d.cpp b/video/direct3d.cpp new file mode 100644 index 0000000..4567f05 --- /dev/null +++ b/video/direct3d.cpp @@ -0,0 +1,443 @@ +#undef interface +#define interface struct +#include +#include +#undef interface + +#define D3DVERTEX (D3DFVF_XYZRHW | D3DFVF_TEX1) + +typedef HRESULT (__stdcall* EffectProc)(LPDIRECT3DDEVICE9, LPCVOID, UINT, D3DXMACRO const*, LPD3DXINCLUDE, DWORD, LPD3DXEFFECTPOOL, LPD3DXEFFECT*, LPD3DXBUFFER*); +typedef HRESULT (__stdcall* TextureProc)(LPDIRECT3DDEVICE9, LPCTSTR, LPDIRECT3DTEXTURE9*); + +struct VideoD3D : Video { + ~VideoD3D() { term(); } + + LPDIRECT3D9 lpd3d = nullptr; + LPDIRECT3DDEVICE9 device = nullptr; + LPDIRECT3DVERTEXBUFFER9 vertex_buffer = nullptr; + LPDIRECT3DVERTEXBUFFER9* vertex_ptr = nullptr; + D3DPRESENT_PARAMETERS presentation; + D3DSURFACE_DESC d3dsd; + D3DLOCKED_RECT d3dlr; + D3DRASTER_STATUS d3drs; + D3DCAPS9 d3dcaps; + LPDIRECT3DTEXTURE9 texture = nullptr; + LPDIRECT3DSURFACE9 surface = nullptr; + LPD3DXEFFECT effect = nullptr; + string shader_source_markup; + + bool lost = true; + unsigned iwidth; + unsigned iheight; + + struct d3dvertex { + float x, y, z, rhw; //screen coords + float u, v; //texture coords + }; + + struct { + uint32_t t_usage, v_usage; + uint32_t t_pool, v_pool; + uint32_t lock; + uint32_t filter; + } flags; + + struct { + bool dynamic; //device supports dynamic textures + bool shader; //device supports pixel shaders + } caps; + + struct { + HWND handle = nullptr; + bool synchronize = false; + unsigned filter = Video::FilterLinear; + + unsigned width; + unsigned height; + } settings; + + struct { + unsigned width; + unsigned height; + } state; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + if(name == Video::Synchronize) return true; + if(name == Video::Filter) return true; + if(name == Video::Shader) return false; + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return (uintptr_t)settings.handle; + if(name == Video::Synchronize) return settings.synchronize; + if(name == Video::Filter) return settings.filter; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = (HWND)value.get(); + return true; + } + + if(name == Video::Synchronize && value.is()) { + settings.synchronize = value.get(); + return true; + } + + if(name == Video::Filter && value.is()) { + settings.filter = value.get(); + if(lpd3d) update_filter(); + return true; + } + + if(name == Video::Shader && value.is()) { + return false; + //set_shader(value.get()); + //return true; + } + + return false; + } + + auto recover() -> bool { + if(!device) return false; + + if(lost) { + release_resources(); + if(device->Reset(&presentation) != D3D_OK) return false; + } + + lost = false; + + device->SetDialogBoxMode(false); + + device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); + device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); + device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); + + device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); + device->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE); + device->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE); + + device->SetRenderState(D3DRS_LIGHTING, false); + device->SetRenderState(D3DRS_ZENABLE, false); + device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + + device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + device->SetRenderState(D3DRS_ALPHABLENDENABLE, false); + + device->SetVertexShader(NULL); + device->SetFVF(D3DVERTEX); + + device->CreateVertexBuffer(sizeof(d3dvertex) * 4, flags.v_usage, D3DVERTEX, (D3DPOOL)flags.v_pool, &vertex_buffer, NULL); + iwidth = 0; + iheight = 0; + resize(settings.width = 256, settings.height = 256); + update_filter(); + clear(); + return true; + } + + auto rounded_power_of_two(unsigned n) -> unsigned { + n--; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + return n + 1; + } + + auto resize(unsigned width, unsigned height) -> void { + if(iwidth >= width && iheight >= height) return; + + iwidth = rounded_power_of_two(max(width, iwidth )); + iheight = rounded_power_of_two(max(height, iheight)); + + if(d3dcaps.MaxTextureWidth < iwidth || d3dcaps.MaxTextureWidth < iheight) { + //TODO: attempt to handle this more gracefully + return; + } + + if(texture) texture->Release(); + device->CreateTexture(iwidth, iheight, 1, flags.t_usage, D3DFMT_X8R8G8B8, (D3DPOOL)flags.t_pool, &texture, NULL); + } + + auto update_filter() -> void { + if(!device) return; + if(lost && !recover()) return; + + flags.filter = (settings.filter == Video::FilterNearest ? D3DTEXF_POINT : D3DTEXF_LINEAR); + device->SetSamplerState(0, D3DSAMP_MINFILTER, flags.filter); + device->SetSamplerState(0, D3DSAMP_MAGFILTER, flags.filter); + } + + // Vertex format: + // + // 0----------1 + // | /| + // | / | + // | / | + // | / | + // | / | + // 2----------3 + // + // (x,y) screen coords, in pixels + // (u,v) texture coords, betweeen 0.0 (top, left) to 1.0 (bottom, right) + auto set_vertex( + uint32_t px, uint32_t py, uint32_t pw, uint32_t ph, + uint32_t tw, uint32_t th, + uint32_t x, uint32_t y, uint32_t w, uint32_t h + ) -> void { + d3dvertex vertex[4]; + vertex[0].x = vertex[2].x = (double)(x - 0.5); + vertex[1].x = vertex[3].x = (double)(x + w - 0.5); + vertex[0].y = vertex[1].y = (double)(y - 0.5); + vertex[2].y = vertex[3].y = (double)(y + h - 0.5); + + //Z-buffer and RHW are unused for 2D blit, set to normal values + vertex[0].z = vertex[1].z = vertex[2].z = vertex[3].z = 0.0; + vertex[0].rhw = vertex[1].rhw = vertex[2].rhw = vertex[3].rhw = 1.0; + + double rw = (double)w / (double)pw * (double)tw; + double rh = (double)h / (double)ph * (double)th; + vertex[0].u = vertex[2].u = (double)(px ) / rw; + vertex[1].u = vertex[3].u = (double)(px + w) / rw; + vertex[0].v = vertex[1].v = (double)(py ) / rh; + vertex[2].v = vertex[3].v = (double)(py + h) / rh; + + vertex_buffer->Lock(0, sizeof(d3dvertex) * 4, (void**)&vertex_ptr, 0); + memcpy(vertex_ptr, vertex, sizeof(d3dvertex) * 4); + vertex_buffer->Unlock(); + + device->SetStreamSource(0, vertex_buffer, 0, sizeof(d3dvertex)); + } + + auto clear() -> void { + if(lost && !recover()) return; + + texture->GetLevelDesc(0, &d3dsd); + texture->GetSurfaceLevel(0, &surface); + + if(surface) { + device->ColorFill(surface, 0, D3DCOLOR_XRGB(0x00, 0x00, 0x00)); + surface->Release(); + surface = nullptr; + } + + //clear primary display and all backbuffers + for(unsigned i = 0; i < 3; i++) { + device->Clear(0, 0, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0x00, 0x00, 0x00), 1.0f, 0); + device->Present(0, 0, 0, 0); + } + } + + auto lock(uint32_t*& data, unsigned& pitch, unsigned width, unsigned height) -> bool { + if(lost && !recover()) return false; + + if(width != settings.width || height != settings.height) { + resize(settings.width = width, settings.height = height); + } + + texture->GetLevelDesc(0, &d3dsd); + texture->GetSurfaceLevel(0, &surface); + + surface->LockRect(&d3dlr, 0, flags.lock); + pitch = d3dlr.Pitch; + return data = (uint32_t*)d3dlr.pBits; + } + + auto unlock() -> void { + surface->UnlockRect(); + surface->Release(); + surface = nullptr; + } + + auto refresh() -> void { + if(lost && !recover()) return; + + RECT rd, rs; //dest, source rectangles + GetClientRect(settings.handle, &rd); + SetRect(&rs, 0, 0, settings.width, settings.height); + + //if output size changed, driver must be re-initialized. + //failure to do so causes scaling issues on some video drivers. + if(state.width != rd.right || state.height != rd.bottom) { + init(); + set_shader(shader_source_markup); + return; + } + + if(caps.shader && effect) { + device->BeginScene(); + set_vertex(0, 0, settings.width, settings.height, iwidth, iheight, 0, 0, rd.right, rd.bottom); + + D3DXVECTOR4 rubyTextureSize; + rubyTextureSize.x = iwidth; + rubyTextureSize.y = iheight; + rubyTextureSize.z = 1.0 / iheight; + rubyTextureSize.w = 1.0 / iwidth; + effect->SetVector("rubyTextureSize", &rubyTextureSize); + + D3DXVECTOR4 rubyInputSize; + rubyInputSize.x = settings.width; + rubyInputSize.y = settings.height; + rubyInputSize.z = 1.0 / settings.height; + rubyInputSize.w = 1.0 / settings.width; + effect->SetVector("rubyInputSize", &rubyInputSize); + + D3DXVECTOR4 rubyOutputSize; + rubyOutputSize.x = rd.right; + rubyOutputSize.y = rd.bottom; + rubyOutputSize.z = 1.0 / rd.bottom; + rubyOutputSize.w = 1.0 / rd.right; + effect->SetVector("rubyOutputSize", &rubyOutputSize); + + UINT passes; + effect->Begin(&passes, 0); + effect->SetTexture("rubyTexture", texture); + device->SetTexture(0, texture); + for(unsigned pass = 0; pass < passes; pass++) { + effect->BeginPass(pass); + device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); + effect->EndPass(); + } + effect->End(); + device->EndScene(); + } else { + device->BeginScene(); + set_vertex(0, 0, settings.width, settings.height, iwidth, iheight, 0, 0, rd.right, rd.bottom); + device->SetTexture(0, texture); + device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); + device->EndScene(); + } + + if(settings.synchronize) { + D3DRASTER_STATUS status; + //wait for a previous vblank to finish, if necessary + while(true) { + device->GetRasterStatus(0, &status); + if(status.InVBlank == false) break; + } + //wait for next vblank to begin + while(true) { + device->GetRasterStatus(0, &status); + if(status.InVBlank == true) break; + } + } + + if(device->Present(0, 0, 0, 0) == D3DERR_DEVICELOST) lost = true; + } + + auto set_shader(const char* source) -> void { + if(!caps.shader) return; + + if(effect) { + effect->Release(); + effect = NULL; + } + + if(!source || !*source) { + shader_source_markup = ""; + return; + } + shader_source_markup = source; + + auto document = BML::unserialize(shader_source_markup); + bool is_hlsl = document["shader"]["language"].text() == "HLSL"; + string shader_source = document["shader"]["source"].text(); + if(shader_source == "") return; + + HMODULE d3dx; + for(unsigned i = 0; i < 256; i++) { + char t[256]; + sprintf(t, "d3dx9_%u.dll", i); + d3dx = LoadLibraryW(utf16_t(t)); + if(d3dx) break; + } + if(!d3dx) d3dx = LoadLibraryW(L"d3dx9.dll"); + if(!d3dx) return; + + EffectProc effectProc = (EffectProc)GetProcAddress(d3dx, "D3DXCreateEffect"); + TextureProc textureProc = (TextureProc)GetProcAddress(d3dx, "D3DXCreateTextureFromFileA"); + + LPD3DXBUFFER pBufferErrors = NULL; + effectProc(device, (const void*)shader_source.data(), lstrlenA(shader_source), NULL, NULL, 0, NULL, &effect, &pBufferErrors); + + D3DXHANDLE hTech; + effect->FindNextValidTechnique(NULL, &hTech); + effect->SetTechnique(hTech); + } + + auto init() -> bool { + RECT rd; + GetClientRect(settings.handle, &rd); + state.width = rd.right; + state.height = rd.bottom; + + lpd3d = Direct3DCreate9(D3D_SDK_VERSION); + if(!lpd3d) return false; + + memset(&presentation, 0, sizeof(presentation)); + presentation.Flags = D3DPRESENTFLAG_VIDEO; + presentation.SwapEffect = D3DSWAPEFFECT_FLIP; + presentation.hDeviceWindow = settings.handle; + presentation.BackBufferCount = 1; + presentation.MultiSampleType = D3DMULTISAMPLE_NONE; + presentation.MultiSampleQuality = 0; + presentation.EnableAutoDepthStencil = false; + presentation.AutoDepthStencilFormat = D3DFMT_UNKNOWN; + presentation.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; + presentation.Windowed = true; + presentation.BackBufferFormat = D3DFMT_UNKNOWN; + presentation.BackBufferWidth = 0; + presentation.BackBufferHeight = 0; + + if(lpd3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, settings.handle, + D3DCREATE_FPU_PRESERVE | D3DCREATE_SOFTWARE_VERTEXPROCESSING, &presentation, &device) != D3D_OK) { + return false; + } + + device->GetDeviceCaps(&d3dcaps); + + caps.dynamic = bool(d3dcaps.Caps2 & D3DCAPS2_DYNAMICTEXTURES); + caps.shader = d3dcaps.PixelShaderVersion > D3DPS_VERSION(1, 4); + + if(caps.dynamic == true) { + flags.t_usage = D3DUSAGE_DYNAMIC; + flags.v_usage = D3DUSAGE_WRITEONLY | D3DUSAGE_DYNAMIC; + flags.t_pool = D3DPOOL_DEFAULT; + flags.v_pool = D3DPOOL_DEFAULT; + flags.lock = D3DLOCK_NOSYSLOCK | D3DLOCK_DISCARD; + } else { + flags.t_usage = 0; + flags.v_usage = D3DUSAGE_WRITEONLY; + flags.t_pool = D3DPOOL_MANAGED; + flags.v_pool = D3DPOOL_MANAGED; + flags.lock = D3DLOCK_NOSYSLOCK | D3DLOCK_DISCARD; + } + + lost = false; + recover(); + return true; + } + + auto release_resources() -> void { + if(effect) { effect->Release(); effect = 0; } + if(vertex_buffer) { vertex_buffer->Release(); vertex_buffer = 0; } + if(surface) { surface->Release(); surface = 0; } + if(texture) { texture->Release(); texture = 0; } + } + + auto term() -> void { + release_resources(); + if(device) { device->Release(); device = 0; } + if(lpd3d) { lpd3d->Release(); lpd3d = 0; } + } +}; + +#undef D3DVERTEX diff --git a/video/directdraw.cpp b/video/directdraw.cpp new file mode 100644 index 0000000..645153f --- /dev/null +++ b/video/directdraw.cpp @@ -0,0 +1,171 @@ +#include + +struct VideoDD : Video { + ~VideoDD() { term(); } + + LPDIRECTDRAW lpdd = nullptr; + LPDIRECTDRAW7 lpdd7 = nullptr; + LPDIRECTDRAWSURFACE7 screen = nullptr; + LPDIRECTDRAWSURFACE7 raster = nullptr; + LPDIRECTDRAWCLIPPER clipper = nullptr; + DDSURFACEDESC2 ddsd; + DDSCAPS2 ddscaps; + unsigned iwidth; + unsigned iheight; + + struct { + HWND handle = nullptr; + bool synchronize = false; + + unsigned width; + unsigned height; + } settings; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + if(name == Video::Synchronize) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return (uintptr_t)settings.handle; + if(name == Video::Synchronize) return settings.synchronize; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = (HWND)value.get(); + return true; + } + + if(name == Video::Synchronize && value.is()) { + settings.synchronize = value.get(); + return true; + } + + return false; + } + + auto resize(unsigned width, unsigned height) -> void { + if(iwidth >= width && iheight >= height) return; + + iwidth = max(width, iwidth); + iheight = max(height, iheight); + + if(raster) raster->Release(); + + screen->GetSurfaceDesc(&ddsd); + int depth = ddsd.ddpfPixelFormat.dwRGBBitCount; + if(depth == 32) goto try_native_surface; + + memset(&ddsd, 0, sizeof(DDSURFACEDESC2)); + ddsd.dwSize = sizeof(DDSURFACEDESC2); + ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT | DDSD_PIXELFORMAT; + ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_VIDEOMEMORY; //DDSCAPS_SYSTEMMEMORY + ddsd.dwWidth = iwidth; + ddsd.dwHeight = iheight; + + ddsd.ddpfPixelFormat.dwSize = sizeof(DDPIXELFORMAT); + ddsd.ddpfPixelFormat.dwFlags = DDPF_RGB; + ddsd.ddpfPixelFormat.dwRGBBitCount = 32; + ddsd.ddpfPixelFormat.dwRBitMask = 0xff0000; + ddsd.ddpfPixelFormat.dwGBitMask = 0x00ff00; + ddsd.ddpfPixelFormat.dwBBitMask = 0x0000ff; + + if(lpdd7->CreateSurface(&ddsd, &raster, 0) == DD_OK) return clear(); + + try_native_surface: + memset(&ddsd, 0, sizeof(DDSURFACEDESC2)); + ddsd.dwSize = sizeof(DDSURFACEDESC2); + ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT; + ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_VIDEOMEMORY; //DDSCAPS_SYSTEMMEMORY + ddsd.dwWidth = iwidth; + ddsd.dwHeight = iheight; + + if(lpdd7->CreateSurface(&ddsd, &raster, 0) == DD_OK) return clear(); + } + + auto clear() -> void { + DDBLTFX fx; + fx.dwSize = sizeof(DDBLTFX); + fx.dwFillColor = 0x00000000; + screen->Blt(0, 0, 0, DDBLT_WAIT | DDBLT_COLORFILL, &fx); + raster->Blt(0, 0, 0, DDBLT_WAIT | DDBLT_COLORFILL, &fx); + } + + auto lock(uint32_t*& data, unsigned& pitch, unsigned width, unsigned height) -> bool { + if(width != settings.width || height != settings.height) { + resize(settings.width = width, settings.height = height); + } + + if(raster->Lock(0, &ddsd, DDLOCK_WAIT, 0) != DD_OK) { + raster->Restore(); + if(raster->Lock(0, &ddsd, DDLOCK_WAIT, 0) != DD_OK) return false; + } + pitch = ddsd.lPitch; + return data = (uint32_t*)ddsd.lpSurface; + } + + auto unlock() -> void { + raster->Unlock(0); + } + + auto refresh() -> void { + if(settings.synchronize) { + while(true) { + BOOL in_vblank; + lpdd7->GetVerticalBlankStatus(&in_vblank); + if(in_vblank == true) break; + } + } + + HRESULT hr; + RECT rd, rs; + SetRect(&rs, 0, 0, settings.width, settings.height); + + POINT p = {0, 0}; + ClientToScreen(settings.handle, &p); + GetClientRect(settings.handle, &rd); + OffsetRect(&rd, p.x, p.y); + + if(screen->Blt(&rd, raster, &rs, DDBLT_WAIT, 0) == DDERR_SURFACELOST) { + screen->Restore(); + raster->Restore(); + } + } + + auto init() -> bool { + DirectDrawCreate(0, &lpdd, 0); + lpdd->QueryInterface(IID_IDirectDraw7, (void**)&lpdd7); + if(lpdd) { lpdd->Release(); lpdd = 0; } + + lpdd7->SetCooperativeLevel(settings.handle, DDSCL_NORMAL); + + memset(&ddsd, 0, sizeof(DDSURFACEDESC2)); + ddsd.dwSize = sizeof(DDSURFACEDESC2); + + ddsd.dwFlags = DDSD_CAPS; + ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; + lpdd7->CreateSurface(&ddsd, &screen, 0); + + lpdd7->CreateClipper(0, &clipper, 0); + clipper->SetHWnd(0, settings.handle); + screen->SetClipper(clipper); + + raster = 0; + iwidth = 0; + iheight = 0; + resize(settings.width = 256, settings.height = 256); + + return true; + } + + auto term() -> void { + if(clipper) { clipper->Release(); clipper = 0; } + if(raster) { raster->Release(); raster = 0; } + if(screen) { screen->Release(); screen = 0; } + if(lpdd7) { lpdd7->Release(); lpdd7 = 0; } + if(lpdd) { lpdd->Release(); lpdd = 0; } + } +}; diff --git a/video/gdi.cpp b/video/gdi.cpp new file mode 100644 index 0000000..f3afb8d --- /dev/null +++ b/video/gdi.cpp @@ -0,0 +1,89 @@ +#include + +struct VideoGDI : Video { + ~VideoGDI() { term(); } + + uint32_t* buffer = nullptr; + HBITMAP bitmap = nullptr; + HDC bitmapdc = nullptr; + BITMAPINFO bmi; + + struct { + HWND handle = nullptr; + + unsigned width = 0; + unsigned height = 0; + } settings; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return (uintptr_t)settings.handle; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = (HWND)value.get(); + return true; + } + + return false; + } + + auto lock(uint32_t*& data, unsigned& pitch, unsigned width, unsigned height) -> bool { + settings.width = width; + settings.height = height; + + pitch = 1024 * 4; + return data = buffer; + } + + auto unlock() -> void {} + + auto clear() -> void {} + + auto refresh() -> void { + RECT rc; + GetClientRect(settings.handle, &rc); + + SetDIBits(bitmapdc, bitmap, 0, settings.height, (void*)buffer, &bmi, DIB_RGB_COLORS); + HDC hdc = GetDC(settings.handle); + StretchBlt(hdc, rc.left, rc.top, rc.right, rc.bottom, bitmapdc, 0, 1024 - settings.height, settings.width, settings.height, SRCCOPY); + ReleaseDC(settings.handle, hdc); + } + + auto init() -> bool { + buffer = (uint32_t*)memory::allocate(1024 * 1024 * sizeof(uint32_t)); + + HDC hdc = GetDC(settings.handle); + bitmapdc = CreateCompatibleDC(hdc); + assert(bitmapdc); + bitmap = CreateCompatibleBitmap(hdc, 1024, 1024); + assert(bitmap); + SelectObject(bitmapdc, bitmap); + ReleaseDC(settings.handle, hdc); + + memset(&bmi, 0, sizeof(BITMAPINFO)); + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = 1024; + bmi.bmiHeader.biHeight = -1024; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; //biBitCount of 15 is invalid, biBitCount of 16 is really RGB555 + bmi.bmiHeader.biCompression = BI_RGB; + bmi.bmiHeader.biSizeImage = 1024 * 1024 * sizeof(uint32_t); + + settings.width = 256; + settings.height = 256; + return true; + } + + auto term() -> void { + DeleteObject(bitmap); + DeleteDC(bitmapdc); + if(buffer) { memory::free(buffer); buffer = nullptr; } + } +}; diff --git a/video/glx.cpp b/video/glx.cpp new file mode 100644 index 0000000..81530c1 --- /dev/null +++ b/video/glx.cpp @@ -0,0 +1,250 @@ +#include "opengl/opengl.hpp" + +#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091 +#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092 + +auto VideoGLX_X11ErrorHandler(Display*, XErrorEvent*) -> int { + return 0; //suppress errors +} + +struct VideoGLX : Video, OpenGL { + ~VideoGLX() { term(); } + + auto (*glXSwapInterval)(signed) -> signed = nullptr; + + Display* display = nullptr; + signed screen = 0; + Window xwindow = 0; + Colormap colormap = 0; + GLXContext glxcontext = nullptr; + GLXWindow glxwindow = 0; + + struct { + signed versionMajor = 0; + signed versionMinor = 0; + bool doubleBuffer = false; + bool isDirect = false; + } glx; + + struct { + Window handle = 0; + bool synchronize = false; + unsigned depth = 24; + unsigned filter = 1; //linear + string shader; + } settings; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + if(name == Video::Synchronize) return true; + if(name == Video::Depth) return true; + if(name == Video::Filter) return true; + if(name == Video::Shader) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return (uintptr_t)settings.handle; + if(name == Video::Synchronize) return settings.synchronize; + if(name == Video::Depth) return settings.depth; + if(name == Video::Filter) return settings.filter; + if(name == Video::Shader) return settings.shader; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = value.get(); + return true; + } + + if(name == Video::Synchronize && value.is()) { + if(settings.synchronize != value.get()) { + settings.synchronize = value.get(); + if(glXSwapInterval) glXSwapInterval(settings.synchronize); + return true; + } + } + + if(name == Video::Depth && value.is()) { + unsigned depth = value.get(); + if(depth > DefaultDepth(display, screen)) return false; + + switch(depth) { + case 24: inputFormat = GL_RGBA8; break; + case 30: inputFormat = GL_RGB10_A2; break; + default: return false; + } + + settings.depth = depth; + return true; + } + + if(name == Video::Filter && value.is()) { + settings.filter = value.get(); + if(settings.shader.empty()) OpenGL::filter = settings.filter ? GL_LINEAR : GL_NEAREST; + return true; + } + + if(name == Video::Shader && value.is()) { + settings.shader = value.get(); + OpenGL::shader(settings.shader); + if(settings.shader.empty()) OpenGL::filter = settings.filter ? GL_LINEAR : GL_NEAREST; + return true; + } + + return false; + } + + auto lock(uint32_t*& data, unsigned& pitch, unsigned width, unsigned height) -> bool { + OpenGL::size(width, height); + return OpenGL::lock(data, pitch); + } + + auto unlock() -> void { + } + + auto clear() -> void { + OpenGL::clear(); + if(glx.doubleBuffer) glXSwapBuffers(display, glxwindow); + } + + auto refresh() -> void { + //we must ensure that the child window is the same size as the parent window. + //unfortunately, we cannot hook the parent window resize event notification, + //as we did not create the parent window, nor have any knowledge of the toolkit used. + //therefore, inelegant as it may be, we query each window size and resize as needed. + XWindowAttributes parent, child; + XGetWindowAttributes(display, settings.handle, &parent); + XGetWindowAttributes(display, xwindow, &child); + if(child.width != parent.width || child.height != parent.height) { + XResizeWindow(display, xwindow, parent.width, parent.height); + } + + outputWidth = parent.width, outputHeight = parent.height; + OpenGL::refresh(); + if(glx.doubleBuffer) glXSwapBuffers(display, glxwindow); + } + + auto init() -> bool { + display = XOpenDisplay(0); + screen = DefaultScreen(display); + + //require GLX 1.2+ API + glXQueryVersion(display, &glx.versionMajor, &glx.versionMinor); + if(glx.versionMajor < 1 || (glx.versionMajor == 1 && glx.versionMinor < 2)) return false; + + XWindowAttributes windowAttributes; + XGetWindowAttributes(display, settings.handle, &windowAttributes); + + //let GLX determine the best Visual to use for GL output; provide a few hints + //note: some video drivers will override double buffering attribute + signed attributeList[] = { + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DOUBLEBUFFER, True, + GLX_RED_SIZE, (signed)(settings.depth / 3), + GLX_GREEN_SIZE, (signed)(settings.depth / 3) + (signed)(settings.depth % 3), + GLX_BLUE_SIZE, (signed)(settings.depth / 3), + None + }; + + signed fbCount = 0; + GLXFBConfig* fbConfig = glXChooseFBConfig(display, screen, attributeList, &fbCount); + if(fbCount == 0) return false; + + XVisualInfo* vi = glXGetVisualFromFBConfig(display, fbConfig[0]); + + //Window settings.handle has already been realized, most likely with DefaultVisual. + //GLX requires that the GL output window has the same Visual as the GLX context. + //it is not possible to change the Visual of an already realized (created) window. + //therefore a new child window, using the same GLX Visual, must be created and binded to settings.handle. + colormap = XCreateColormap(display, RootWindow(display, vi->screen), vi->visual, AllocNone); + XSetWindowAttributes attributes; + attributes.colormap = colormap; + attributes.border_pixel = 0; + xwindow = XCreateWindow(display, /* parent = */ settings.handle, + /* x = */ 0, /* y = */ 0, windowAttributes.width, windowAttributes.height, + /* border_width = */ 0, vi->depth, InputOutput, vi->visual, + CWColormap | CWBorderPixel, &attributes); + XSetWindowBackground(display, xwindow, /* color = */ 0); + XMapWindow(display, xwindow); + XFlush(display); + + //window must be realized (appear onscreen) before we make the context current + while(XPending(display)) { + XEvent event; + XNextEvent(display, &event); + } + + glxcontext = glXCreateContext(display, vi, /* sharelist = */ 0, /* direct = */ GL_TRUE); + glXMakeCurrent(display, glxwindow = xwindow, glxcontext); + + //glXSwapInterval is used to toggle Vsync + //note that the ordering is very important! MESA declares SGI, but the SGI function does nothing + glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalEXT"); + if(!glXSwapInterval) glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalMESA"); + if(!glXSwapInterval) glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalSGI"); + + if(auto glXCreateContextAttribs = (auto (*)(Display*, GLXFBConfig, GLXContext, signed, const signed*) -> GLXContext)glGetProcAddress("glXCreateContextAttribsARB")) { + signed attributes[] = { + GLX_CONTEXT_MAJOR_VERSION_ARB, 3, + GLX_CONTEXT_MINOR_VERSION_ARB, 2, + None + }; + + //glXCreateContextAttribs tends to throw BadRequest errors instead of simply failing gracefully + auto originalHandler = XSetErrorHandler(VideoGLX_X11ErrorHandler); + auto context = glXCreateContextAttribs(display, fbConfig[0], nullptr, true, attributes); + XSync(display, False); + XSetErrorHandler(originalHandler); + + if(context) { + glXMakeCurrent(display, 0, nullptr); + glXDestroyContext(display, glxcontext); + glXMakeCurrent(display, glxwindow, glxcontext = context); + } else { + //OpenGL 3.2+ not supported (most likely OpenGL 2.x) + return false; + } + } else { + //missing required glXCreateContextAtribs function + return false; + } + + if(glXSwapInterval) glXSwapInterval(settings.synchronize); + + //read attributes of frame buffer for later use, as requested attributes from above are not always granted + signed value = 0; + glXGetConfig(display, vi, GLX_DOUBLEBUFFER, &value); + glx.doubleBuffer = value; + glx.isDirect = glXIsDirect(display, glxcontext); + + OpenGL::init(); + return true; + } + + auto term() -> void { + OpenGL::term(); + + if(glxcontext) { + glXDestroyContext(display, glxcontext); + glxcontext = nullptr; + } + + if(xwindow) { + XUnmapWindow(display, xwindow); + xwindow = 0; + } + + if(colormap) { + XFreeColormap(display, colormap); + colormap = 0; + } + + if(display) { + XCloseDisplay(display); + display = nullptr; + } + } +}; diff --git a/video/glx2.cpp b/video/glx2.cpp new file mode 100644 index 0000000..71b6c18 --- /dev/null +++ b/video/glx2.cpp @@ -0,0 +1,245 @@ +//Xorg/GLX OpenGL 2.0 driver + +//note: this is a fallback driver for use when OpenGL 3.2 is not available. +//see glx.cpp for comments on how this driver operates (they are very similar.) + +struct VideoGLX2 : Video { + ~VideoGLX2() { term(); } + + auto (*glXSwapInterval)(signed) -> signed = nullptr; + Display* display = nullptr; + signed screen = 0; + Window xwindow = 0; + Colormap colormap = 0; + GLXContext glxcontext = nullptr; + GLXWindow glxwindow = 0; + + struct { + Window handle = 0; + bool synchronize = false; + unsigned filter = 1; //linear + + unsigned width = 256; + unsigned height = 256; + + bool isDoubleBuffered = false; + bool isDirect = false; + } settings; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + if(name == Video::Synchronize) return true; + if(name == Video::Filter) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return (uintptr_t)settings.handle; + if(name == Video::Synchronize) return settings.synchronize; + if(name == Video::Filter) return settings.filter; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = value.get(); + return true; + } + + if(name == Video::Synchronize && value.is()) { + if(settings.synchronize != value.get()) { + settings.synchronize = value.get(); + if(glXSwapInterval) glXSwapInterval(settings.synchronize); + return true; + } + } + + if(name == Video::Filter && value.is()) { + settings.filter = value.get(); + return true; + } + + return false; + } + + auto lock(uint32_t*& data, unsigned& pitch, unsigned width, unsigned height) -> bool { + if(width != settings.width || height != settings.height) resize(width, height); + pitch = glwidth * sizeof(uint32_t); + return data = glbuffer; + } + + auto unlock() -> void { + } + + auto clear() -> void { + memory::fill(glbuffer, glwidth * glheight * sizeof(uint32_t)); + glClearColor(0.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT); + glFlush(); + if(settings.isDoubleBuffered) glXSwapBuffers(display, glxwindow); + } + + auto refresh() -> void { + XWindowAttributes parent, child; + XGetWindowAttributes(display, settings.handle, &parent); + XGetWindowAttributes(display, xwindow, &child); + if(child.width != parent.width || child.height != parent.height) { + XResizeWindow(display, xwindow, parent.width, parent.height); + } + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, settings.filter ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, settings.filter ? GL_LINEAR : GL_NEAREST); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0, parent.width, 0, parent.height, -1.0, 1.0); + glViewport(0, 0, parent.width, parent.height); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glPixelStorei(GL_UNPACK_ROW_LENGTH, glwidth); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, settings.width, settings.height, + GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, glbuffer); + + double w = (double)settings.width / (double)glwidth; + double h = (double)settings.height / (double)glheight; + signed u = parent.width; + signed v = parent.height; + + glBegin(GL_TRIANGLE_STRIP); + glTexCoord2f(0, 0); glVertex3i(0, v, 0); + glTexCoord2f(w, 0); glVertex3i(u, v, 0); + glTexCoord2f(0, h); glVertex3i(0, 0, 0); + glTexCoord2f(w, h); glVertex3i(u, 0, 0); + glEnd(); + glFlush(); + + if(settings.isDoubleBuffered) glXSwapBuffers(display, glxwindow); + } + + auto init() -> bool { + display = XOpenDisplay(0); + screen = DefaultScreen(display); + + signed versionMajor = 0, versionMinor = 0; + glXQueryVersion(display, &versionMajor, &versionMinor); + if(versionMajor < 1 || (versionMajor == 1 && versionMinor < 2)) return false; + + XWindowAttributes windowAttributes; + XGetWindowAttributes(display, settings.handle, &windowAttributes); + + signed attributeList[] = { + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DOUBLEBUFFER, True, + GLX_RED_SIZE, 8, + GLX_GREEN_SIZE, 8, + GLX_BLUE_SIZE, 8, + None + }; + + signed fbCount = 0; + auto fbConfig = glXChooseFBConfig(display, screen, attributeList, &fbCount); + if(fbCount == 0) return false; + + auto vi = glXGetVisualFromFBConfig(display, fbConfig[0]); + colormap = XCreateColormap(display, RootWindow(display, vi->screen), vi->visual, AllocNone); + XSetWindowAttributes attributes; + attributes.colormap = colormap; + attributes.border_pixel = 0; + xwindow = XCreateWindow(display, settings.handle, 0, 0, windowAttributes.width, windowAttributes.height, + 0, vi->depth, InputOutput, vi->visual, CWColormap | CWBorderPixel, &attributes); + XSetWindowBackground(display, xwindow, 0); + XMapWindow(display, xwindow); + XFlush(display); + + while(XPending(display)) { + XEvent event; + XNextEvent(display, &event); + } + + glxcontext = glXCreateContext(display, vi, 0, GL_TRUE); + glXMakeCurrent(display, glxwindow = xwindow, glxcontext); + + glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalEXT"); + if(!glXSwapInterval) glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalMESA"); + if(!glXSwapInterval) glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalSGI"); + + if(glXSwapInterval) glXSwapInterval(settings.synchronize); + + signed value = 0; + glXGetConfig(display, vi, GLX_DOUBLEBUFFER, &value); + settings.isDoubleBuffered = value; + settings.isDirect = glXIsDirect(display, glxcontext); + + glDisable(GL_ALPHA_TEST); + glDisable(GL_BLEND); + glDisable(GL_DEPTH_TEST); + glDisable(GL_POLYGON_SMOOTH); + glDisable(GL_STENCIL_TEST); + + glEnable(GL_DITHER); + glEnable(GL_TEXTURE_2D); + + resize(256, 256); + return true; + } + + auto term() -> void { + if(gltexture) { + glDeleteTextures(1, &gltexture); + gltexture = 0; + } + + if(glbuffer) { + delete[] glbuffer; + glbuffer = 0; + } + + glwidth = 0; + glheight = 0; + + if(glxcontext) { + glXDestroyContext(display, glxcontext); + glxcontext = nullptr; + } + + if(xwindow) { + XUnmapWindow(display, xwindow); + xwindow = 0; + } + + if(colormap) { + XFreeColormap(display, colormap); + colormap = 0; + } + + if(display) { + XCloseDisplay(display); + display = nullptr; + } + } + +private: + GLuint gltexture = 0; + uint32_t* glbuffer = nullptr; + unsigned glwidth = 0; + unsigned glheight = 0; + + auto resize(unsigned width, unsigned height) -> void { + settings.width = width; + settings.height = height; + + if(gltexture == 0) glGenTextures(1, &gltexture); + glwidth = max(glwidth, width); + glheight = max(glheight, height); + if(glbuffer) delete[] glbuffer; + glbuffer = new uint32_t[glwidth * glheight](); + + glBindTexture(GL_TEXTURE_2D, gltexture); + glPixelStorei(GL_UNPACK_ROW_LENGTH, glwidth); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, glwidth, glheight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, glbuffer); + } +}; diff --git a/video/opengl/bind.hpp b/video/opengl/bind.hpp new file mode 100644 index 0000000..e65bf0d --- /dev/null +++ b/video/opengl/bind.hpp @@ -0,0 +1,101 @@ +#if defined(DISPLAY_WINDOWS) || defined(DISPLAY_XORG) +PFNGLCREATEPROGRAMPROC glCreateProgram = nullptr; +PFNGLDELETEPROGRAMPROC glDeleteProgram = nullptr; +PFNGLUSEPROGRAMPROC glUseProgram = nullptr; +PFNGLCREATESHADERPROC glCreateShader = nullptr; +PFNGLDELETESHADERPROC glDeleteShader = nullptr; +PFNGLSHADERSOURCEPROC glShaderSource = nullptr; +PFNGLCOMPILESHADERPROC glCompileShader = nullptr; +PFNGLGETSHADERIVPROC glGetShaderiv = nullptr; +PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog = nullptr; +PFNGLATTACHSHADERPROC glAttachShader = nullptr; +PFNGLDETACHSHADERPROC glDetachShader = nullptr; +PFNGLLINKPROGRAMPROC glLinkProgram = nullptr; +PFNGLVALIDATEPROGRAMPROC glValidateProgram = nullptr; +PFNGLGETPROGRAMIVPROC glGetProgramiv = nullptr; +PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog = nullptr; +PFNGLGENVERTEXARRAYSPROC glGenVertexArrays = nullptr; +PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays = nullptr; +PFNGLBINDVERTEXARRAYPROC glBindVertexArray = nullptr; +PFNGLGENBUFFERSPROC glGenBuffers = nullptr; +PFNGLDELETEBUFFERSPROC glDeleteBuffers = nullptr; +PFNGLBINDBUFFERPROC glBindBuffer = nullptr; +PFNGLBUFFERDATAPROC glBufferData = nullptr; +PFNGLGETATTRIBLOCATIONPROC glGetAttribLocation = nullptr; +PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer = nullptr; +PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray = nullptr; +PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray = nullptr; +PFNGLBINDFRAGDATALOCATIONPROC glBindFragDataLocation = nullptr; +PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation = nullptr; +PFNGLGETUNIFORMIVPROC glGetUniformiv = nullptr; +PFNGLUNIFORM1IPROC glUniform1i = nullptr; +PFNGLUNIFORM1FPROC glUniform1f = nullptr; +PFNGLUNIFORM2FPROC glUniform2f = nullptr; +PFNGLUNIFORM2FVPROC glUniform2fv = nullptr; +PFNGLUNIFORM4FPROC glUniform4f = nullptr; +PFNGLUNIFORM4FVPROC glUniform4fv = nullptr; +PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv = nullptr; +PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers = nullptr; +PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers = nullptr; +PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer = nullptr; +PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D = nullptr; +#endif +#if defined(DISPLAY_WINDOWS) +PFNGLACTIVETEXTUREPROC glActiveTexture = nullptr; +#endif + +static bool OpenGLBind() { + #define bind(prototype, function) \ + function = (prototype)glGetProcAddress(#function); \ + if(function == nullptr) return false + + #if defined(DISPLAY_WINDOWS) || defined(DISPLAY_XORG) + bind(PFNGLCREATEPROGRAMPROC, glCreateProgram); + bind(PFNGLDELETEPROGRAMPROC, glDeleteProgram); + bind(PFNGLUSEPROGRAMPROC, glUseProgram); + bind(PFNGLCREATESHADERPROC, glCreateShader); + bind(PFNGLDELETESHADERPROC, glDeleteShader); + bind(PFNGLSHADERSOURCEPROC, glShaderSource); + bind(PFNGLCOMPILESHADERPROC, glCompileShader); + bind(PFNGLGETSHADERIVPROC, glGetShaderiv); + bind(PFNGLGETSHADERINFOLOGPROC, glGetShaderInfoLog); + bind(PFNGLATTACHSHADERPROC, glAttachShader); + bind(PFNGLDETACHSHADERPROC, glDetachShader); + bind(PFNGLLINKPROGRAMPROC, glLinkProgram); + bind(PFNGLVALIDATEPROGRAMPROC, glValidateProgram); + bind(PFNGLGETPROGRAMIVPROC, glGetProgramiv); + bind(PFNGLGETPROGRAMINFOLOGPROC, glGetProgramInfoLog); + bind(PFNGLGENVERTEXARRAYSPROC, glGenVertexArrays); + bind(PFNGLDELETEVERTEXARRAYSPROC, glDeleteVertexArrays); + bind(PFNGLBINDVERTEXARRAYPROC, glBindVertexArray); + bind(PFNGLGENBUFFERSPROC, glGenBuffers); + bind(PFNGLDELETEBUFFERSPROC, glDeleteBuffers); + bind(PFNGLBINDBUFFERPROC, glBindBuffer); + bind(PFNGLBUFFERDATAPROC, glBufferData); + bind(PFNGLGETATTRIBLOCATIONPROC, glGetAttribLocation); + bind(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer); + bind(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray); + bind(PFNGLDISABLEVERTEXATTRIBARRAYPROC, glDisableVertexAttribArray); + bind(PFNGLBINDFRAGDATALOCATIONPROC, glBindFragDataLocation); + bind(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation); + bind(PFNGLGETUNIFORMIVPROC, glGetUniformiv); + bind(PFNGLUNIFORM1IPROC, glUniform1i); + bind(PFNGLUNIFORM1FPROC, glUniform1f); + bind(PFNGLUNIFORM2FPROC, glUniform2f); + bind(PFNGLUNIFORM2FVPROC, glUniform2fv); + bind(PFNGLUNIFORM4FPROC, glUniform4f); + bind(PFNGLUNIFORM4FVPROC, glUniform4fv); + bind(PFNGLUNIFORMMATRIX4FVPROC, glUniformMatrix4fv); + bind(PFNGLGENFRAMEBUFFERSPROC, glGenFramebuffers); + bind(PFNGLDELETEFRAMEBUFFERSPROC, glDeleteFramebuffers); + bind(PFNGLBINDFRAMEBUFFERPROC, glBindFramebuffer); + bind(PFNGLFRAMEBUFFERTEXTURE2DPROC, glFramebufferTexture2D); + #endif + #if defined(DISPLAY_WINDOWS) + bind(PFNGLACTIVETEXTUREPROC, glActiveTexture); + #endif + + #undef bind + + return true; +} diff --git a/video/opengl/main.hpp b/video/opengl/main.hpp new file mode 100644 index 0000000..3c4f6d1 --- /dev/null +++ b/video/opengl/main.hpp @@ -0,0 +1,212 @@ +auto OpenGL::shader(const string& pathname) -> void { + for(auto& program : programs) program.release(); + programs.reset(); + + settings.reset(); + + format = inputFormat; + filter = GL_LINEAR; + wrap = GL_CLAMP_TO_BORDER; + absoluteWidth = 0, absoluteHeight = 0; + relativeWidth = 0, relativeHeight = 0; + + unsigned historySize = 0; + if(pathname) { + auto document = BML::unserialize(file::read({pathname, "manifest.bml"})); + + for(auto node : document["settings"]) { + settings.insert({node.name(), node.text()}); + } + + for(auto node : document["input"]) { + if(node.name() == "history") historySize = node.natural(); + if(node.name() == "format") format = glrFormat(node.text()); + if(node.name() == "filter") filter = glrFilter(node.text()); + if(node.name() == "wrap") wrap = glrWrap(node.text()); + } + + for(auto node : document["output"]) { + string text = node.text(); + if(node.name() == "width") { + if(text.endsWith("%")) relativeWidth = real(text.rtrim("%", 1L)) / 100.0; + else absoluteWidth = text.natural(); + } + if(node.name() == "height") { + if(text.endsWith("%")) relativeHeight = real(text.rtrim("%", 1L)) / 100.0; + else absoluteHeight = text.natural(); + } + } + + for(auto node : document.find("program")) { + unsigned n = programs.size(); + programs(n).bind(this, node, pathname); + } + } + + //changing shaders may change input format, which requires the input texture to be recreated + if(texture) { glDeleteTextures(1, &texture); texture = 0; } + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, getFormat(), getType(), buffer); + allocateHistory(historySize); +} + +auto OpenGL::allocateHistory(unsigned size) -> void { + for(auto& frame : history) glDeleteTextures(1, &frame.texture); + history.reset(); + while(size--) { + OpenGLTexture frame; + frame.filter = filter; + frame.wrap = wrap; + glGenTextures(1, &frame.texture); + glBindTexture(GL_TEXTURE_2D, frame.texture); + glTexImage2D(GL_TEXTURE_2D, 0, format, frame.width = width, frame.height = height, 0, getFormat(), getType(), buffer); + history.append(frame); + } +} + +auto OpenGL::lock(uint32_t*& data, unsigned& pitch) -> bool { + pitch = width * sizeof(uint32_t); + return data = buffer; +} + +auto OpenGL::clear() -> void { + for(auto& p : programs) { + glUseProgram(p.program); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, p.framebuffer); + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + } + glUseProgram(0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); +} + +auto OpenGL::refresh() -> void { + clear(); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, getFormat(), getType(), buffer); + + struct Source { + GLuint texture; + unsigned width, height; + GLuint filter, wrap; + }; + vector sources; + sources.prepend({texture, width, height, filter, wrap}); + + for(auto& p : programs) { + unsigned targetWidth = p.absoluteWidth ? p.absoluteWidth : outputWidth; + unsigned targetHeight = p.absoluteHeight ? p.absoluteHeight : outputHeight; + if(p.relativeWidth) targetWidth = sources[0].width * p.relativeWidth; + if(p.relativeHeight) targetHeight = sources[0].height * p.relativeHeight; + + p.size(targetWidth, targetHeight); + glUseProgram(p.program); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, p.framebuffer); + + glrUniform1i("phase", p.phase); + glrUniform1i("historyLength", history.size()); + glrUniform1i("sourceLength", sources.size()); + glrUniform1i("pixmapLength", p.pixmaps.size()); + glrUniform4f("targetSize", targetWidth, targetHeight, 1.0 / targetWidth, 1.0 / targetHeight); + glrUniform4f("outputSize", outputWidth, outputHeight, 1.0 / outputWidth, 1.0 / outputHeight); + + unsigned aid = 0; + for(auto& frame : history) { + glrUniform1i({"history[", aid, "]"}, aid); + glrUniform4f({"historySize[", aid, "]"}, frame.width, frame.height, 1.0 / frame.width, 1.0 / frame.height); + glActiveTexture(GL_TEXTURE0 + (aid++)); + glBindTexture(GL_TEXTURE_2D, frame.texture); + glrParameters(frame.filter, frame.wrap); + } + + unsigned bid = 0; + for(auto& source : sources) { + glrUniform1i({"source[", bid, "]"}, aid + bid); + glrUniform4f({"sourceSize[", bid, "]"}, source.width, source.height, 1.0 / source.width, 1.0 / source.height); + glActiveTexture(GL_TEXTURE0 + aid + (bid++)); + glBindTexture(GL_TEXTURE_2D, source.texture); + glrParameters(source.filter, source.wrap); + } + + unsigned cid = 0; + for(auto& pixmap : p.pixmaps) { + glrUniform1i({"pixmap[", cid, "]"}, aid + bid + cid); + glrUniform4f({"pixmapSize[", bid, "]"}, pixmap.width, pixmap.height, 1.0 / pixmap.width, 1.0 / pixmap.height); + glActiveTexture(GL_TEXTURE0 + aid + bid + (cid++)); + glBindTexture(GL_TEXTURE_2D, pixmap.texture); + glrParameters(pixmap.filter, pixmap.wrap); + } + + glActiveTexture(GL_TEXTURE0); + glrParameters(sources[0].filter, sources[0].wrap); + p.render(sources[0].width, sources[0].height, targetWidth, targetHeight); + glBindTexture(GL_TEXTURE_2D, p.texture); + + p.phase = (p.phase + 1) % p.modulo; + sources.prepend({p.texture, p.width, p.height, p.filter, p.wrap}); + } + + unsigned targetWidth = absoluteWidth ? absoluteWidth : outputWidth; + unsigned targetHeight = absoluteHeight ? absoluteHeight : outputHeight; + if(relativeWidth) targetWidth = sources[0].width * relativeWidth; + if(relativeHeight) targetHeight = sources[0].height * relativeHeight; + + glUseProgram(program); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + + glrUniform1i("source[0]", 0); + glrUniform4f("targetSize", targetWidth, targetHeight, 1.0 / targetWidth, 1.0 / targetHeight); + glrUniform4f("outputSize", outputWidth, outputHeight, 1.0 / outputWidth, 1.0 / outputHeight); + + glrParameters(sources[0].filter, sources[0].wrap); + render(sources[0].width, sources[0].height, outputWidth, outputHeight); + + if(history.size() > 0) { + OpenGLTexture frame = history.takeLast(); + + glBindTexture(GL_TEXTURE_2D, frame.texture); + if(width == frame.width && height == frame.height) { + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, getFormat(), getType(), buffer); + } else { + glTexImage2D(GL_TEXTURE_2D, 0, format, frame.width = width, frame.height = height, 0, getFormat(), getType(), buffer); + } + + history.prepend(frame); + } +} + +auto OpenGL::init() -> bool { + if(!OpenGLBind()) return false; + + glDisable(GL_ALPHA_TEST); + glDisable(GL_BLEND); + glDisable(GL_DEPTH_TEST); + glDisable(GL_POLYGON_SMOOTH); + glDisable(GL_STENCIL_TEST); + + glEnable(GL_DITHER); + glEnable(GL_TEXTURE_2D); + + program = glCreateProgram(); + vertex = glrCreateShader(program, GL_VERTEX_SHADER, OpenGLOutputVertexShader); +//geometry = glrCreateShader(program, GL_GEOMETRY_SHADER, OpenGLGeometryShader); + fragment = glrCreateShader(program, GL_FRAGMENT_SHADER, OpenGLFragmentShader); + OpenGLSurface::allocate(); + glrLinkProgram(program); + + shader(""); + return initialized = true; +} + +auto OpenGL::term() -> void { + if(initialized == false) return; + shader(""); //release shader resources (eg frame[] history) + OpenGLSurface::release(); + if(buffer) { delete[] buffer; buffer = nullptr; } + initialized = false; +} diff --git a/video/opengl/opengl.hpp b/video/opengl/opengl.hpp new file mode 100644 index 0000000..797f3fa --- /dev/null +++ b/video/opengl/opengl.hpp @@ -0,0 +1,93 @@ +#if defined(DISPLAY_XORG) + #include + #include + #define glGetProcAddress(name) (*glXGetProcAddress)((const GLubyte*)(name)) +#elif defined(DISPLAY_QUARTZ) + #include +#elif defined(DISPLAY_WINDOWS) + #include + #include + #define glGetProcAddress(name) wglGetProcAddress(name) +#else + #error "ruby::OpenGL: unsupported platform" +#endif + +#include "bind.hpp" +#include "shaders.hpp" +#include "utility.hpp" + +struct OpenGL; + +struct OpenGLTexture { + auto getFormat() const -> GLuint; + auto getType() const -> GLuint; + + GLuint texture = 0; + unsigned width = 0; + unsigned height = 0; + GLuint format = GL_RGBA8; + GLuint filter = GL_LINEAR; + GLuint wrap = GL_CLAMP_TO_BORDER; +}; + +struct OpenGLSurface : OpenGLTexture { + auto allocate() -> void; + auto size(unsigned width, unsigned height) -> void; + auto release() -> void; + auto render(unsigned sourceWidth, unsigned sourceHeight, unsigned targetWidth, unsigned targetHeight) -> void; + + GLuint program = 0; + GLuint framebuffer = 0; + GLuint vao = 0; + GLuint vbo[3] = {0, 0, 0}; + GLuint vertex = 0; + GLuint geometry = 0; + GLuint fragment = 0; + uint32_t* buffer = nullptr; +}; + +struct OpenGLProgram : OpenGLSurface { + auto bind(OpenGL* instance, const Markup::Node& node, const string& pathname) -> void; + auto parse(OpenGL* instance, string& source) -> void; + auto release() -> void; + + unsigned phase = 0; //frame counter + unsigned modulo = 0; //frame counter modulus + unsigned absoluteWidth = 0; + unsigned absoluteHeight = 0; + double relativeWidth = 0; + double relativeHeight = 0; + vector pixmaps; +}; + +struct OpenGL : OpenGLProgram { + auto shader(const string& pathname) -> void; + auto allocateHistory(unsigned size) -> void; + auto lock(uint32_t*& data, unsigned& pitch) -> bool; + auto clear() -> void; + auto refresh() -> void; + auto init() -> bool; + auto term() -> void; + + vector programs; + vector history; + GLuint inputFormat = GL_RGBA8; + unsigned outputWidth = 0; + unsigned outputHeight = 0; + struct Setting { + string name; + string value; + bool operator< (const Setting& source) const { return name < source.name; } + bool operator==(const Setting& source) const { return name == source.name; } + Setting() = default; + Setting(const string& name) : name(name) {} + Setting(const string& name, const string& value) : name(name), value(value) {} + }; + set settings; + bool initialized = false; +}; + +#include "texture.hpp" +#include "surface.hpp" +#include "program.hpp" +#include "main.hpp" diff --git a/video/opengl/program.hpp b/video/opengl/program.hpp new file mode 100644 index 0000000..1d34682 --- /dev/null +++ b/video/opengl/program.hpp @@ -0,0 +1,108 @@ +auto OpenGLProgram::bind(OpenGL* instance, const Markup::Node& node, const string& pathname) -> void { + filter = glrFilter(node["filter"].text()); + wrap = glrWrap(node["wrap"].text()); + modulo = glrModulo(node["modulo"].integer()); + + string w = node["width"].text(), h = node["height"].text(); + if(w.endsWith("%")) relativeWidth = real(w.rtrim("%", 1L)) / 100.0; + else absoluteWidth = w.natural(); + if(h.endsWith("%")) relativeHeight = real(h.rtrim("%", 1L)) / 100.0; + else absoluteHeight = h.natural(); + + format = glrFormat(node["format"].text()); + + program = glCreateProgram(); + glGenFramebuffers(1, &framebuffer); + + if(file::exists({pathname, node["vertex"].text()})) { + string source = file::read({pathname, node["vertex"].text()}); + parse(instance, source); + vertex = glrCreateShader(program, GL_VERTEX_SHADER, source); + } else { + vertex = glrCreateShader(program, GL_VERTEX_SHADER, OpenGLVertexShader); + } + + if(file::exists({pathname, node["geometry"].text()})) { + string source = file::read({pathname, node["geometry"].text()}); + parse(instance, source); + geometry = glrCreateShader(program, GL_GEOMETRY_SHADER, source); + } else { + //geometry shaders, when attached, must pass all vertex output through to the fragment shaders + //geometry = glrCreateShader(program, GL_GEOMETRY_SHADER, OpenGLGeometryShader); + } + + if(file::exists({pathname, node["fragment"].text()})) { + string source = file::read({pathname, node["fragment"].text()}); + parse(instance, source); + fragment = glrCreateShader(program, GL_FRAGMENT_SHADER, source); + } else { + fragment = glrCreateShader(program, GL_FRAGMENT_SHADER, OpenGLFragmentShader); + } + + for(auto& leaf : node.find("pixmap")) { + nall::image image({pathname, leaf.text()}); + image.transform(); + if(image.empty()) continue; + + GLuint texture; + glGenTextures(1, &texture); + + unsigned n = pixmaps.size(); + pixmaps(n).texture = texture; + pixmaps(n).width = image.width(); + pixmaps(n).height = image.height(); + pixmaps(n).format = format; + pixmaps(n).filter = filter; + pixmaps(n).wrap = wrap; + if(leaf["format"]) pixmaps(n).format = glrFormat(leaf["format"].text()); + if(leaf["filter"]) pixmaps(n).filter = glrFilter(leaf["filter"].text()); + if(leaf["wrap"]) pixmaps(n).wrap = glrWrap(leaf["wrap"].text()); + + unsigned w = glrSize(image.width()), h = glrSize(image.height()); + uint32_t* buffer = new uint32_t[w * h](); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, pixmaps(n).format, w, h, 0, pixmaps(n).getFormat(), pixmaps(n).getType(), buffer); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image.width(), image.height(), getFormat(), getType(), image.data()); + delete[] buffer; + } + + OpenGLSurface::allocate(); + glrLinkProgram(program); +} + +//apply manifest settings to shader source #in tags +auto OpenGLProgram::parse(OpenGL* instance, string& source) -> void { + lstring lines = source.split("\n"); + for(auto& line : lines) { + string s = line; + if(auto position = s.find("//")) s.resize(position()); //strip comments + s.strip(); //remove extraneous whitespace + if(s.match("#in ?*")) { + s.ltrim("#in ", 1L).strip(); + if(auto setting = instance->settings.find({s})) { + line = {"#define ", setting().name, " ", setting().value}; + } else { + line.reset(); //undefined variable (test in source with #ifdef) + } + } + } + source = lines.merge("\n"); +} + +auto OpenGLProgram::release() -> void { + OpenGLSurface::release(); + for(auto& pixmap : pixmaps) glDeleteTextures(1, &pixmap.texture); + pixmaps.reset(); + + width = 0; + height = 0; + format = GL_RGBA8; + filter = GL_LINEAR; + wrap = GL_CLAMP_TO_BORDER; + phase = 0; + modulo = 0; + absoluteWidth = 0; + absoluteHeight = 0; + relativeWidth = 0; + relativeHeight = 0; +} diff --git a/video/opengl/shaders.hpp b/video/opengl/shaders.hpp new file mode 100644 index 0000000..91f9efc --- /dev/null +++ b/video/opengl/shaders.hpp @@ -0,0 +1,91 @@ +static string OpenGLOutputVertexShader = R"( + #version 150 + + uniform vec4 targetSize; + uniform vec4 outputSize; + + in vec2 texCoord; + + out Vertex { + vec2 texCoord; + } vertexOut; + + void main() { + //center image within output window + if(gl_VertexID == 0 || gl_VertexID == 2) { + gl_Position.x = -(targetSize.x / outputSize.x); + } else { + gl_Position.x = +(targetSize.x / outputSize.x); + } + + //center and flip vertically (buffer[0, 0] = top-left; OpenGL[0, 0] = bottom-left) + if(gl_VertexID == 0 || gl_VertexID == 1) { + gl_Position.y = +(targetSize.y / outputSize.y); + } else { + gl_Position.y = -(targetSize.y / outputSize.y); + } + + //align image to even pixel boundary to prevent aliasing + vec2 align = fract((outputSize.xy + targetSize.xy) / 2.0) * 2.0; + gl_Position.xy -= align / outputSize.xy; + gl_Position.zw = vec2(0.0, 1.0); + + vertexOut.texCoord = texCoord; + } +)"; + +static string OpenGLVertexShader = R"( + #version 150 + + in vec4 position; + in vec2 texCoord; + + out Vertex { + vec2 texCoord; + } vertexOut; + + void main() { + gl_Position = position; + vertexOut.texCoord = texCoord; + } +)"; + +static string OpenGLGeometryShader = R"( + #version 150 + + layout(triangles) in; + layout(triangle_strip, max_vertices = 3) out; + + in Vertex { + vec2 texCoord; + } vertexIn[]; + + out Vertex { + vec2 texCoord; + }; + + void main() { + for(int i = 0; i < gl_in.length(); i++) { + gl_Position = gl_in[i].gl_Position; + texCoord = vertexIn[i].texCoord; + EmitVertex(); + } + EndPrimitive(); + } +)"; + +static string OpenGLFragmentShader = R"( + #version 150 + + uniform sampler2D source[]; + + in Vertex { + vec2 texCoord; + }; + + out vec4 fragColor; + + void main() { + fragColor = texture(source[0], texCoord); + } +)"; diff --git a/video/opengl/surface.hpp b/video/opengl/surface.hpp new file mode 100644 index 0000000..7922c47 --- /dev/null +++ b/video/opengl/surface.hpp @@ -0,0 +1,114 @@ +auto OpenGLSurface::allocate() -> void { + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + glGenBuffers(3, &vbo[0]); +} + +auto OpenGLSurface::size(unsigned w, unsigned h) -> void { + if(width == w && height == h) return; + width = w, height = h; + w = glrSize(w), h = glrSize(h); + + if(texture) { glDeleteTextures(1, &texture); texture = 0; } + if(buffer) { delete[] buffer; buffer = nullptr; } + + buffer = new uint32_t[w * h](); + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, format, w, h, 0, getFormat(), getType(), buffer); + + if(framebuffer) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + delete[] buffer; + buffer = nullptr; + } +} + +auto OpenGLSurface::release() -> void { + if(vbo[0]) { glDeleteBuffers(3, &vbo[0]); for(auto &o : vbo) o = 0; } + if(vao) { glDeleteVertexArrays(1, &vao); vao = 0; } + if(vertex) { glDetachShader(program, vertex); glDeleteShader(vertex); vertex = 0; } + if(geometry) { glDetachShader(program, geometry); glDeleteShader(geometry); geometry = 0; } + if(fragment) { glDetachShader(program, fragment); glDeleteShader(fragment); fragment = 0; } + if(texture) { glDeleteTextures(1, &texture); texture = 0; } + if(framebuffer) { glDeleteFramebuffers(1, &framebuffer); framebuffer = 0; } + if(program) { glDeleteProgram(program); program = 0; } + width = 0, height = 0; +} + +auto OpenGLSurface::render(unsigned sourceWidth, unsigned sourceHeight, unsigned targetWidth, unsigned targetHeight) -> void { + glViewport(0, 0, targetWidth, targetHeight); + + float w = (float)sourceWidth / (float)glrSize(sourceWidth); + float h = (float)sourceHeight / (float)glrSize(sourceHeight); + float u = (float)targetWidth, v = (float)targetHeight; + GLint location; + + GLfloat modelView[] = { + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + }; + + GLfloat projection[] = { + 2.0f/u, 0.0f, 0.0f, 0.0f, + 0.0f, 2.0f/v, 0.0f, 0.0f, + 0.0f, 0.0f, -1.0f, 0.0f, + -1.0f, -1.0f, 0.0f, 1.0f, + }; + + GLfloat modelViewProjection[4 * 4]; + Matrix::Multiply(modelViewProjection, modelView, 4, 4, projection, 4, 4); + + GLfloat vertices[] = { + 0, 0, 0, 1, + u, 0, 0, 1, + 0, v, 0, 1, + u, v, 0, 1, + }; + + GLfloat positions[4 * 4]; + for(unsigned n = 0; n < 16; n += 4) { + Matrix::Multiply(&positions[n], &vertices[n], 1, 4, modelViewProjection, 4, 4); + } + + GLfloat texCoords[] = { + 0, 0, + w, 0, + 0, h, + w, h, + }; + + glrUniformMatrix4fv("modelView", modelView); + glrUniformMatrix4fv("projection", projection); + glrUniformMatrix4fv("modelViewProjection", modelViewProjection); + + glBindVertexArray(vao); + + glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); + glBufferData(GL_ARRAY_BUFFER, 16 * sizeof(GLfloat), vertices, GL_STATIC_DRAW); + GLuint locationVertex = glGetAttribLocation(program, "vertex"); + glEnableVertexAttribArray(locationVertex); + glVertexAttribPointer(locationVertex, 4, GL_FLOAT, GL_FALSE, 0, 0); + + glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); + glBufferData(GL_ARRAY_BUFFER, 16 * sizeof(GLfloat), positions, GL_STATIC_DRAW); + GLuint locationPosition = glGetAttribLocation(program, "position"); + glEnableVertexAttribArray(locationPosition); + glVertexAttribPointer(locationPosition, 4, GL_FLOAT, GL_FALSE, 0, 0); + + glBindBuffer(GL_ARRAY_BUFFER, vbo[2]); + glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(GLfloat), texCoords, GL_STATIC_DRAW); + GLuint locationTexCoord = glGetAttribLocation(program, "texCoord"); + glEnableVertexAttribArray(locationTexCoord); + glVertexAttribPointer(locationTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0); + + glBindFragDataLocation(program, 0, "fragColor"); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + glDisableVertexAttribArray(locationVertex); + glDisableVertexAttribArray(locationPosition); + glDisableVertexAttribArray(locationTexCoord); +} diff --git a/video/opengl/texture.hpp b/video/opengl/texture.hpp new file mode 100644 index 0000000..d79a6a8 --- /dev/null +++ b/video/opengl/texture.hpp @@ -0,0 +1,12 @@ +auto OpenGLTexture::getFormat() const -> GLuint { + if(format == GL_R32I) return GL_RED_INTEGER; + if(format == GL_R32UI) return GL_RED_INTEGER; + return GL_BGRA; +} + +auto OpenGLTexture::getType() const -> GLuint { + if(format == GL_R32I) return GL_UNSIGNED_INT; + if(format == GL_R32UI) return GL_UNSIGNED_INT; + if(format == GL_RGB10_A2) return GL_UNSIGNED_INT_2_10_10_10_REV; + return GL_UNSIGNED_INT_8_8_8_8_REV; +} diff --git a/video/opengl/utility.hpp b/video/opengl/utility.hpp new file mode 100644 index 0000000..6a632c1 --- /dev/null +++ b/video/opengl/utility.hpp @@ -0,0 +1,106 @@ +static auto glrSize(unsigned size) -> unsigned { + return size; +//return bit::round(size); //return nearest power of two +} + +static auto glrFormat(const string& format) -> GLuint { + if(format == "r32i" ) return GL_R32I; + if(format == "r32ui" ) return GL_R32UI; + if(format == "rgba8" ) return GL_RGBA8; + if(format == "rgb10a2") return GL_RGB10_A2; + if(format == "rgba12" ) return GL_RGBA12; + if(format == "rgba16" ) return GL_RGBA16; + if(format == "rgba16f") return GL_RGBA16F; + if(format == "rgba32f") return GL_RGBA32F; + return GL_RGBA8; +} + +static auto glrFilter(const string& filter) -> GLuint { + if(filter == "nearest") return GL_NEAREST; + if(filter == "linear" ) return GL_LINEAR; + return GL_LINEAR; +} + +static auto glrWrap(const string& wrap) -> GLuint { + if(wrap == "border") return GL_CLAMP_TO_BORDER; + if(wrap == "edge" ) return GL_CLAMP_TO_EDGE; + if(wrap == "repeat") return GL_REPEAT; + return GL_CLAMP_TO_BORDER; +} + +static auto glrModulo(unsigned modulo) -> unsigned { + if(modulo) return modulo; + return 300; //divisible by 2, 3, 4, 5, 6, 10, 12, 15, 20, 25, 30, 50, 60, 100, 150 +} + +static auto glrProgram() -> GLuint { + GLuint program = 0; + glGetIntegerv(GL_CURRENT_PROGRAM, (GLint*)&program); + return program; +} + +static auto glrUniform1i(const string& name, GLint value) -> void { + GLint location = glGetUniformLocation(glrProgram(), name); + glUniform1i(location, value); +} + +static auto glrUniform4f(const string& name, GLfloat value0, GLfloat value1, GLfloat value2, GLfloat value3) -> void { + GLint location = glGetUniformLocation(glrProgram(), name); + glUniform4f(location, value0, value1, value2, value3); +} + +static auto glrUniformMatrix4fv(const string& name, GLfloat* values) -> void { + GLint location = glGetUniformLocation(glrProgram(), name); + glUniformMatrix4fv(location, 1, GL_FALSE, values); +} + +static auto glrParameters(GLuint filter, GLuint wrap) -> void { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap); +} + +static auto glrCreateShader(GLuint program, GLuint type, const char* source) -> GLuint { + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, 0); + glCompileShader(shader); + GLint result = GL_FALSE; + glGetShaderiv(shader, GL_COMPILE_STATUS, &result); + if(result == GL_FALSE) { + GLint length = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); + char text[length + 1]; + glGetShaderInfoLog(shader, length, &length, text); + text[length] = 0; + print("[ruby::OpenGL: shader compiler error]\n", (const char*)text, "\n\n"); + return 0; + } + glAttachShader(program, shader); + return shader; +} + +static auto glrLinkProgram(GLuint program) -> void { + glLinkProgram(program); + GLint result = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &result); + if(result == GL_FALSE) { + GLint length = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); + char text[length + 1]; + glGetProgramInfoLog(program, length, &length, text); + text[length] = 0; + print("[ruby::OpenGL: shader linker error]\n", (const char*)text, "\n\n"); + } + glValidateProgram(program); + result = GL_FALSE; + glGetProgramiv(program, GL_VALIDATE_STATUS, &result); + if(result == GL_FALSE) { + GLint length = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); + char text[length + 1]; + glGetProgramInfoLog(program, length, &length, text); + text[length] = 0; + print("[ruby::OpenGL: shader validation error]\n", (const char*)text, "\n\n"); + } +} diff --git a/video/sdl.cpp b/video/sdl.cpp new file mode 100644 index 0000000..da335e0 --- /dev/null +++ b/video/sdl.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include +#include + +struct VideoSDL : Video { + ~VideoSDL() { term(); } + + Display* display = nullptr; + SDL_Surface* screen = nullptr; + SDL_Surface* buffer = nullptr; + unsigned iwidth = 0; + unsigned iheight = 0; + + struct { + uintptr_t handle = 0; + + unsigned width = 0; + unsigned height = 0; + } settings; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return settings.handle; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = value.get(); + return true; + } + + return false; + } + + auto resize(unsigned width, unsigned height) -> void { + if(iwidth >= width && iheight >= height) return; + + iwidth = max(width, iwidth); + iheight = max(height, iheight); + + if(buffer) SDL_FreeSurface(buffer); + buffer = SDL_CreateRGBSurface( + SDL_SWSURFACE, iwidth, iheight, 32, + 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 + ); + } + + auto lock(uint32_t*& data, unsigned& pitch, unsigned width, unsigned height) -> bool { + if(width != settings.width || height != settings.height) { + resize(settings.width = width, settings.height = height); + } + + if(SDL_MUSTLOCK(buffer)) SDL_LockSurface(buffer); + pitch = buffer->pitch; + return data = (uint32_t*)buffer->pixels; + } + + auto unlock() -> void { + if(SDL_MUSTLOCK(buffer)) SDL_UnlockSurface(buffer); + } + + auto clear() -> void { + if(SDL_MUSTLOCK(buffer)) SDL_LockSurface(buffer); + for(unsigned y = 0; y < iheight; y++) { + uint32_t* data = (uint32_t*)buffer->pixels + y * (buffer->pitch >> 2); + for(unsigned x = 0; x < iwidth; x++) *data++ = 0xff000000; + } + if(SDL_MUSTLOCK(buffer)) SDL_UnlockSurface(buffer); + refresh(); + } + + auto refresh() -> void { + //ruby input is X8R8G8B8, top 8-bits are ignored. + //as SDL forces us to use a 32-bit buffer, we must set alpha to 255 (full opacity) + //to prevent blending against the window beneath when X window visual is 32-bits. + if(SDL_MUSTLOCK(buffer)) SDL_LockSurface(buffer); + for(unsigned y = 0; y < settings.height; y++) { + uint32_t* data = (uint32_t*)buffer->pixels + y * (buffer->pitch >> 2); + for(unsigned x = 0; x < settings.width; x++) *data++ |= 0xff000000; + } + if(SDL_MUSTLOCK(buffer)) SDL_UnlockSurface(buffer); + + XWindowAttributes attributes; + XGetWindowAttributes(display, settings.handle, &attributes); + + SDL_Rect src, dest; + + src.x = 0; + src.y = 0; + src.w = settings.width; + src.h = settings.height; + + dest.x = 0; + dest.y = 0; + dest.w = attributes.width; + dest.h = attributes.height; + + SDL_SoftStretch(buffer, &src, screen, &dest); + SDL_UpdateRect(screen, dest.x, dest.y, dest.w, dest.h); + } + + auto init() -> bool { + display = XOpenDisplay(0); + + //todo: this causes a segfault inside SDL_SetVideoMode on FreeBSD (works under Linux) + char env[512]; + sprintf(env, "SDL_WINDOWID=%ld", (long)settings.handle); + putenv(env); + + SDL_InitSubSystem(SDL_INIT_VIDEO); + screen = SDL_SetVideoMode(2560, 1600, 32, SDL_HWSURFACE); + XUndefineCursor(display, settings.handle); + + buffer = 0; + iwidth = 0; + iheight = 0; + resize(settings.width = 256, settings.height = 256); + + return true; + } + + auto term() -> void { + XCloseDisplay(display); + SDL_FreeSurface(buffer); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + } +}; diff --git a/video/wgl.cpp b/video/wgl.cpp new file mode 100644 index 0000000..703953b --- /dev/null +++ b/video/wgl.cpp @@ -0,0 +1,142 @@ +#include "opengl/opengl.hpp" + +#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091 +#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092 + +struct VideoWGL : Video, OpenGL { + ~VideoWGL() { term(); } + + auto (APIENTRY* wglCreateContextAttribs)(HDC, HGLRC, const int*) -> HGLRC = nullptr; + auto (APIENTRY* wglSwapInterval)(int) -> BOOL = nullptr; + + HDC display = nullptr; + HGLRC wglcontext = nullptr; + HWND window = nullptr; + HINSTANCE glwindow = nullptr; + + struct { + HWND handle = nullptr; + bool synchronize = false; + uint filter = Video::FilterNearest; + string shader; + } settings; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + if(name == Video::Synchronize) return true; + if(name == Video::Filter) return true; + if(name == Video::Shader) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return (uintptr)settings.handle; + if(name == Video::Synchronize) return settings.synchronize; + if(name == Video::Filter) return settings.filter; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = (HWND)value.get(); + return true; + } + + if(name == Video::Synchronize && value.is()) { + if(settings.synchronize != value.get()) { + settings.synchronize = value.get(); + if(wglcontext) { + init(); + OpenGL::shader(settings.shader); + if(settings.shader.empty()) OpenGL::filter = settings.filter ? GL_LINEAR : GL_NEAREST; + } + } + } + + if(name == Video::Filter && value.is()) { + settings.filter = value.get(); + if(!settings.shader) OpenGL::filter = settings.filter ? GL_LINEAR : GL_NEAREST; + return true; + } + + if(name == Video::Shader && value.is()) { + settings.shader = value.get(); + OpenGL::shader(settings.shader); + if(!settings.shader) OpenGL::filter = settings.filter ? GL_LINEAR : GL_NEAREST; + return true; + } + + return false; + } + + auto lock(uint32_t*& data, uint& pitch, uint width, uint height) -> bool { + OpenGL::size(width, height); + return OpenGL::lock(data, pitch); + } + + auto unlock() -> void { + } + + auto clear() -> void { + OpenGL::clear(); + SwapBuffers(display); + } + + auto refresh() -> void { + RECT rc; + GetClientRect(settings.handle, &rc); + outputWidth = rc.right - rc.left, outputHeight = rc.bottom - rc.top; + OpenGL::refresh(); + SwapBuffers(display); + } + + auto init() -> bool { + GLuint pixel_format; + PIXELFORMATDESCRIPTOR pfd; + memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR)); + pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); + pfd.nVersion = 1; + pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pfd.iPixelType = PFD_TYPE_RGBA; + + display = GetDC(settings.handle); + pixel_format = ChoosePixelFormat(display, &pfd); + SetPixelFormat(display, pixel_format, &pfd); + + wglcontext = wglCreateContext(display); + wglMakeCurrent(display, wglcontext); + + wglCreateContextAttribs = (HGLRC (APIENTRY*)(HDC, HGLRC, const int*))glGetProcAddress("wglCreateContextAttribsARB"); + wglSwapInterval = (BOOL (APIENTRY*)(int))glGetProcAddress("wglSwapIntervalEXT"); + + if(wglCreateContextAttribs) { + int attributes[] = { + WGL_CONTEXT_MAJOR_VERSION_ARB, 3, + WGL_CONTEXT_MINOR_VERSION_ARB, 2, + 0 + }; + HGLRC context = wglCreateContextAttribs(display, 0, attributes); + if(context) { + wglMakeCurrent(NULL, NULL); + wglDeleteContext(wglcontext); + wglMakeCurrent(display, wglcontext = context); + } + } + + if(wglSwapInterval) { + wglSwapInterval(settings.synchronize); + } + + OpenGL::init(); + return true; + } + + auto term() -> void { + OpenGL::term(); + + if(wglcontext) { + wglDeleteContext(wglcontext); + wglcontext = nullptr; + } + } +}; diff --git a/video/xshm.cpp b/video/xshm.cpp new file mode 100644 index 0000000..7631a71 --- /dev/null +++ b/video/xshm.cpp @@ -0,0 +1,205 @@ +//XShm driver for Xorg + +//Note that on composited displays, the alpha bits will allow translucency underneath the active window +//As this is not a feature of ruby, this driver must always set the alpha bits on clear() and refresh() + +//Linear interpolation is only applied horizontally for performance reasons, although Nearest is still much faster + +#include +#include + +struct VideoXShm : Video { + ~VideoXShm() { term(); } + + struct Device { + Display* display = nullptr; + signed screen = 0; + signed depth = 0; + Visual* visual = nullptr; + Window window = 0; + + XShmSegmentInfo shmInfo; + XImage* image = nullptr; + uint32_t* buffer = nullptr; + unsigned width = 0; + unsigned height = 0; + } device; + + struct Settings { + uintptr_t handle = 0; + unsigned filter = Video::FilterLinear; + + uint32_t* buffer = nullptr; + unsigned width = 0; + unsigned height = 0; + } settings; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + if(name == Video::Filter) return true; + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return settings.handle; + if(name == Video::Filter) return settings.filter; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = value.get(); + return true; + } + if(name == Video::Filter && value.is()) { + settings.filter = value.get(); + return true; + } + return false; + } + + auto lock(uint32_t*& data, unsigned& pitch, unsigned width, unsigned height) -> bool { + if(settings.buffer == nullptr || settings.width != width || settings.height != height) { + if(settings.buffer) delete[] settings.buffer; + settings.width = width, settings.height = height; + settings.buffer = new uint32_t[width * height + 16]; //+16 is padding for linear interpolation + } + + data = settings.buffer; + pitch = settings.width * sizeof(uint32_t); + return true; + } + + auto unlock() -> void { + } + + auto clear() -> void { + if(settings.buffer == nullptr) return; + uint32_t* dp = settings.buffer; + unsigned length = settings.width * settings.height; + while(length--) *dp++ = 255u << 24; + refresh(); + } + + auto refresh() -> void { + if(settings.buffer == nullptr) return; + size(); + + float xratio = (float)settings.width / (float)device.width; + float yratio = (float)settings.height / (float)device.height; + + #pragma omp parallel for + for(unsigned y = 0; y < device.height; y++) { + float ystep = y * yratio; + float xstep = 0; + + uint32_t* sp = settings.buffer + (unsigned)ystep * settings.width; + uint32_t* dp = device.buffer + y * device.width; + + if(settings.filter == Video::FilterNearest) { + for(unsigned x = 0; x < device.width; x++) { + *dp++ = 255u << 24 | sp[(unsigned)xstep]; + xstep += xratio; + } + } else { //settings.filter == Video::FilterLinear + for(unsigned x = 0; x < device.width; x++) { + *dp++ = 255u << 24 | interpolate(xstep - (unsigned)xstep, sp[(unsigned)xstep], sp[(unsigned)xstep + 1]); + xstep += xratio; + } + } + } + + GC gc = XCreateGC(device.display, device.window, 0, 0); + XShmPutImage( + device.display, device.window, gc, device.image, + 0, 0, 0, 0, device.width, device.height, False + ); + XFreeGC(device.display, gc); + XFlush(device.display); + } + + auto init() -> bool { + device.display = XOpenDisplay(0); + device.screen = DefaultScreen(device.display); + + XWindowAttributes getAttributes; + XGetWindowAttributes(device.display, (Window)settings.handle, &getAttributes); + device.depth = getAttributes.depth; + device.visual = getAttributes.visual; + //driver only supports 32-bit pixels + //note that even on 15-bit and 16-bit displays, the window visual's depth should be 32 + if(device.depth < 24 || device.depth > 32) { + free(); + return false; + } + + XSetWindowAttributes setAttributes = {0}; + setAttributes.border_pixel = 0; + device.window = XCreateWindow(device.display, (Window)settings.handle, + 0, 0, 256, 256, 0, + getAttributes.depth, InputOutput, getAttributes.visual, + CWBorderPixel, &setAttributes + ); + XSetWindowBackground(device.display, device.window, 0); + XMapWindow(device.display, device.window); + XFlush(device.display); + + while(XPending(device.display)) { + XEvent event; + XNextEvent(device.display, &event); + } + + if(size() == false) return false; + return true; + } + + auto term() -> void { + free(); + if(device.display) { + XCloseDisplay(device.display); + device.display = nullptr; + } + } + +private: + auto size() -> bool { + XWindowAttributes windowAttributes; + XGetWindowAttributes(device.display, settings.handle, &windowAttributes); + + if(device.buffer && device.width == windowAttributes.width && device.height == windowAttributes.height) return true; + device.width = windowAttributes.width, device.height = windowAttributes.height; + XResizeWindow(device.display, device.window, device.width, device.height); + free(); + + device.shmInfo.shmid = shmget(IPC_PRIVATE, device.width * device.height * sizeof(uint32_t), IPC_CREAT | 0777); + if(device.shmInfo.shmid < 0) return false; + + device.shmInfo.shmaddr = (char*)shmat(device.shmInfo.shmid, 0, 0); + device.shmInfo.readOnly = False; + XShmAttach(device.display, &device.shmInfo); + device.buffer = (uint32_t*)device.shmInfo.shmaddr; + device.image = XShmCreateImage(device.display, device.visual, device.depth, + ZPixmap, device.shmInfo.shmaddr, &device.shmInfo, device.width, device.height + ); + + return true; + } + + auto free() -> void { + if(device.buffer == nullptr) return; + device.buffer = nullptr; + XShmDetach(device.display, &device.shmInfo); + XDestroyImage(device.image); + shmdt(device.shmInfo.shmaddr); + shmctl(device.shmInfo.shmid, IPC_RMID, 0); + } + + alwaysinline auto interpolate(float mu, uint32_t a, uint32_t b) -> uint32_t { + uint8_t ar = a >> 16, ag = a >> 8, ab = a >> 0; + uint8_t br = b >> 16, bg = b >> 8, bb = b >> 0; + uint8_t cr = ar * (1.0 - mu) + br * mu; + uint8_t cg = ag * (1.0 - mu) + bg * mu; + uint8_t cb = ab * (1.0 - mu) + bb * mu; + return cr << 16 | cg << 8 | cb << 0; + } +}; diff --git a/video/xv.cpp b/video/xv.cpp new file mode 100644 index 0000000..723ecf0 --- /dev/null +++ b/video/xv.cpp @@ -0,0 +1,489 @@ +#include +#include +#include +#include +#include + +extern "C" auto XvShmCreateImage(Display*, XvPortID, signed, char*, signed, signed, XShmSegmentInfo*) -> XvImage*; + +struct VideoXv : Video { + ~VideoXv() { term(); } + + uint32_t* buffer = nullptr; + uint8_t* ytable = nullptr; + uint8_t* utable = nullptr; + uint8_t* vtable = nullptr; + + enum XvFormat : unsigned { + XvFormatRGB32, + XvFormatRGB24, + XvFormatRGB16, + XvFormatRGB15, + XvFormatYUY2, + XvFormatUYVY, + XvFormatUnknown, + }; + + struct { + Display* display = nullptr; + GC gc = 0; + Window window = 0; + Colormap colormap = 0; + XShmSegmentInfo shminfo; + + signed port = -1; + signed depth = 0; + signed visualid = 0; + + XvImage* image = nullptr; + XvFormat format = XvFormatUnknown; + uint32_t fourcc = 0; + + unsigned width = 0; + unsigned height = 0; + } device; + + struct { + Window handle = 0; + bool synchronize = false; + + unsigned width = 0; + unsigned height = 0; + } settings; + + auto cap(const string& name) -> bool { + if(name == Video::Handle) return true; + if(name == Video::Synchronize) { + Display* display = XOpenDisplay(nullptr); + bool result = XInternAtom(display, "XV_SYNC_TO_VBLANK", true) != None; + XCloseDisplay(display); + return result; + } + return false; + } + + auto get(const string& name) -> any { + if(name == Video::Handle) return settings.handle; + if(name == Video::Synchronize) return settings.synchronize; + return {}; + } + + auto set(const string& name, const any& value) -> bool { + if(name == Video::Handle && value.is()) { + settings.handle = value.get(); + return true; + } + + if(name == Video::Synchronize && value.is()) { + bool result = false; + Display* display = XOpenDisplay(nullptr); + Atom atom = XInternAtom(display, "XV_SYNC_TO_VBLANK", true); + if(atom != None && device.port >= 0) { + settings.synchronize = value.get(); + XvSetPortAttribute(display, device.port, atom, settings.synchronize); + result = true; + } + XCloseDisplay(display); + return result; + } + + return false; + } + + auto resize(unsigned width, unsigned height) -> void { + if(device.width >= width && device.height >= height) return; + device.width = max(width, device.width); + device.height = max(height, device.height); + + XShmDetach(device.display, &device.shminfo); + shmdt(device.shminfo.shmaddr); + shmctl(device.shminfo.shmid, IPC_RMID, nullptr); + XFree(device.image); + delete[] buffer; + + device.image = XvShmCreateImage(device.display, device.port, device.fourcc, 0, device.width, device.height, &device.shminfo); + + device.shminfo.shmid = shmget(IPC_PRIVATE, device.image->data_size, IPC_CREAT | 0777); + device.shminfo.shmaddr = device.image->data = (char*)shmat(device.shminfo.shmid, 0, 0); + device.shminfo.readOnly = false; + XShmAttach(device.display, &device.shminfo); + + buffer = new uint32_t[device.width * device.height]; + } + + auto lock(uint32_t*& data, unsigned& pitch, unsigned width, unsigned height) -> bool { + if(width != settings.width || height != settings.height) { + resize(settings.width = width, settings.height = height); + } + + pitch = device.width * 4; + return data = buffer; + } + + auto unlock() -> void { + } + + auto clear() -> void { + memory::fill(buffer, device.width * device.height * sizeof(uint32_t)); + //clear twice in case video is double buffered ... + refresh(); + refresh(); + } + + auto refresh() -> void { + unsigned width = settings.width; + unsigned height = settings.height; + + XWindowAttributes target; + XGetWindowAttributes(device.display, device.window, &target); + + //we must ensure that the child window is the same size as the parent window. + //unfortunately, we cannot hook the parent window resize event notification, + //as we did not create the parent window, nor have any knowledge of the toolkit used. + //therefore, query each window size and resize as needed. + XWindowAttributes parent; + XGetWindowAttributes(device.display, settings.handle, &parent); + if(target.width != parent.width || target.height != parent.height) { + XResizeWindow(device.display, device.window, parent.width, parent.height); + } + + //update target width and height attributes + XGetWindowAttributes(device.display, device.window, &target); + + switch(device.format) { + case XvFormatRGB32: renderRGB32(width, height); break; + case XvFormatRGB24: renderRGB24(width, height); break; + case XvFormatRGB16: renderRGB16(width, height); break; + case XvFormatRGB15: renderRGB15(width, height); break; + case XvFormatYUY2: renderYUY2 (width, height); break; + case XvFormatUYVY: renderUYVY (width, height); break; + } + + XvShmPutImage(device.display, device.port, device.window, device.gc, device.image, + 0, 0, width, height, + 0, 0, target.width, target.height, + true); + } + + auto init() -> bool { + device.display = XOpenDisplay(nullptr); + + if(!XShmQueryExtension(device.display)) { + fprintf(stderr, "VideoXv: XShm extension not found.\n"); + return false; + } + + //find an appropriate Xv port + device.port = -1; + XvAdaptorInfo* adaptor_info; + unsigned adaptor_count; + XvQueryAdaptors(device.display, DefaultRootWindow(device.display), &adaptor_count, &adaptor_info); + for(unsigned i = 0; i < adaptor_count; i++) { + //find adaptor that supports both input (memory->drawable) and image (drawable->screen) masks + if(adaptor_info[i].num_formats < 1) continue; + if(!(adaptor_info[i].type & XvInputMask)) continue; + if(!(adaptor_info[i].type & XvImageMask)) continue; + + device.port = adaptor_info[i].base_id; + device.depth = adaptor_info[i].formats->depth; + device.visualid = adaptor_info[i].formats->visual_id; + break; + } + XvFreeAdaptorInfo(adaptor_info); + if(device.port < 0) { + fprintf(stderr, "VideoXv: failed to find valid XvPort.\n"); + return false; + } + + //create child window to attach to parent window. + //this is so that even if parent window visual depth doesn't match Xv visual + //(common with composited windows), Xv can still render to child window. + XWindowAttributes window_attributes; + XGetWindowAttributes(device.display, settings.handle, &window_attributes); + + XVisualInfo visualtemplate; + visualtemplate.visualid = device.visualid; + visualtemplate.screen = DefaultScreen(device.display); + visualtemplate.depth = device.depth; + visualtemplate.visual = 0; + signed visualmatches = 0; + XVisualInfo* visualinfo = XGetVisualInfo(device.display, VisualIDMask | VisualScreenMask | VisualDepthMask, &visualtemplate, &visualmatches); + if(visualmatches < 1 || !visualinfo->visual) { + if(visualinfo) XFree(visualinfo); + fprintf(stderr, "VideoXv: unable to find Xv-compatible visual.\n"); + return false; + } + + device.colormap = XCreateColormap(device.display, settings.handle, visualinfo->visual, AllocNone); + XSetWindowAttributes attributes; + attributes.colormap = device.colormap; + attributes.border_pixel = 0; + attributes.event_mask = StructureNotifyMask; + device.window = XCreateWindow(device.display, /* parent = */ settings.handle, + /* x = */ 0, /* y = */ 0, window_attributes.width, window_attributes.height, + /* border_width = */ 0, device.depth, InputOutput, visualinfo->visual, + CWColormap | CWBorderPixel | CWEventMask, &attributes); + XFree(visualinfo); + XSetWindowBackground(device.display, device.window, /* color = */ 0); + XMapWindow(device.display, device.window); + + device.gc = XCreateGC(device.display, device.window, 0, 0); + + //set colorkey to auto paint, so that Xv video output is always visible + Atom atom = XInternAtom(device.display, "XV_AUTOPAINT_COLORKEY", true); + if(atom != None) XvSetPortAttribute(device.display, device.port, atom, 1); + + //find optimal rendering format + device.format = XvFormatUnknown; + signed format_count; + XvImageFormatValues* format = XvListImageFormats(device.display, device.port, &format_count); + + if(device.format == XvFormatUnknown) for(signed i = 0; i < format_count; i++) { + if(format[i].type == XvRGB && format[i].bits_per_pixel == 32) { + device.format = XvFormatRGB32; + device.fourcc = format[i].id; + break; + } + } + + if(device.format == XvFormatUnknown) for(signed i = 0; i < format_count; i++) { + if(format[i].type == XvRGB && format[i].bits_per_pixel == 24) { + device.format = XvFormatRGB24; + device.fourcc = format[i].id; + break; + } + } + + if(device.format == XvFormatUnknown) for(signed i = 0; i < format_count; i++) { + if(format[i].type == XvRGB && format[i].bits_per_pixel <= 16 && format[i].red_mask == 0xf800) { + device.format = XvFormatRGB16; + device.fourcc = format[i].id; + break; + } + } + + if(device.format == XvFormatUnknown) for(signed i = 0; i < format_count; i++) { + if(format[i].type == XvRGB && format[i].bits_per_pixel <= 16 && format[i].red_mask == 0x7c00) { + device.format = XvFormatRGB15; + device.fourcc = format[i].id; + break; + } + } + + if(device.format == XvFormatUnknown) for(signed i = 0; i < format_count; i++) { + if(format[i].type == XvYUV && format[i].bits_per_pixel == 16 && format[i].format == XvPacked) { + if(format[i].component_order[0] == 'Y' && format[i].component_order[1] == 'U' + && format[i].component_order[2] == 'Y' && format[i].component_order[3] == 'V' + ) { + device.format = XvFormatYUY2; + device.fourcc = format[i].id; + break; + } + } + } + + if(device.format == XvFormatUnknown) for(signed i = 0; i < format_count; i++) { + if(format[i].type == XvYUV && format[i].bits_per_pixel == 16 && format[i].format == XvPacked) { + if(format[i].component_order[0] == 'U' && format[i].component_order[1] == 'Y' + && format[i].component_order[2] == 'V' && format[i].component_order[3] == 'Y' + ) { + device.format = XvFormatUYVY; + device.fourcc = format[i].id; + break; + } + } + } + + free(format); + if(device.format == XvFormatUnknown) { + fprintf(stderr, "VideoXv: unable to find a supported image format.\n"); + return false; + } + + device.width = 256; + device.height = 256; + + device.image = XvShmCreateImage(device.display, device.port, device.fourcc, 0, device.width, device.height, &device.shminfo); + if(!device.image) { + fprintf(stderr, "VideoXv: XShmCreateImage failed.\n"); + return false; + } + + device.shminfo.shmid = shmget(IPC_PRIVATE, device.image->data_size, IPC_CREAT | 0777); + device.shminfo.shmaddr = device.image->data = (char*)shmat(device.shminfo.shmid, 0, 0); + device.shminfo.readOnly = false; + if(!XShmAttach(device.display, &device.shminfo)) { + fprintf(stderr, "VideoXv: XShmAttach failed.\n"); + return false; + } + + buffer = new uint32_t[device.width * device.height]; + settings.width = 256; + settings.height = 256; + initTables(); + clear(); + return true; + } + + auto term() -> void { + XShmDetach(device.display, &device.shminfo); + shmdt(device.shminfo.shmaddr); + shmctl(device.shminfo.shmid, IPC_RMID, nullptr); + XFree(device.image); + + if(device.window) { + XUnmapWindow(device.display, device.window); + device.window = 0; + } + + if(device.colormap) { + XFreeColormap(device.display, device.colormap); + device.colormap = 0; + } + + if(device.display) { + XCloseDisplay(device.display); + device.display = nullptr; + } + + if(buffer) { delete[] buffer; buffer = nullptr; } + if(ytable) { delete[] ytable; ytable = nullptr; } + if(utable) { delete[] utable; utable = nullptr; } + if(vtable) { delete[] vtable; vtable = nullptr; } + } + +private: + auto renderRGB32(unsigned width, unsigned height) -> void { + uint32_t* input = (uint32_t*)buffer; + uint32_t* output = (uint32_t*)device.image->data; + + for(unsigned y = 0; y < height; y++) { + memcpy(output, input, width * 4); + input += device.width; + output += device.width; + } + } + + auto renderRGB24(unsigned width, unsigned height) -> void { + uint32_t* input = (uint32_t*)buffer; + uint8_t* output = (uint8_t*)device.image->data; + + for(unsigned y = 0; y < height; y++) { + for(unsigned x = 0; x < width; x++) { + uint32_t p = *input++; + *output++ = p; + *output++ = p >> 8; + *output++ = p >> 16; + } + + input += (device.width - width); + output += (device.width - width) * 3; + } + } + + auto renderRGB16(unsigned width, unsigned height) -> void { + uint32_t* input = (uint32_t*)buffer; + uint16_t* output = (uint16_t*)device.image->data; + + for(unsigned y = 0; y < height; y++) { + for(unsigned x = 0; x < width; x++) { + uint32_t p = *input++; + *output++ = ((p >> 8) & 0xf800) | ((p >> 5) & 0x07e0) | ((p >> 3) & 0x001f); //RGB32->RGB16 + } + + input += device.width - width; + output += device.width - width; + } + } + + auto renderRGB15(unsigned width, unsigned height) -> void { + uint32_t* input = (uint32_t*)buffer; + uint16_t* output = (uint16_t*)device.image->data; + + for(unsigned y = 0; y < height; y++) { + for(unsigned x = 0; x < width; x++) { + uint32_t p = *input++; + *output++ = ((p >> 9) & 0x7c00) | ((p >> 6) & 0x03e0) | ((p >> 3) & 0x001f); //RGB32->RGB15 + } + + input += device.width - width; + output += device.width - width; + } + } + + auto renderYUY2(unsigned width, unsigned height) -> void { + uint32_t* input = (uint32_t*)buffer; + uint16_t* output = (uint16_t*)device.image->data; + + for(unsigned y = 0; y < height; y++) { + for(unsigned x = 0; x < width >> 1; x++) { + uint32_t p0 = *input++; + uint32_t p1 = *input++; + p0 = ((p0 >> 8) & 0xf800) + ((p0 >> 5) & 0x07e0) + ((p0 >> 3) & 0x001f); //RGB32->RGB16 + p1 = ((p1 >> 8) & 0xf800) + ((p1 >> 5) & 0x07e0) + ((p1 >> 3) & 0x001f); //RGB32->RGB16 + + uint8_t u = (utable[p0] + utable[p1]) >> 1; + uint8_t v = (vtable[p0] + vtable[p1]) >> 1; + + *output++ = (u << 8) | ytable[p0]; + *output++ = (v << 8) | ytable[p1]; + } + + input += device.width - width; + output += device.width - width; + } + } + + auto renderUYVY(unsigned width, unsigned height) -> void { + uint32_t* input = (uint32_t*)buffer; + uint16_t* output = (uint16_t*)device.image->data; + + for(unsigned y = 0; y < height; y++) { + for(unsigned x = 0; x < width >> 1; x++) { + uint32_t p0 = *input++; + uint32_t p1 = *input++; + p0 = ((p0 >> 8) & 0xf800) + ((p0 >> 5) & 0x07e0) + ((p0 >> 3) & 0x001f); + p1 = ((p1 >> 8) & 0xf800) + ((p1 >> 5) & 0x07e0) + ((p1 >> 3) & 0x001f); + + uint8_t u = (utable[p0] + utable[p1]) >> 1; + uint8_t v = (vtable[p0] + vtable[p1]) >> 1; + + *output++ = (ytable[p0] << 8) | u; + *output++ = (ytable[p1] << 8) | v; + } + + input += device.width - width; + output += device.width - width; + } + } + + auto initTables() -> void { + ytable = new uint8_t[65536]; + utable = new uint8_t[65536]; + vtable = new uint8_t[65536]; + + for(unsigned i = 0; i < 65536; i++) { + //extract RGB565 color data from i + uint8_t r = (i >> 11) & 31, g = (i >> 5) & 63, b = (i) & 31; + r = (r << 3) | (r >> 2); //R5->R8 + g = (g << 2) | (g >> 4); //G6->G8 + b = (b << 3) | (b >> 2); //B5->B8 + + //ITU-R Recommendation BT.601 + //double lr = 0.299, lg = 0.587, lb = 0.114; + int y = int( +(double(r) * 0.257) + (double(g) * 0.504) + (double(b) * 0.098) + 16.0 ); + int u = int( -(double(r) * 0.148) - (double(g) * 0.291) + (double(b) * 0.439) + 128.0 ); + int v = int( +(double(r) * 0.439) - (double(g) * 0.368) - (double(b) * 0.071) + 128.0 ); + + //ITU-R Recommendation BT.709 + //double lr = 0.2126, lg = 0.7152, lb = 0.0722; + //int y = int( double(r) * lr + double(g) * lg + double(b) * lb ); + //int u = int( (double(b) - y) / (2.0 - 2.0 * lb) + 128.0 ); + //int v = int( (double(r) - y) / (2.0 - 2.0 * lr) + 128.0 ); + + ytable[i] = y < 0 ? 0 : y > 255 ? 255 : y; + utable[i] = u < 0 ? 0 : u > 255 ? 255 : u; + vtable[i] = v < 0 ? 0 : v > 255 ? 255 : v; + } + } +};