From dda5634189007653f0f5a79c7d4686c3c5be724d Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Wed, 12 Apr 2023 22:28:04 -0700 Subject: [PATCH] Scripting: Add canvas API --- include/mgba/feature/video-backend.h | 13 +- include/mgba/script.h | 1 + include/mgba/script/canvas.h | 23 ++ src/platform/qt/Display.h | 1 + src/platform/qt/Window.cpp | 24 +- .../qt/scripting/ScriptingController.cpp | 10 +- .../qt/scripting/ScriptingController.h | 3 + src/script/CMakeLists.txt | 1 + src/script/canvas.c | 269 ++++++++++++++++++ 9 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 include/mgba/script/canvas.h create mode 100644 src/script/canvas.c diff --git a/include/mgba/feature/video-backend.h b/include/mgba/feature/video-backend.h index 7baf82483..292dc9c82 100644 --- a/include/mgba/feature/video-backend.h +++ b/include/mgba/feature/video-backend.h @@ -26,10 +26,21 @@ enum VideoLayer { VIDEO_LAYER_BACKGROUND = 0, VIDEO_LAYER_BEZEL, VIDEO_LAYER_IMAGE, - VIDEO_LAYER_OVERLAY, + VIDEO_LAYER_OVERLAY0, + VIDEO_LAYER_OVERLAY1, + VIDEO_LAYER_OVERLAY2, + VIDEO_LAYER_OVERLAY3, + VIDEO_LAYER_OVERLAY4, + VIDEO_LAYER_OVERLAY5, + VIDEO_LAYER_OVERLAY6, + VIDEO_LAYER_OVERLAY7, + VIDEO_LAYER_OVERLAY8, + VIDEO_LAYER_OVERLAY9, VIDEO_LAYER_MAX }; +#define VIDEO_LAYER_OVERLAY_COUNT VIDEO_LAYER_MAX - VIDEO_LAYER_OVERLAY0 + struct VideoBackend { void (*init)(struct VideoBackend*, WHandle handle); void (*deinit)(struct VideoBackend*); diff --git a/include/mgba/script.h b/include/mgba/script.h index 3497cd0d0..747dfedc1 100644 --- a/include/mgba/script.h +++ b/include/mgba/script.h @@ -7,6 +7,7 @@ #define M_SCRIPT_H #include +#include #include #include #include diff --git a/include/mgba/script/canvas.h b/include/mgba/script/canvas.h new file mode 100644 index 000000000..39c8edfd1 --- /dev/null +++ b/include/mgba/script/canvas.h @@ -0,0 +1,23 @@ +/* Copyright (c) 2013-2023 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef M_SCRIPT_CANVAS_H +#define M_SCRIPT_CANVAS_H + +#include + +CXX_GUARD_START + +#include +#include + +struct VideoBackend; +void mScriptContextAttachCanvas(struct mScriptContext* context); +void mScriptCanvasUpdate(struct mScriptContext* context); +void mScriptCanvasUpdateBackend(struct mScriptContext* context, struct VideoBackend*); + +CXX_GUARD_END + +#endif diff --git a/src/platform/qt/Display.h b/src/platform/qt/Display.h index 6830e8515..e9c37aa55 100644 --- a/src/platform/qt/Display.h +++ b/src/platform/qt/Display.h @@ -14,6 +14,7 @@ #include "MessagePainter.h" struct VDir; +struct VideoBackend; struct VideoShader; namespace QGBA { diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index a94d839a7..26d5d4255 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -970,6 +970,12 @@ void Window::gameStopped() { updateTitle(); if (m_pendingClose) { +#ifdef ENABLE_SCRIPTING + std::shared_ptr proxy = m_display->videoProxy(); + if (m_scripting && proxy) { + m_scripting->setVideoBackend(nullptr); + } +#endif m_display.reset(); close(); } @@ -1020,6 +1026,15 @@ void Window::reloadDisplayDriver() { m_display->stopDrawing(); detachWidget(); } +#ifdef ENABLE_SCRIPTING + if (m_scripting) { + m_scripting->setVideoBackend(nullptr); + } +#endif + std::shared_ptr proxy; + if (m_display) { + proxy = m_display->videoProxy(); + } m_display = std::unique_ptr(Display::create(this)); if (!m_display) { LOG(QT, ERROR) << tr("Failed to create an appropriate display device, falling back to software display. " @@ -1061,8 +1076,15 @@ void Window::reloadDisplayDriver() { m_display->setBackgroundImage(QImage{m_config->getOption("backgroundImage")}); - std::shared_ptr proxy = std::make_shared(); + if (!proxy) { + proxy = std::make_shared(); + } m_display->setVideoProxy(proxy); +#ifdef ENABLE_SCRIPTING + if (m_scripting) { + m_scripting->setVideoBackend(proxy->backend()); + } +#endif } void Window::reloadAudioDriver() { diff --git a/src/platform/qt/scripting/ScriptingController.cpp b/src/platform/qt/scripting/ScriptingController.cpp index 3062dc96a..3542bf28b 100644 --- a/src/platform/qt/scripting/ScriptingController.cpp +++ b/src/platform/qt/scripting/ScriptingController.cpp @@ -87,6 +87,10 @@ void ScriptingController::setInputController(InputController* input) { connect(m_inputController, &InputController::updated, this, &ScriptingController::updateGamepad); } +void ScriptingController::setVideoBackend(VideoBackend* backend) { + mScriptCanvasUpdateBackend(&m_scriptContext, backend); +} + bool ScriptingController::loadFile(const QString& path) { VFileDevice vf(path, QIODevice::ReadOnly); if (!vf.isOpen()) { @@ -304,11 +308,13 @@ void ScriptingController::detachGamepad() { void ScriptingController::init() { mScriptContextInit(&m_scriptContext); mScriptContextAttachStdlib(&m_scriptContext); + mScriptContextAttachCanvas(&m_scriptContext); + mScriptContextAttachImage(&m_scriptContext); + mScriptContextAttachInput(&m_scriptContext); + mScriptContextAttachSocket(&m_scriptContext); #ifdef USE_JSON_C mScriptContextAttachStorage(&m_scriptContext); #endif - mScriptContextAttachSocket(&m_scriptContext); - mScriptContextAttachInput(&m_scriptContext); mScriptContextRegisterEngines(&m_scriptContext); mScriptContextAttachLogger(&m_scriptContext, &m_logger); diff --git a/src/platform/qt/scripting/ScriptingController.h b/src/platform/qt/scripting/ScriptingController.h index e34b34542..da99a5785 100644 --- a/src/platform/qt/scripting/ScriptingController.h +++ b/src/platform/qt/scripting/ScriptingController.h @@ -20,6 +20,8 @@ class QKeyEvent; class QTextDocument; +struct VideoBackend; + namespace QGBA { class CoreController; @@ -36,6 +38,7 @@ public: void setController(std::shared_ptr controller); void setInputController(InputController* controller); + void setVideoBackend(VideoBackend* backend); bool loadFile(const QString& path); bool load(VFileDevice& vf, const QString& name); diff --git a/src/script/CMakeLists.txt b/src/script/CMakeLists.txt index a451ff6d8..0f64514c0 100644 --- a/src/script/CMakeLists.txt +++ b/src/script/CMakeLists.txt @@ -1,5 +1,6 @@ include(ExportDirectory) set(SOURCE_FILES + canvas.c context.c input.c image.c diff --git a/src/script/canvas.c b/src/script/canvas.c new file mode 100644 index 000000000..c8b0441ac --- /dev/null +++ b/src/script/canvas.c @@ -0,0 +1,269 @@ +/* Copyright (c) 2013-2023 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include + +#include +#include +#include + +struct mScriptCanvasContext; +struct mScriptCanvasLayer { + struct VideoBackend* backend; + enum VideoLayer layer; + struct mImage* image; + int x; + int y; + bool dirty; + bool sizeDirty; + bool dimsDirty; + bool contentsDirty; +}; + +struct mScriptCanvasContext { + struct mScriptCanvasLayer overlays[VIDEO_LAYER_OVERLAY_COUNT]; + struct VideoBackend* backend; + struct mScriptContext* context; + uint32_t frameCbid; +}; + +mSCRIPT_DECLARE_STRUCT(mScriptCanvasContext); +mSCRIPT_DECLARE_STRUCT(mScriptCanvasLayer); + +static void mScriptCanvasLayerDestroy(struct mScriptCanvasLayer* layer); +static void mScriptCanvasLayerUpdate(struct mScriptCanvasLayer* layer); + +static int _getNextAvailableOverlay(struct mScriptCanvasContext* context) { + if (!context->backend) { + return -1; + } + size_t i; + for (i = 0; i < VIDEO_LAYER_OVERLAY_COUNT; ++i) { + int w = -1, h = -1; + context->backend->imageSize(context->backend, VIDEO_LAYER_OVERLAY0 + i, &w, &h); + if (w <= 0 && h <= 0) { + return i; + } + } + return -1; +} + +static void mScriptCanvasContextDeinit(struct mScriptCanvasContext* context) { + size_t i; + for (i = 0; i < VIDEO_LAYER_OVERLAY_COUNT; ++i) { + if (context->overlays[i].image) { + mScriptCanvasLayerDestroy(&context->overlays[i]); + } + } +} + +void mScriptCanvasUpdateBackend(struct mScriptContext* context, struct VideoBackend* backend) { + struct mScriptValue* value = mScriptContextGetGlobal(context, "canvas"); + if (!value) { + return; + } + struct mScriptCanvasContext* canvas = value->value.opaque; + canvas->backend = backend; + size_t i; + for (i = 0; i < VIDEO_LAYER_OVERLAY_COUNT; ++i) { + struct mScriptCanvasLayer* layer = &canvas->overlays[i]; + layer->backend = backend; + layer->dirty = true; + layer->dimsDirty = true; + layer->sizeDirty = true; + layer->contentsDirty = true; + } +} + +static void _mScriptCanvasUpdate(struct mScriptCanvasContext* canvas) { + size_t i; + for (i = 0; i < VIDEO_LAYER_OVERLAY_COUNT; ++i) { + mScriptCanvasLayerUpdate(&canvas->overlays[i]); + } +} + +static unsigned _mScriptCanvasWidth(struct mScriptCanvasContext* canvas) { + if (!canvas->backend) { + return 0; + } + unsigned w, h; + VideoBackendGetFrameSize(canvas->backend, &w, &h); + return w; +} + +static unsigned _mScriptCanvasHeight(struct mScriptCanvasContext* canvas) { + if (!canvas->backend) { + return 0; + } + unsigned w, h; + VideoBackendGetFrameSize(canvas->backend, &w, &h); + return h; +} + +static int _mScriptCanvasScreenWidth(struct mScriptCanvasContext* canvas) { + if (!canvas->backend) { + return 0; + } + struct mRectangle dims; + canvas->backend->layerDimensions(canvas->backend, VIDEO_LAYER_IMAGE, &dims); + return dims.width; +} + +static int _mScriptCanvasScreenHeight(struct mScriptCanvasContext* canvas) { + if (!canvas->backend) { + return 0; + } + struct mRectangle dims; + canvas->backend->layerDimensions(canvas->backend, VIDEO_LAYER_IMAGE, &dims); + return dims.height; +} + +void mScriptCanvasUpdate(struct mScriptContext* context) { + struct mScriptValue* value = mScriptContextGetGlobal(context, "canvas"); + if (!value) { + return; + } + struct mScriptCanvasContext* canvas = value->value.opaque; + _mScriptCanvasUpdate(canvas); +} + +static struct mScriptValue* mScriptCanvasLayerCreate(struct mScriptCanvasContext* context, int w, int h) { + if (w <= 0 || h <= 0) { + return NULL; + } + int next = _getNextAvailableOverlay(context); + if (next < 0) { + return NULL; + } + + struct mScriptCanvasLayer* layer = &context->overlays[next]; + if (layer->image) { + // This shouldn't exist yet + abort(); + } + + layer->image = mImageCreate(w, h, mCOLOR_ABGR8); + layer->dirty = true; + layer->dimsDirty = true; + layer->sizeDirty = true; + layer->contentsDirty = true; + mScriptCanvasLayerUpdate(layer); + + struct mScriptValue* value = mScriptValueAlloc(mSCRIPT_TYPE_MS_S(mScriptCanvasLayer)); + value->value.opaque = layer; + return value; +} + +static void mScriptCanvasLayerDestroy(struct mScriptCanvasLayer* layer) { + struct mRectangle frame = {0}; + if (layer->backend) { + layer->backend->setLayerDimensions(layer->backend, layer->layer, &frame); + layer->backend->setImageSize(layer->backend, layer->layer, 0, 0); + } + mImageDestroy(layer->image); + layer->image = NULL; +} + +static void mScriptCanvasLayerUpdate(struct mScriptCanvasLayer* layer) { + if (!layer->dirty || !layer->image || !layer->backend) { + return; + } + + struct VideoBackend* backend = layer->backend; + if (layer->sizeDirty) { + backend->setImageSize(backend, layer->layer, layer->image->width, layer->image->height); + layer->sizeDirty = false; + // Resizing the image invalidates the contents in many backends + layer->contentsDirty = true; + } + if (layer->dimsDirty) { + struct mRectangle frame = { + .x = layer->x, + .y = layer->y, + .width = layer->image->width, + .height = layer->image->height, + }; + backend->setLayerDimensions(backend, layer->layer, &frame); + layer->dimsDirty = false; + } + if (layer->contentsDirty) { + backend->setImage(backend, layer->layer, layer->image->data); + layer->contentsDirty = false; + } + layer->dirty = false; +} + + +static void mScriptCanvasLayerSetPosition(struct mScriptCanvasLayer* layer, int32_t x, int32_t y) { + layer->x = x; + layer->y = y; + layer->dimsDirty = true; + layer->dirty = true; +} + +static void mScriptCanvasLayerInvalidate(struct mScriptCanvasLayer* layer) { + layer->contentsDirty = true; + layer->dirty = true; +} + +void mScriptContextAttachCanvas(struct mScriptContext* context) { + struct mScriptCanvasContext* canvas = calloc(1, sizeof(*canvas)); + size_t i; + for (i = 0; i < VIDEO_LAYER_OVERLAY_COUNT; ++i) { + canvas->overlays[i].layer = VIDEO_LAYER_OVERLAY0 + i; + } + struct mScriptValue* value = mScriptValueAlloc(mSCRIPT_TYPE_MS_S(mScriptCanvasContext)); + value->flags = mSCRIPT_VALUE_FLAG_FREE_BUFFER; + value->value.opaque = canvas; + + canvas->context = context; + struct mScriptValue* lambda = mScriptObjectBindLambda(value, "update", NULL); + canvas->frameCbid = mScriptContextAddCallback(context, "frame", lambda); + mScriptValueDeref(lambda); + + mScriptContextSetGlobal(context, "canvas", value); + mScriptContextSetDocstring(context, "canvas", "Singleton instance of struct::mScriptCanvasContext"); +} + +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptCanvasContext, _deinit, mScriptCanvasContextDeinit, 0); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptCanvasContext, W(mScriptCanvasLayer), newLayer, mScriptCanvasLayerCreate, 2, S32, width, S32, height); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptCanvasContext, update, _mScriptCanvasUpdate, 0); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptCanvasContext, U32, width, _mScriptCanvasWidth, 0); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptCanvasContext, U32, height, _mScriptCanvasHeight, 0); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptCanvasContext, S32, screenWidth, _mScriptCanvasScreenWidth, 0); +mSCRIPT_DECLARE_STRUCT_METHOD(mScriptCanvasContext, S32, screenHeight, _mScriptCanvasScreenHeight, 0); + +mSCRIPT_DEFINE_STRUCT(mScriptCanvasContext) + mSCRIPT_DEFINE_CLASS_DOCSTRING( + "A canvas that can be used for drawing images on or around the screen." + ) + mSCRIPT_DEFINE_STRUCT_DEINIT(mScriptCanvasContext) + mSCRIPT_DEFINE_DOCSTRING("Create a new layer of a given size. If multiple layers overlap, the most recently created one takes priority.") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCanvasContext, newLayer) + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCanvasContext, update) + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCanvasContext, width) + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCanvasContext, height) + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCanvasContext, screenWidth) + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCanvasContext, screenHeight) +mSCRIPT_DEFINE_END; + +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptCanvasLayer, update, mScriptCanvasLayerInvalidate, 0); +mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptCanvasLayer, setPosition, mScriptCanvasLayerSetPosition, 2, S32, x, S32, y); + +mSCRIPT_DEFINE_STRUCT(mScriptCanvasLayer) + mSCRIPT_DEFINE_CLASS_DOCSTRING( + "An individual layer of a drawable canvas." + ) + mSCRIPT_DEFINE_DOCSTRING("Mark the contents of the layer as needed to be repainted") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCanvasLayer, update) + mSCRIPT_DEFINE_DOCSTRING("Set the position of the layer in the canvas") + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptCanvasLayer, setPosition) + mSCRIPT_DEFINE_DOCSTRING("The image that has the pixel contents of the image") + mSCRIPT_DEFINE_STRUCT_MEMBER(mScriptCanvasLayer, PS(mImage), image) + mSCRIPT_DEFINE_DOCSTRING("The current x (horizontal) position of this layer") + mSCRIPT_DEFINE_STRUCT_CONST_MEMBER(mScriptCanvasLayer, S32, x) + mSCRIPT_DEFINE_DOCSTRING("The current y (vertical) position of this layer") + mSCRIPT_DEFINE_STRUCT_CONST_MEMBER(mScriptCanvasLayer, S32, y) +mSCRIPT_DEFINE_END;