diff --git a/01_HelloCoreSystemAsset/main.cpp b/01_HelloCoreSystemAsset/main.cpp index 7ca4badb4..2ec216c93 100644 --- a/01_HelloCoreSystemAsset/main.cpp +++ b/01_HelloCoreSystemAsset/main.cpp @@ -82,7 +82,11 @@ int main(int argc, char** argv) } else { + #ifdef _NBL_PLATFORM_WINDOWS_ logger = make_smart_refctd_ptr(LogLevel); + #else + logger = make_smart_refctd_ptr(LogLevel); + #endif logger->log("Could not create \"%s\\log.txt\" logging to STDOUT instead!\n",ILogger::ELL_ERROR,CWD.string().c_str()); } } diff --git a/08_HelloSwapchain/main.cpp b/08_HelloSwapchain/main.cpp index d2bc076a5..045e05fa7 100644 --- a/08_HelloSwapchain/main.cpp +++ b/08_HelloSwapchain/main.cpp @@ -160,7 +160,12 @@ class HelloSwapchainApp final : public examples::SimpleWindowedApplication auto window = m_winMgr->createWindow(std::move(params)); // uncomment for some nasty testing of swapchain creation! //m_winMgr->minimize(window.get()); - const_cast&>(m_surface) = CSmoothResizeSurface::create(CSurfaceVulkanWin32::create(smart_refctd_ptr(m_api),move_and_static_cast(window))); + #ifdef _NBL_PLATFORM_WINDOWS_ + auto surface = CSurfaceVulkanWin32::create(smart_refctd_ptr(m_api),move_and_static_cast(window)); + #elif defined(_NBL_PLATFORM_LINUX_) + auto surface = CSurfaceVulkanXcb::create(smart_refctd_ptr(m_api),move_and_static_cast(window)); + #endif + const_cast&>(m_surface) = CSmoothResizeSurface::create(std::move(surface)); } return {{m_surface->getSurface()/*,EQF_NONE*/}}; } diff --git a/09_GeometryCreator/main.cpp b/09_GeometryCreator/main.cpp index 56f850e5f..d17407a17 100644 --- a/09_GeometryCreator/main.cpp +++ b/09_GeometryCreator/main.cpp @@ -278,7 +278,7 @@ class GeometryCreatorApp final : public MonoWindowApplication, public BuiltinRes if (ev.type==nbl::ui::SMouseEvent::EET_SCROLL && m_renderer) { gcIndex += int16_t(core::sign(ev.scrollEvent.verticalScroll)); - gcIndex = core::clamp(gcIndex,0ull,m_renderer->getGeometries().size()-1); + gcIndex = core::clamp(gcIndex,size_t(0),m_renderer->getGeometries().size()-1); } } } diff --git a/23_Arithmetic2UnitTest/main.cpp b/23_Arithmetic2UnitTest/main.cpp index 2fa978953..a42bcaa9d 100644 --- a/23_Arithmetic2UnitTest/main.cpp +++ b/23_Arithmetic2UnitTest/main.cpp @@ -181,6 +181,9 @@ class Workgroup2ScanTestApp final : public application_templates::BasicMultiQueu const auto MaxWorkgroupSize = m_physicalDevice->getLimits().maxComputeWorkGroupInvocations; const auto MinSubgroupSize = m_physicalDevice->getLimits().minSubgroupSize; const auto MaxSubgroupSize = m_physicalDevice->getLimits().maxSubgroupSize; + // a required subgroup size caps the workgroup at `requiredSubgroupSize*maxComputeWorkgroupSubgroups` (VUID-VkPipelineShaderStageCreateInfo-pNext-02756), + // Intel ANV reports 64 subgroups so SIMD8 can't go past 512 invocations and the GPU hangs + const auto MaxWorkgroupSubgroups = m_physicalDevice->getLimits().maxComputeWorkgroupSubgroups; for (uint32_t useNative = 0; useNative <= uint32_t(m_physicalDevice->getProperties().limits.shaderSubgroupArithmetic); useNative++) { if (useNative) @@ -191,7 +194,7 @@ class Workgroup2ScanTestApp final : public application_templates::BasicMultiQueu for (auto subgroupSize = MinSubgroupSize; subgroupSize <= MaxSubgroupSize; subgroupSize *= 2u) { const uint8_t subgroupSizeLog2 = hlsl::findMSB(subgroupSize); - for (uint32_t workgroupSize = subgroupSize; workgroupSize <= MaxWorkgroupSize; workgroupSize *= 2u) + for (uint32_t workgroupSize = subgroupSize; workgroupSize <= hlsl::min(MaxWorkgroupSize,subgroupSize*MaxWorkgroupSubgroups); workgroupSize *= 2u) { // make sure renderdoc captures everything for debugging m_api->startCapture(); diff --git a/70_FLIPFluids/README.md b/70_FLIPFluids/README.md index 5dc4e9fe4..9f6b09c3a 100644 --- a/70_FLIPFluids/README.md +++ b/70_FLIPFluids/README.md @@ -1 +1,10 @@ https://github.com/user-attachments/assets/fc8bfac9-c8fb-49f1-87d6-251aae015945 + +## Running on Linux (X11) + +```bash +cmake --build build/linux-clang-release -j4 --target 70_flipfluids +(cd examples_tests/70_FLIPFluids/bin && DISPLAY=:1 ./70_flipfluids) +``` + +XCB windows don't deliver mouse or keyboard input yet, so when no input device is connected the camera orbits the tank on its own and the water is dropped again every 12 seconds. Stop it with Ctrl+C. diff --git a/70_FLIPFluids/main.cpp b/70_FLIPFluids/main.cpp index 714b9d6b4..fa952eef4 100644 --- a/70_FLIPFluids/main.cpp +++ b/70_FLIPFluids/main.cpp @@ -44,132 +44,6 @@ struct SMVPParams float P[4*4]; }; -class CSwapchainFramebuffersAndDepth final : public nbl::video::CDefaultSwapchainFramebuffers -{ - using scbase_t = CDefaultSwapchainFramebuffers; - -public: - template - inline CSwapchainFramebuffersAndDepth(ILogicalDevice* device, const asset::E_FORMAT _desiredDepthFormat, Args&&... args) - : CDefaultSwapchainFramebuffers(device, std::forward(args)...) - { - const IPhysicalDevice::SImageFormatPromotionRequest req = { - .originalFormat = _desiredDepthFormat, - .usages = {IGPUImage::EUF_RENDER_ATTACHMENT_BIT} - }; - m_depthFormat = m_device->getPhysicalDevice()->promoteImageFormat(req, IGPUImage::TILING::OPTIMAL); - - const static IGPURenderpass::SCreationParams::SDepthStencilAttachmentDescription depthAttachments[] = { - {{ - { - .format = m_depthFormat, - .samples = IGPUImage::ESCF_1_BIT, - .mayAlias = false - }, - /*.loadOp = */{IGPURenderpass::LOAD_OP::CLEAR}, - /*.storeOp = */{IGPURenderpass::STORE_OP::STORE}, - /*.initialLayout = */{IGPUImage::LAYOUT::UNDEFINED}, // because we clear we don't care about contents - /*.finalLayout = */{IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} // transition to presentation right away so we can skip a barrier - }}, - IGPURenderpass::SCreationParams::DepthStencilAttachmentsEnd - }; - m_params.depthStencilAttachments = depthAttachments; - - static IGPURenderpass::SCreationParams::SSubpassDescription subpasses[] = { - m_params.subpasses[0], - IGPURenderpass::SCreationParams::SubpassesEnd - }; - subpasses[0].depthStencilAttachment.render = { .attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL }; - m_params.subpasses = subpasses; - - // TODO: Two subpass external dependencies SRC and DST needed! - } - -protected: - inline bool onCreateSwapchain_impl(const uint8_t qFam) override - { - auto device = const_cast(m_renderpass->getOriginDevice()); - - const auto depthFormat = m_renderpass->getCreationParameters().depthStencilAttachments[0].format; - const auto& sharedParams = getSwapchain()->getCreationParameters().sharedParams; - auto image = device->createImage({ IImage::SCreationParams{ - .type = IGPUImage::ET_2D, - .samples = IGPUImage::ESCF_1_BIT, - .format = depthFormat, - .extent = {sharedParams.width,sharedParams.height,1}, - .mipLevels = 1, - .arrayLayers = 1, - .depthUsage = IGPUImage::EUF_RENDER_ATTACHMENT_BIT - } }); - - device->allocate(image->getMemoryReqs(), { image.get() }); - - m_depthBuffer = device->createImageView({ - .flags = IGPUImageView::ECF_NONE, - .subUsages = IGPUImage::EUF_RENDER_ATTACHMENT_BIT, - .image = std::move(image), - .viewType = IGPUImageView::ET_2D, - .format = depthFormat, - .subresourceRange = {IGPUImage::EAF_DEPTH_BIT,0,1,0,1} - }); - - const auto retval = scbase_t::onCreateSwapchain_impl(qFam); - m_depthBuffer = nullptr; - return retval; - } - - inline smart_refctd_ptr createFramebuffer(IGPUFramebuffer::SCreationParams&& params) override - { - params.depthStencilAttachments = &m_depthBuffer.get(); - return m_device->createFramebuffer(std::move(params)); - } - - E_FORMAT m_depthFormat; - smart_refctd_ptr m_depthBuffer; -}; - -class CEventCallback : public ISimpleManagedSurface::ICallback -{ -public: - CEventCallback(nbl::core::smart_refctd_ptr&& m_inputSystem, nbl::system::logger_opt_smart_ptr&& logger) : m_inputSystem(std::move(m_inputSystem)), m_logger(std::move(logger)) {} - CEventCallback() {} - - void setLogger(nbl::system::logger_opt_smart_ptr& logger) - { - m_logger = logger; - } - void setInputSystem(nbl::core::smart_refctd_ptr&& m_inputSystem) - { - m_inputSystem = std::move(m_inputSystem); - } -private: - - void onMouseConnected_impl(nbl::core::smart_refctd_ptr&& mch) override - { - m_logger.log("A mouse %p has been connected", nbl::system::ILogger::ELL_INFO, mch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_mouse, std::move(mch)); - } - void onMouseDisconnected_impl(nbl::ui::IMouseEventChannel* mch) override - { - m_logger.log("A mouse %p has been disconnected", nbl::system::ILogger::ELL_INFO, mch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_mouse, mch); - } - void onKeyboardConnected_impl(nbl::core::smart_refctd_ptr&& kbch) override - { - m_logger.log("A keyboard %p has been connected", nbl::system::ILogger::ELL_INFO, kbch.get()); - m_inputSystem.get()->add(m_inputSystem.get()->m_keyboard, std::move(kbch)); - } - void onKeyboardDisconnected_impl(nbl::ui::IKeyboardEventChannel* kbch) override - { - m_logger.log("A keyboard %p has been disconnected", nbl::system::ILogger::ELL_INFO, kbch); - m_inputSystem.get()->remove(m_inputSystem.get()->m_keyboard, kbch); - } - -private: - nbl::core::smart_refctd_ptr m_inputSystem = nullptr; - nbl::system::logger_opt_smart_ptr m_logger = nullptr; -}; - class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinResourcesApplication { using device_base_t = SimpleWindowedApplication; @@ -211,7 +85,11 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso const_cast&>(m_window) = m_winMgr->createWindow(std::move(params)); } + #ifdef _NBL_PLATFORM_WINDOWS_ auto surface = CSurfaceVulkanWin32::create(smart_refctd_ptr(m_api), smart_refctd_ptr_static_cast(m_window)); + #elif defined(_NBL_PLATFORM_LINUX_) + auto surface = CSurfaceVulkanXcb::create(smart_refctd_ptr(m_api), smart_refctd_ptr_static_cast(m_window)); + #endif const_cast&>(m_surface) = nbl::video::CSimpleResizeSurface::create(std::move(surface)); } @@ -295,7 +173,7 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso params.size = numParticles * 6 * sizeof(VertexInfo); createBuffer(particleVertexBuffer, params, IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); - asset::VkExtent3D gridExtent = { m_gridData.gridSize.x, m_gridData.gridSize.y, m_gridData.gridSize.z }; + asset::VkExtent3D gridExtent = { uint32_t(m_gridData.gridSize.x), uint32_t(m_gridData.gridSize.y), uint32_t(m_gridData.gridSize.z) }; // cell materials createGridTexture(gridCellMaterialImageView, asset::EF_R16_UINT, gridExtent, asset::IImage::EUF_STORAGE_BIT, "cell material0"); @@ -908,8 +786,9 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso const auto resourceIx = m_realFrameIx % MaxFramesInFlight; - m_inputSystem->getDefaultMouse(&mouse); - m_inputSystem->getDefaultKeyboard(&keyboard); + // `getDefault*` blocks until a device connects and windows without input (XCB for now) never get one, + // the `tryGetDefault*` variants check and bind under one lock, so a disconnect falls back to autoplay + const bool hasInput = m_inputSystem->tryGetDefaultMouse(&mouse) && m_inputSystem->tryGetDefaultKeyboard(&keyboard); auto updatePresentationTimestamp = [&]() { @@ -931,6 +810,9 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso cmdbuf->reset(IGPUCommandBuffer::RESET_FLAGS::RELEASE_RESOURCES_BIT); cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); cmdbuf->beginDebugMarker("Frame Debug FLIP sim begin"); + if (!hasInput) + autoplay(); + else { std::vector cameraMouseEvents; std::vector cameraKeyboardEvents; @@ -1554,7 +1436,7 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso return logFail("Failed to create render semaphore!\n"); ISwapchain::SCreationParams swapchainParams{ - .surface = m_surface->getSurface() + .surface = smart_refctd_ptr(m_surface->getSurface()) }; if (!swapchainParams.deduceFormat(m_physicalDevice)) return logFail("Could not choose a surface format for the swapchain!\n"); @@ -1711,6 +1593,27 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso } } + // Without input devices, orbit the camera around the tank and drop the water again periodically + void autoplay() + { + const auto now = clock_t::now(); + if (m_autoplayStart == clock_t::time_point{}) + m_autoplayStart = now; + if (now - m_lastDropTime > AutoplayDropPeriod) + { + m_lastDropTime = now; + m_shouldInitParticles = true; + } + + const double t = std::chrono::duration(now - m_autoplayStart).count(); + const double angle = t * 2.0 * core::PI() / AutoplayOrbitSeconds; + const auto center = hlsl::float64_t3(m_gridData.worldMax.x, m_gridData.worldMax.y * 0.5f, m_gridData.worldMax.z) * 0.5; + const auto eye = center + hlsl::float64_t3(std::cos(angle) * 13.0, 6.0, std::sin(angle) * 13.0); + hlsl::math::quaternion orientation; + if (ext::cameras::CCameraMathUtilities::tryCreateQuaternionFromLookAt(eye, center, hlsl::float64_t3(0.0, 1.0, 0.0), orientation)) + camera = core::make_smart_refctd_ptr(eye, orientation); + } + // in-loop functions void initializeParticles(IGPUCommandBuffer* cmdbuf) @@ -1861,6 +1764,10 @@ class FLIPFluidsApp final : public SimpleWindowedApplication, public BuiltinReso InputSystem::ChannelReader keyboard; core::smart_refctd_ptr camera; + constexpr static inline clock_t::duration AutoplayDropPeriod = std::chrono::seconds(12); + constexpr static inline double AutoplayOrbitSeconds = 40.0; + clock_t::time_point m_autoplayStart = {}; + clock_t::time_point m_lastDropTime = clock_t::now(); ext::cameras::CCameraMouseKeyboardController cameraController; hlsl::float32_t4x4 cameraProjection = hlsl::float32_t4x4(1.0f); video::CDumbPresentationOracle oracle; diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 65d2b0ced..e9b764666 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -108,3 +108,5 @@ set(NBL_EXAMPLES_API_TARGET ${LIB_NAME} PARENT_SCOPE) set(NBL_EXAMPLES_API_LIBRARIES ${TARGETS} PARENT_SCOPE) NBL_ADJUST_FOLDERS(common) + +add_subdirectory(test) diff --git a/common/include/nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp b/common/include/nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp index 949026a3c..48eb4f851 100644 --- a/common/include/nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp +++ b/common/include/nbl/examples/common/CSwapchainFramebuffersAndDepth.hpp @@ -50,7 +50,7 @@ class CSwapchainFramebuffersAndDepth final : public video::CDefaultSwapchainFram m_params.subpasses[0], IGPURenderpass::SCreationParams::SubpassesEnd }; - subpasses[0].depthStencilAttachment.render = { .attachmentIndex = 0,.layout = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL }; + subpasses[0].depthStencilAttachment.render = { .attachmentIndex = 0,.layout = {.depth = IGPUImage::LAYOUT::ATTACHMENT_OPTIMAL} }; m_params.subpasses = subpasses; } @@ -66,7 +66,7 @@ class CSwapchainFramebuffersAndDepth final : public video::CDefaultSwapchainFram const auto depthFormat = m_renderpass->getCreationParameters().depthStencilAttachments[0].format; const auto& sharedParams = getSwapchain()->getCreationParameters().sharedParams; - auto image = device->createImage({ IImage::SCreationParams{ + auto image = device->createImage(IGPUImage::SCreationParams{ IImage::SCreationParams{ .type = IGPUImage::ET_2D, .samples = IGPUImage::ESCF_1_BIT, .format = depthFormat, @@ -78,7 +78,7 @@ class CSwapchainFramebuffersAndDepth final : public video::CDefaultSwapchainFram device->allocate(image->getMemoryReqs(), { image.get() }); - m_depthBuffer = device->createImageView({ + m_depthBuffer = device->createImageView(IGPUImageView::SCreationParams{ .flags = IGPUImageView::ECF_NONE, .subUsages = IGPUImage::EUF_RENDER_ATTACHMENT_BIT, .image = std::move(image), diff --git a/common/include/nbl/examples/common/InputSystem.hpp b/common/include/nbl/examples/common/InputSystem.hpp index f99d53932..dc8116aed 100644 --- a/common/include/nbl/examples/common/InputSystem.hpp +++ b/common/include/nbl/examples/common/InputSystem.hpp @@ -55,6 +55,15 @@ class InputSystem : public core::IReferenceCounted { getDefault(m_keyboard,reader); } + // Nonblocking variants, return false and leave the reader alone when no device is connected + bool tryGetDefaultMouse(ChannelReader* reader) + { + return tryGetDefault(m_mouse,reader); + } + bool tryGetDefaultKeyboard(ChannelReader* reader) + { + return tryGetDefault(m_keyboard,reader); + } template void add(Channels& channels, core::smart_refctd_ptr&& channel) { @@ -83,6 +92,35 @@ class InputSystem : public core::IReferenceCounted } template void getDefault(Channels& channels, ChannelReader* reader) + { + std::unique_lock lock(channels.lock); + while (channels.channels.empty()) + { + m_logger.log("Waiting For Input Device to be connected...",system::ILogger::ELL_INFO); + channels.added.wait(lock); + } + selectDefault(channels,reader); + } + // Checks for a device and binds the reader under the same lock, so a disconnect can't slip in between. + // The reader holds a reference, so a channel removed afterwards stays valid until the next selection. + template + bool tryGetDefault(Channels& channels, ChannelReader* reader) + { + std::unique_lock lock(channels.lock); + if (channels.channels.empty()) + return false; + selectDefault(channels,reader); + return true; + } + + system::logger_opt_smart_ptr m_logger; + Channels m_mouse; + Channels m_keyboard; + + private: + // `channels.lock` must be held and `channels.channels` must not be empty + template + void selectDefault(Channels& channels, ChannelReader* reader) { /* * TODO: Improve default device switching. @@ -93,13 +131,6 @@ class InputSystem : public core::IReferenceCounted * Switch the channel choice, but prune away all events younger than the old default's consumption timestamp. * (Basically switch keyboards but dont try to process events older than the events you've processed from the old keyboard) */ - - std::unique_lock lock(channels.lock); - while (channels.channels.empty()) - { - m_logger.log("Waiting For Input Device to be connected...",system::ILogger::ELL_INFO); - channels.added.wait(lock); - } using namespace std::chrono; constexpr long long DefaultChannelTimeoutInMicroSeconds = 100*1e3; // 100 mili-seconds @@ -179,10 +210,6 @@ class InputSystem : public core::IReferenceCounted // no `size - capacity` underflow on a channel younger than the ring. reader->consumedCounter = defaultChannel->getEvents().size(); } - - system::logger_opt_smart_ptr m_logger; - Channels m_mouse; - Channels m_keyboard; }; } diff --git a/common/include/nbl/examples/common/MonoWindowApplication.hpp b/common/include/nbl/examples/common/MonoWindowApplication.hpp index ab7cff307..9b5540976 100644 --- a/common/include/nbl/examples/common/MonoWindowApplication.hpp +++ b/common/include/nbl/examples/common/MonoWindowApplication.hpp @@ -47,7 +47,11 @@ class MonoWindowApplication : public virtual SimpleWindowedApplication const_cast&>(m_window) = m_winMgr->createWindow(std::move(params)); } + #ifdef _NBL_PLATFORM_WINDOWS_ auto surface = CSurfaceVulkanWin32::create(smart_refctd_ptr(m_api), smart_refctd_ptr_static_cast(m_window)); + #elif defined(_NBL_PLATFORM_LINUX_) + auto surface = CSurfaceVulkanXcb::create(smart_refctd_ptr(m_api), smart_refctd_ptr_static_cast(m_window)); + #endif const_cast&>(m_surface) = CSimpleResizeSurface::create(std::move(surface)); } diff --git a/common/include/nbl/examples/common/SimpleWindowedApplication.hpp b/common/include/nbl/examples/common/SimpleWindowedApplication.hpp index ddb510eb7..573a37772 100644 --- a/common/include/nbl/examples/common/SimpleWindowedApplication.hpp +++ b/common/include/nbl/examples/common/SimpleWindowedApplication.hpp @@ -63,8 +63,12 @@ class SimpleWindowedApplication : public virtual application_templates::BasicMul #ifdef _NBL_PLATFORM_WINDOWS_ m_winMgr = nbl::ui::IWindowManagerWin32::create(); + #elif defined(_NBL_PLATFORM_LINUX_) + m_winMgr = nbl::ui::IWindowManagerXcb::create(); + if (!m_winMgr) + m_logger->log("Could not connect to an X server, is DISPLAY set?",system::ILogger::ELL_ERROR); #else - #error "Unimplemented!" + m_logger->log("No window manager is implemented for this platform yet!",system::ILogger::ELL_ERROR); #endif if (!m_winMgr) return false; diff --git a/common/test/CInputSystemTest.cpp b/common/test/CInputSystemTest.cpp new file mode 100644 index 000000000..d45887b90 --- /dev/null +++ b/common/test/CInputSystemTest.cpp @@ -0,0 +1,164 @@ +// Regression test for `InputSystem::tryGetDefault*`: it must never block, whether devices are absent, connected, +// or disconnected while a frame is acquiring them. The blocking `getDefault*` must keep waiting for a device. +// Exit codes: 0 pass, 1 failed check, 2 timed out (blocked). +#include "nabla.h" +#include "nbl/examples/common/InputSystem.hpp" + +#include +#include +#include +#include +#include +#include + +using namespace nbl; +using namespace nbl::ui; +using namespace nbl::examples; + +namespace +{ +constexpr auto Timeout = std::chrono::seconds(10); +constexpr size_t ChannelCapacity = 64u; + +std::atomic_bool g_failed = false; +#define NBL_CHECK(COND) if (!(COND)) { std::fprintf(stderr,"FAILED: %s (%s:%d)\n",#COND,__FILE__,__LINE__); g_failed = true; } + +// Runs a case on its own thread, a case still running after `Timeout` is blocked and can't be joined +template +void runBounded(const char* name, F&& testCase) +{ + std::packaged_task task(std::forward(testCase)); + auto done = task.get_future(); + std::thread(std::move(task)).detach(); + if (done.wait_for(Timeout)!=std::future_status::ready) + { + std::fprintf(stderr,"TIMED OUT: %s blocked for more than %lld s\n",name,static_cast(Timeout.count())); + std::fflush(stderr); + std::_Exit(2); + } + done.get(); + std::printf("%s: done\n",name); +} + +auto makeMouse() { return core::make_smart_refctd_ptr(ChannelCapacity); } +auto makeKeyboard() { return core::make_smart_refctd_ptr(ChannelCapacity); } +auto makeInputSystem() { return core::make_smart_refctd_ptr(system::logger_opt_smart_ptr(nullptr)); } +} + +int main() +{ + runBounded("absent devices",[]() -> void + { + auto input = makeInputSystem(); + InputSystem::ChannelReader mouse; + InputSystem::ChannelReader keyboard; + NBL_CHECK(!input->tryGetDefaultMouse(&mouse)); + NBL_CHECK(!input->tryGetDefaultKeyboard(&keyboard)); + NBL_CHECK(!mouse.channel && !keyboard.channel); + }); + + runBounded("connected devices",[]() -> void + { + auto input = makeInputSystem(); + auto mouseChannel = makeMouse(); + auto keyboardChannel = makeKeyboard(); + input->add(input->m_mouse,core::smart_refctd_ptr(mouseChannel)); + input->add(input->m_keyboard,core::smart_refctd_ptr(keyboardChannel)); + + InputSystem::ChannelReader mouse; + InputSystem::ChannelReader keyboard; + NBL_CHECK(input->tryGetDefaultMouse(&mouse) && input->tryGetDefaultKeyboard(&keyboard)); + NBL_CHECK(mouse.channel==mouseChannel && keyboard.channel==keyboardChannel); + // a repeat selection of the same channel keeps the reader's position + mouse.consumedCounter = 7u; + NBL_CHECK(input->tryGetDefaultMouse(&mouse) && mouse.consumedCounter==7u); + }); + + runBounded("disconnect after acquisition",[]() -> void + { + auto input = makeInputSystem(); + input->add(input->m_mouse,makeMouse()); + InputSystem::ChannelReader mouse; + NBL_CHECK(input->tryGetDefaultMouse(&mouse)); + const IMouseEventChannel* const acquired = mouse.channel.get(); + + // the last mouse goes away, the next frame must fall back instead of waiting + input->remove(input->m_mouse,acquired); + NBL_CHECK(!input->tryGetDefaultMouse(&mouse)); + // the reader's reference keeps the removed channel alive and readable + NBL_CHECK(mouse.channel.get()==acquired && mouse.channel->getEvents().size()==0u); + + // reconnecting binds the new device + auto reconnected = makeMouse(); + input->add(input->m_mouse,core::smart_refctd_ptr(reconnected)); + NBL_CHECK(input->tryGetDefaultMouse(&mouse) && mouse.channel==reconnected); + }); + + // The NAB-16 interleaving: the event thread drops the last mouse while the frame acquires it, and nothing + // reconnects until that frame is done. The old check-then-`getDefault` sequence waits forever when it loses. + runBounded("disconnect during acquisition",[]() -> void + { + constexpr uint32_t Rounds = 20000u; + auto input = makeInputSystem(); + input->add(input->m_keyboard,makeKeyboard()); + + std::atomic_uint32_t go = 0u, removed = 0u; + std::atomic pending = nullptr; + std::thread eventThread([&]() -> void + { + for (uint32_t round=1u; round<=Rounds; round++) + { + while (go.load()!=round) + std::this_thread::yield(); + input->remove(input->m_mouse,pending.load()); + removed = round; + } + }); + + uint32_t withInput = 0u; + InputSystem::ChannelReader mouse; + InputSystem::ChannelReader keyboard; + for (uint32_t round=1u; round<=Rounds; round++) + { + auto channel = makeMouse(); + pending = channel.get(); + input->add(input->m_mouse,std::move(channel)); + go = round; + // sweep the interleaving, so the disconnect lands before, during and after the acquisition + for (volatile uint32_t spin=(round*2654435761u)%2048u; spin; spin--) {} + const bool hasInput = input->tryGetDefaultMouse(&mouse) && input->tryGetDefaultKeyboard(&keyboard); + if (hasInput) + { + withInput++; + NBL_CHECK(mouse.channel && keyboard.channel); + } + while (removed.load()!=round) + std::this_thread::yield(); + NBL_CHECK(!input->tryGetDefaultMouse(&mouse)); + } + eventThread.join(); + std::printf(" %u rounds, %u acquired before the disconnect\n",Rounds,withInput); + }); + + runBounded("blocking getDefault still waits",[]() -> void + { + auto input = makeInputSystem(); + auto mouseChannel = makeMouse(); + std::atomic_bool added = false; + std::thread connector([&]() -> void + { + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + added = true; + input->add(input->m_mouse,core::smart_refctd_ptr(mouseChannel)); + }); + InputSystem::ChannelReader mouse; + input->getDefaultMouse(&mouse); + NBL_CHECK(added && mouse.channel==mouseChannel); + connector.join(); + }); + + if (g_failed) + return 1; + std::printf("PASSED\n"); + return 0; +} diff --git a/common/test/CMakeLists.txt b/common/test/CMakeLists.txt new file mode 100644 index 000000000..d2b5bec47 --- /dev/null +++ b/common/test/CMakeLists.txt @@ -0,0 +1,19 @@ +# Regression test for `InputSystem::tryGetDefault*`, needs neither a GPU nor a display. +# Run with `ctest --test-dir /examples_tests/common/test`. +enable_testing() + +add_executable(nbl_examples_input_system_test CInputSystemTest.cpp) +target_link_libraries(nbl_examples_input_system_test PRIVATE Nabla) +target_compile_features(nbl_examples_input_system_test PRIVATE cxx_std_20) + +# examples_tests/CMakeLists.txt links every target under it to the examples API and reuses its PCH, +# so build with the same flags as the examples (see `nbl_create_executable_project`) +nbl_adjust_flags(TARGET nbl_examples_input_system_test MAP_RELEASE Release MAP_RELWITHDEBINFO RelWithDebInfo MAP_DEBUG Debug) +nbl_adjust_definitions() +if(NOT WIN32 AND NOT ANDROID AND CMAKE_POSITION_INDEPENDENT_CODE) + set_target_properties(nbl_examples_input_system_test PROPERTIES POSITION_INDEPENDENT_CODE OFF) + target_compile_options(nbl_examples_input_system_test PRIVATE -fPIC) +endif() + +add_test(NAME NBL_EXAMPLES_INPUT_SYSTEM COMMAND $) +set_tests_properties(NBL_EXAMPLES_INPUT_SYSTEM PROPERTIES TIMEOUT 60)