ctrl: fix rendering, erase Endooms, implement zooming and panning

This commit is contained in:
oxmox 2023-02-19 23:52:54 +01:00
parent cbe6f76920
commit 284e5d1b0e

View file

@ -8,6 +8,7 @@
#include <array> #include <array>
#include <cerrno> #include <cerrno>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <thread> #include <thread>
#include <type_traits> #include <type_traits>
#include <vector> #include <vector>
@ -20,22 +21,64 @@
#include "dp_common.h" #include "dp_common.h"
#include "dp_util.hpp" #include "dp_util.hpp"
#if DoomBytesPerPixel == 3
static const u32 DoomSdlTexturePixelFormat = SDL_PIXELFORMAT_RGB888;
#elif DoomBytesPerPixel == 4
static const u32 DoomSdlTexturePixelFormat = SDL_PIXELFORMAT_ARGB8888;
#else
#error Unhandled DoomBytesPerPixel value. Which SDL_PIXELFORMAT to use?
#endif
void dp_sdl_fatal(const char *const msg) void dp_sdl_fatal(const char *const msg)
{ {
log_fatal("%s: %s", msg, SDL_GetError()); log_fatal("%s: %s", msg, SDL_GetError());
abort(); abort();
} }
void dp_sdl_error(const char *const msg)
{
log_error("%s: %s", msg, SDL_GetError());
}
struct DoomState struct DoomState
{ {
// So easy to get this wrong and destroy the texture by accident. Cleanup is
// now done in check_on_dooms() before erasing the state. Enabling this code
// still works though.
#if 0
DoomState() = default;
~DoomState()
{
log_trace("~DoomState");
if (texture)
SDL_DestroyTexture(texture);
}
DoomState(const DoomState &) = delete;
DoomState &operator=(const DoomState &) = delete;
DoomState(DoomState &&o)
: DoomState()
{
*this = std::move(o);
}
DoomState &operator=(DoomState &&o)
{
std::swap(id, o.id);
std::swap(state, o.state);
std::swap(tLastActive, o.tLastActive);
std::swap(texture, o.texture);
return *this;
}
#endif
doomid_t id = 0; doomid_t id = 0;
DP_DoomState state = DP_DS_Unknown; DP_DoomState state = DP_DS_Unknown;
std::chrono::steady_clock::time_point tLastActive;
SDL_Texture *texture = nullptr; SDL_Texture *texture = nullptr;
}; };
static_assert(std::is_trivially_copyable<DoomState>::value,
"DoomState must be a trivially copyable type");
struct ControllerContext struct ControllerContext
{ {
nng_socket pub; nng_socket pub;
@ -47,6 +90,10 @@ struct ControllerContext
ExampleAppLog appLog; ExampleAppLog appLog;
int columns = 4; int columns = 4;
std::array<u8, DoomScreenWidth * DoomScreenHeight * DoomBytesPerPixel> pixelBuffer; std::array<u8, DoomScreenWidth * DoomScreenHeight * DoomBytesPerPixel> pixelBuffer;
float scaleFactor = 1.0;
s32 offsetX = 0;
s32 offsetY = 0;
bool isMousePanning = false;
}; };
struct ControllerActions struct ControllerActions
@ -59,29 +106,22 @@ struct ControllerActions
void spawn_doom_posix_spawn(ControllerContext &ctx) void spawn_doom_posix_spawn(ControllerContext &ctx)
{ {
DoomState ds; (void) ctx;
const char *const argv[] = { DOOM_EXECUTABLE, nullptr }; const char *const argv[] = { DOOM_EXECUTABLE, nullptr };
// TODO: Close stdin and stdout? Leave them open for now to see the logging // TODO: Close stdin and stdout? Leave them open for now to see the logging
// output. // output.
// FIXME: SDL is not able to init its sound system in the doomchild. Try to ask // FIXME: SDL is not able to init its sound system in the doomchild. Try
// it if it's already initialized and skip if true. // something similar to xdg-open where a doomgrandchild execvp()s the doom.
if (auto err = posix_spawn(&ds.id, DOOM_EXECUTABLE, nullptr, nullptr, pid_t pid;
if (auto err = posix_spawn(&pid, DOOM_EXECUTABLE, nullptr, nullptr,
const_cast<char *const *>(argv), nullptr)) const_cast<char *const *>(argv), nullptr))
{ {
log_error("Could not spawn doom: %s", strerror(err)); log_error("Could not spawn doom: %s", strerror(err));
return; return;
} }
ds.texture = SDL_CreateTexture(ctx.renderer, SDL_PIXELFORMAT_RGB888, log_info("Spawned new doom, pid=%d", pid);
SDL_TEXTUREACCESS_STREAMING, DoomScreenWidth, DoomScreenHeight);
if (!ds.texture)
dp_sdl_fatal("SDL_CreateTexture");
log_info("Spawned new doom, pid=%d", ds.id);
ctx.dooms.emplace_back(ds);
} }
inline void spawn_doom(ControllerContext &ctx) inline void spawn_doom(ControllerContext &ctx)
@ -105,15 +145,6 @@ void end_all_dooms(ControllerContext &ctx)
dp_nng_fatal("ctrl/sendmsg", res); dp_nng_fatal("ctrl/sendmsg", res);
} }
void signal_all_dooms(ControllerContext &ctx, int signum)
{
std::for_each(std::begin(ctx.dooms), std::end(ctx.dooms),
[signum] (const auto &ds)
{
kill(ds.id, signum);
});
}
void perform_actions(ControllerContext &ctx, const ControllerActions &actions) void perform_actions(ControllerContext &ctx, const ControllerActions &actions)
{ {
if (actions.doomsToSpawn) if (actions.doomsToSpawn)
@ -130,8 +161,12 @@ void perform_actions(ControllerContext &ctx, const ControllerActions &actions)
} }
} }
inline auto dp_now() { return std::chrono::steady_clock::now(); }
inline auto dp_elapsed(const std::chrono::steady_clock::time_point &tStart) { return tStart - dp_now(); }
void check_on_dooms(ControllerContext &ctx) void check_on_dooms(ControllerContext &ctx)
{ {
#if 0
pid_t pid = 0; pid_t pid = 0;
// This works for dooms forked from the controller, not for externally // This works for dooms forked from the controller, not for externally
@ -158,6 +193,18 @@ void check_on_dooms(ControllerContext &ctx)
} }
} }
} while (pid > 0); } while (pid > 0);
#else
const auto sz0 = ctx.dooms.size();
auto eb = std::remove_if(std::begin(ctx.dooms), std::end(ctx.dooms), [] (const auto &ds) { return ds.state == DP_DS_Endoom; });
if (eb != std::end(ctx.dooms))
{
auto count = std::distance(eb, std::end(ctx.dooms));
std::for_each(eb, std::end(ctx.dooms), [] (auto &ds) { SDL_DestroyTexture(ds.texture); ds.texture = nullptr; });
ctx.dooms.erase(eb, std::end(ctx.dooms));
const auto sz1 = ctx.dooms.size();
log_info("Erased %zu dooms which were in Endoom state. doomcount before=%zu, after=%zu", count, sz0, sz1);
}
#endif
} }
void do_networking(ControllerContext &ctx) void do_networking(ControllerContext &ctx)
@ -168,15 +215,12 @@ void do_networking(ControllerContext &ctx)
// Limit the max time we spend doing network stuff. // Limit the max time we spend doing network stuff.
static const auto MaxNetworkingTime = std::chrono::milliseconds(10); static const auto MaxNetworkingTime = std::chrono::milliseconds(10);
auto tStart = std::chrono::steady_clock::now(); auto tStart = dp_now();
while (true) while (true)
{ {
if (auto elapsed = std::chrono::steady_clock::now() - tStart; if (auto elapsed = dp_elapsed(tStart); elapsed >= MaxNetworkingTime)
elapsed >= MaxNetworkingTime)
{
break; break;
}
nng_msg *msg = nullptr; nng_msg *msg = nullptr;
@ -193,28 +237,52 @@ void do_networking(ControllerContext &ctx)
{ {
auto msgDoomState = DP_NNG_BODY_AS(msg, MsgDoomState); auto msgDoomState = DP_NNG_BODY_AS(msg, MsgDoomState);
// Check if we know this doom. If it was externally started register // Check if we know this doom and register it if we don't.
// it in ctx.dooms.
auto pid = msgDoomState->doomId; auto pid = msgDoomState->doomId;
auto dit = find_in_container(ctx.dooms, [pid] (const auto &ds) { return ds.id == pid; }); auto dit = find_in_container(ctx.dooms, [pid] (const auto &ds) { return ds.id == pid; });
if (dit != std::end(ctx.dooms)) if (dit != std::end(ctx.dooms))
{ {
dit->state = msgDoomState->doomState; dit->state = msgDoomState->doomState;
dit->tLastActive = dp_now();
} }
else else
{ {
DoomState ds; DoomState ds;
ds.id = pid; ds.id = pid;
ds.state = msgDoomState->doomState; ds.state = msgDoomState->doomState;
ds.texture = SDL_CreateTexture(ctx.renderer, SDL_PIXELFORMAT_RGB888, ds.tLastActive = dp_now();
ds.texture = SDL_CreateTexture(
ctx.renderer, DoomSdlTexturePixelFormat,
SDL_TEXTUREACCESS_STREAMING, DoomScreenWidth, DoomScreenHeight); SDL_TEXTUREACCESS_STREAMING, DoomScreenWidth, DoomScreenHeight);
if (!ds.texture) if (!ds.texture)
dp_sdl_fatal("SDL_CreateTexture"); dp_sdl_fatal("SDL_CreateTexture");
log_info("Registered external doom, pid=%d", ds.id); {
ctx.dooms.emplace_back(ds); u32 format = 0;
int access = 0;
int w = 0;
int h = 0;
if (SDL_QueryTexture(ds.texture, &format, &access, &w, &h))
dp_sdl_fatal("SDL_QueryTexture");
log_debug("texture info (wanted, got): format=(%u, %u), access=(%d, %d), w=(%d, %d), h=(%d, %d)",
DoomSdlTexturePixelFormat, format,
SDL_TEXTUREACCESS_STREAMING, access,
DoomScreenWidth, w,
DoomScreenHeight, h
);
assert(format == DoomSdlTexturePixelFormat);
assert(access == SDL_TEXTUREACCESS_STREAMING);
assert(w == DoomScreenWidth);
assert(h == DoomScreenHeight);
}
log_info("Registered new doom (pid=%d)", ds.id);
ctx.dooms.emplace_back(std::move(ds));
} }
if (msgDoomState->doomState == DP_DS_Ready) if (msgDoomState->doomState == DP_DS_Ready)
@ -229,10 +297,42 @@ void do_networking(ControllerContext &ctx)
if (dit != std::end(ctx.dooms)) if (dit != std::end(ctx.dooms))
{ {
auto &ds = *dit; auto &ds = *dit;
SDL_UpdateTexture(ds.texture, nullptr, msgDoomFrame->frame, DoomFramePitch); ds.tLastActive = dp_now();
const u8 *sourcePixels = msgDoomFrame->frame;
#if 0
// FIXME: buggy. black screen with tiny bar on top
//log_trace("Texture update for doom (pid=%d, texture=%p)", ds.id, ds.texture);
u8 *destPixels = nullptr;
int texturePitch = 0;
if (SDL_LockTexture(ds.texture, nullptr, reinterpret_cast<void **>(&destPixels), &texturePitch))
dp_sdl_fatal("SDL_LockTexture");
// When using 3 bytes per pixel (960 bytes per row), SDL yields
// a pitch of 1280 on my machine. This is likely done to get
// good alignment.
assert(DoomFramePitch <= texturePitch);
for (size_t row=0; row<DoomScreenHeight; ++row)
{
u8 *destRow = destPixels + row + texturePitch;
const u8 *sourceRow = sourcePixels + row * DoomFramePitch;
std::memcpy(destRow, sourceRow, DoomFramePitch);
}
SDL_UnlockTexture(ds.texture);
#else
// FIXME: buggy. sometimes crashes with DoomBytesPerPixel=3, hasn't crashed with DoomBytesPerPixel=4 yet.
// In the latter case SDL texture pitch equals DoomFramePitch...
if (SDL_UpdateTexture(ds.texture, nullptr, sourcePixels, DoomFramePitch))
dp_sdl_fatal("SDL_UpdateTexture");
#endif
} }
else else
log_warn("Received DoomFrame from unregistered doom, pid=%d", pid); log_trace("Received DoomFrame from unregistered doom (pid=%d)", pid);
} }
nng_msg_free(msg); nng_msg_free(msg);
@ -264,8 +364,7 @@ void final_cleanup(ControllerContext &ctx)
if (!ctx.dooms.empty()) if (!ctx.dooms.empty())
{ {
log_warn("final cleanup: terminating all %zu remaining dooms", ctx.dooms.size()); log_warn("final cleanup: %zu dooms remain", ctx.dooms.size());
signal_all_dooms(ctx, SIGTERM);
} }
} }
@ -375,6 +474,7 @@ void DrawRectangle(OffscreenBuffer *buffer,
void render_dooms(ControllerContext &ctx) void render_dooms(ControllerContext &ctx)
{ {
#if 0
OffscreenBuffer buffer = OffscreenBuffer buffer =
{ {
DoomScreenWidth, DoomScreenWidth,
@ -384,31 +484,31 @@ void render_dooms(ControllerContext &ctx)
DoomBytesPerPixel DoomBytesPerPixel
}; };
DrawRectangle(&buffer, 0, 0, 10, 10, { 1, 0, 0, 1 }); // top-left red DrawRectangle(&buffer, 0, 0, 10, 10, { 1, 0, 0, 1 });
DrawRectangle(&buffer, 0, buffer.height-10, 10, buffer.height, { 0, 1, 0, 1 }); DrawRectangle(&buffer, 0, buffer.height-10, 10, buffer.height, { 0, 1, 0, 1 });
DrawRectangle(&buffer, buffer.width-10, buffer.height-10, buffer.width, buffer.height, { 0, 0, 1, 1 }); DrawRectangle(&buffer, buffer.width-10, buffer.height-10, buffer.width, buffer.height, { 0, 0, 1, 1 });
DrawRectangle(&buffer, buffer.width-10, 0, buffer.width, 10, { 0.840, 0.0168, 0.717, 1 }); DrawRectangle(&buffer, buffer.width-10, 0, buffer.width, 10, { 0.840, 0.0168, 0.717, 1 });
#endif
//DrawRectangle(&buffer, buffer.width-10, 0, buffer.width, 10, { 0.0, 1.0, 0.0, 1.0 }); // top-right green SDL_Rect destRect = { ctx.offsetX, ctx.offsetY, DoomScreenWidth, DoomScreenHeight };
destRect.w *= ctx.scaleFactor;
destRect.h *= ctx.scaleFactor;
SDL_Rect destRect = {0, 0, buffer.width, buffer.height};
const size_t doomCount = ctx.dooms.size(); const size_t doomCount = ctx.dooms.size();
for (size_t i=0; i<doomCount; ++i) for (size_t i=0; i<doomCount; ++i)
{ {
if (i != 0 && i % ctx.columns == 0) if (i != 0 && i % ctx.columns == 0)
{ {
destRect.x = 0; destRect.x = ctx.offsetX;
destRect.y += buffer.height; destRect.y += destRect.h;
} }
auto &ds = ctx.dooms.at(i); auto &ds = ctx.dooms.at(i);
auto texture = ds.texture; auto texture = ds.texture;
assert(texture);
//SDL_UpdateTexture(texture, nullptr, buffer.pixels, buffer.width * buffer.BytesPerPixel);
SDL_RenderCopy(ctx.renderer, texture, nullptr, &destRect); SDL_RenderCopy(ctx.renderer, texture, nullptr, &destRect);
destRect.x += destRect.w;
destRect.x += buffer.width;
} }
} }
@ -483,15 +583,17 @@ ControllerActions run_ui(ControllerContext &ctx)
ImGui::PushItemWidth(ImGui::GetFontSize() * -16); // affects stuff like slider widths ImGui::PushItemWidth(ImGui::GetFontSize() * -16); // affects stuff like slider widths
ImGui::SliderInt("Layout columns##columns", &ctx.columns, 1, 32, "%d", ImGuiSliderFlags_AlwaysClamp); ImGui::SliderInt("Layout columns##columns", &ctx.columns, 1, 32, "%d", ImGuiSliderFlags_AlwaysClamp);
ImGui::SliderFloat("Doom scale", &ctx.scaleFactor, 0.1, 10, "%.3f", ImGuiSliderFlags_AlwaysClamp);
ImGui::SliderInt("##dooms", &doomsToSpawn, 1, 256, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic); ImGui::SliderInt("##dooms", &doomsToSpawn, 1, 256, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic);
aprintf(strbuf, "Spawn %d more doom%s###spawnmore", doomsToSpawn, doomsToSpawn > 1 ? "s" : ""); aprintf(strbuf, "Spawn %d more doom%s###spawnmore", doomsToSpawn, doomsToSpawn > 1 ? "s" : "");
if (ImGui::SameLine(); ImGui::Button(strbuf.data())) if (ImGui::SameLine(); ImGui::Button(strbuf.data()))
result.doomsToSpawn = doomsToSpawn; result.doomsToSpawn = doomsToSpawn;
if (ImGui::Button("End all Dooms")) if (ImGui::Button("End all dooms"))
result.endAllDooms = true; result.endAllDooms = true;
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::End(); ImGui::End();
@ -507,6 +609,9 @@ int doom_controller_loop(ControllerContext &ctx)
while (!ctx.quit) while (!ctx.quit)
{ {
SDL_Event event; SDL_Event event;
auto &io = ImGui::GetIO();
s32 mouseWheel = 0.0;
while (SDL_PollEvent(&event)) while (SDL_PollEvent(&event))
{ {
ImGui_ImplSDL2_ProcessEvent(&event); ImGui_ImplSDL2_ProcessEvent(&event);
@ -520,18 +625,69 @@ int doom_controller_loop(ControllerContext &ctx)
{ {
ctx.quit = true; ctx.quit = true;
} }
if (event.type == SDL_MOUSEWHEEL)
mouseWheel += event.wheel.y;
} }
// Process input events not consumed by ImGui // Process input events not consumed by ImGui
if (auto &io = ImGui::GetIO(); if (!io.WantCaptureKeyboard)
!io.WantCaptureKeyboard)
{ {
// TODO: make scaling scale with the current scale factor
static const float ScaleStep = 0.01f;
s32 PanStep = io.KeyCtrl ? 10 : 2;
if (io.KeyCtrl && ImGui::IsKeyDown(ImGuiKey_Q)) if (io.KeyCtrl && ImGui::IsKeyDown(ImGuiKey_Q))
{
ctx.quit = true; ctx.quit = true;
if (ImGui::IsKeyDown(ImGuiKey_Equal))
ctx.scaleFactor += ScaleStep;
if (ImGui::IsKeyDown(ImGuiKey_Minus))
ctx.scaleFactor -= ScaleStep;
if (ImGui::IsKeyDown(ImGuiKey_0))
ctx.scaleFactor = 1.0;
if (ImGui::IsKeyDown(ImGuiKey_H))
ctx.offsetX -= PanStep;
if (ImGui::IsKeyDown(ImGuiKey_J))
ctx.offsetY += PanStep;
if (ImGui::IsKeyDown(ImGuiKey_K))
ctx.offsetY -= PanStep;
if (ImGui::IsKeyDown(ImGuiKey_L))
ctx.offsetX += PanStep;
if (ImGui::IsKeyDown(ImGuiKey_G))
ctx.offsetX = ctx.offsetY = 0;
}
static ImVec2 panStartPos;
if (!io.WantCaptureMouse)
{
ctx.scaleFactor += mouseWheel * 0.05;
if (ImGui::IsKeyPressed(ImGuiKey_MouseLeft, false))
{
panStartPos = io.MousePos;
ctx.isMousePanning = true;
} }
} }
if (ImGui::IsKeyReleased(ImGuiKey_MouseLeft))
ctx.isMousePanning = false;
if (ctx.isMousePanning)
{
auto curPos = io.MousePos;
auto dx = curPos.x - panStartPos.x;
auto dy = curPos.y - panStartPos.y;
ctx.offsetX += dx;
ctx.offsetY += dy;
panStartPos = curPos;
}
ctx.scaleFactor = std::clamp(ctx.scaleFactor, 0.1f, 10.0f);
perform_actions(ctx, actions); perform_actions(ctx, actions);
check_on_dooms(ctx); check_on_dooms(ctx);
do_networking(ctx); do_networking(ctx);
@ -603,7 +759,10 @@ int main(int argc, char *argv[])
ImGui_ImplSDL2_InitForSDLRenderer(window, renderer); ImGui_ImplSDL2_InitForSDLRenderer(window, renderer);
ImGui_ImplSDLRenderer_Init(renderer); ImGui_ImplSDLRenderer_Init(renderer);
dp_nng_init_limits(1, 1, 1); // int ncpu_max, int pool_thread_limit_max, int resolv_thread_limit //dp_nng_init_limits(1, 1, 1); // int ncpu_max, int pool_thread_limit_max, int resolv_thread_limit
//nng_set_ncpu_max(ncpu_max);
//nng_set_pool_thread_limit_max(pool_thread_limit_max);
nng_set_resolve_thread_max(1);
ControllerContext ctx; ControllerContext ctx;
ctx.pub = make_ctrl_pub(CtrlUrl); ctx.pub = make_ctrl_pub(CtrlUrl);