diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 5a5ddc457e0..28d5b7be902 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -59,7 +59,7 @@ jobs: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo add-apt-repository 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-16 main' sudo apt-get update - sudo apt-get install -y clang-format-16 ccache e2fsprogs gcc-12 g++-12 libstdc++-12-dev libmpfr-dev libmpc-dev ninja-build optipng qemu-utils qemu-system-i386 unzip generate-ninja + sudo apt-get install -y clang-format-16 ccache e2fsprogs gcc-12 g++-12 libstdc++-12-dev libmpfr-dev libmpc-dev ninja-build optipng qemu-utils qemu-system-i386 unzip generate-ninja libegl1-mesa-dev if ${{ matrix.arch == 'aarch64' }}; then # FIXME: Remove this when we no longer build our own Qemu binary. sudo apt-get install libgtk-3-dev libpixman-1-dev libsdl2-dev libslirp-dev diff --git a/Meta/Azure/Setup.yml b/Meta/Azure/Setup.yml index d9519b573c8..341eccd66a0 100644 --- a/Meta/Azure/Setup.yml +++ b/Meta/Azure/Setup.yml @@ -21,7 +21,7 @@ steps: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - sudo add-apt-repository 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-15 main' sudo apt-get update - sudo apt-get install ccache gcc-12 g++-12 clang-15 libstdc++-12-dev ninja-build unzip qt6-base-dev qt6-tools-dev-tools libqt6svg6-dev qt6-multimedia-dev libgl1-mesa-dev libpulse-dev libssl-dev + sudo apt-get install ccache gcc-12 g++-12 clang-15 libstdc++-12-dev ninja-build unzip qt6-base-dev qt6-tools-dev-tools libqt6svg6-dev qt6-multimedia-dev libgl1-mesa-dev libpulse-dev libssl-dev libegl1-mesa-dev sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-15 100 sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-15 100 diff --git a/Userland/Libraries/CMakeLists.txt b/Userland/Libraries/CMakeLists.txt index 4c88547404a..35db0995448 100644 --- a/Userland/Libraries/CMakeLists.txt +++ b/Userland/Libraries/CMakeLists.txt @@ -1,3 +1,4 @@ +add_subdirectory(LibAccelGfx) add_subdirectory(LibArchive) add_subdirectory(LibAudio) add_subdirectory(LibC) diff --git a/Userland/Libraries/LibAccelGfx/CMakeLists.txt b/Userland/Libraries/LibAccelGfx/CMakeLists.txt new file mode 100644 index 00000000000..87c0c906fe9 --- /dev/null +++ b/Userland/Libraries/LibAccelGfx/CMakeLists.txt @@ -0,0 +1,10 @@ +if (LINUX) + set(SOURCES + Canvas.cpp + Context.cpp + Painter.cpp + ) + + serenity_lib(LibAccelGfx accelgfx) + target_link_libraries(LibAccelGfx PRIVATE LibGfx GL EGL) +endif() diff --git a/Userland/Libraries/LibAccelGfx/Canvas.cpp b/Userland/Libraries/LibAccelGfx/Canvas.cpp new file mode 100644 index 00000000000..9bfddc1b1e9 --- /dev/null +++ b/Userland/Libraries/LibAccelGfx/Canvas.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Canvas.h" +#include +#include + +namespace AccelGfx { + +Canvas Canvas::create(Context& context, NonnullRefPtr bitmap) +{ + VERIFY(bitmap->format() == Gfx::BitmapFormat::BGRA8888); + Canvas canvas { move(bitmap), context }; + canvas.initialize(); + return canvas; +} + +Canvas::Canvas(NonnullRefPtr bitmap, Context& context) + : m_bitmap(move(bitmap)) + , m_context(context) +{ +} + +void Canvas::initialize() +{ + m_surface = m_context.create_surface(width(), height()); + m_context.set_active_surface(m_surface); + glViewport(0, 0, width(), height()); +} + +void Canvas::flush() +{ + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, width(), height(), GL_BGRA, GL_UNSIGNED_BYTE, m_bitmap->scanline(0)); +} + +Canvas::~Canvas() +{ + m_context.destroy_surface(m_surface); +} + +} diff --git a/Userland/Libraries/LibAccelGfx/Canvas.h b/Userland/Libraries/LibAccelGfx/Canvas.h new file mode 100644 index 00000000000..1175a78331f --- /dev/null +++ b/Userland/Libraries/LibAccelGfx/Canvas.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace AccelGfx { + +class Canvas { +public: + static Canvas create(Context& context, NonnullRefPtr bitmap); + + [[nodiscard]] Gfx::IntSize size() const { return m_bitmap->size(); } + [[nodiscard]] int width() const { return m_bitmap->width(); } + [[nodiscard]] int height() const { return m_bitmap->height(); } + + void flush(); + + [[nodiscard]] Gfx::Bitmap const& bitmap() const { return *m_bitmap; } + + ~Canvas(); + +private: + explicit Canvas(NonnullRefPtr, Context&); + + void initialize(); + + NonnullRefPtr m_bitmap; + + Context& m_context; + Context::Surface m_surface; +}; + +} diff --git a/Userland/Libraries/LibAccelGfx/Context.cpp b/Userland/Libraries/LibAccelGfx/Context.cpp new file mode 100644 index 00000000000..8188cab9879 --- /dev/null +++ b/Userland/Libraries/LibAccelGfx/Context.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace AccelGfx { + +Context& Context::the() +{ + static OwnPtr s_the; + if (!s_the) + s_the = Context::create(); + return *s_the; +} + +Context::Surface Context::create_surface(int width, int height) +{ + EGLint const pbuffer_attributes[] = { + EGL_WIDTH, + width, + EGL_HEIGHT, + height, + EGL_NONE, + }; + + auto egl_surface = eglCreatePbufferSurface(m_egl_display, m_egl_config, pbuffer_attributes); + return { egl_surface }; +} + +void Context::destroy_surface(Surface surface) +{ + if (surface.egl_surface) + eglDestroySurface(m_egl_display, surface.egl_surface); +} + +void Context::set_active_surface(Surface surface) +{ + VERIFY(eglMakeCurrent(m_egl_display, surface.egl_surface, surface.egl_surface, m_egl_context)); +} + +OwnPtr Context::create() +{ + EGLDisplay egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + + EGLint major; + EGLint minor; + eglInitialize(egl_display, &major, &minor); + + static EGLint const config_attributes[] = { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_BLUE_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, + EGL_DEPTH_SIZE, 8, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_NONE + }; + + EGLConfig egl_config; + EGLint num_configs; + eglChooseConfig(egl_display, config_attributes, &egl_config, 1, &num_configs); + + static EGLint const context_attributes[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + EGLContext egl_context = eglCreateContext(egl_display, egl_config, EGL_NO_CONTEXT, context_attributes); + + return make(egl_display, egl_context, egl_config); +} + +} diff --git a/Userland/Libraries/LibAccelGfx/Context.h b/Userland/Libraries/LibAccelGfx/Context.h new file mode 100644 index 00000000000..42e20c4a6de --- /dev/null +++ b/Userland/Libraries/LibAccelGfx/Context.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +#ifdef AK_OS_LINUX +# include +#endif + +namespace AccelGfx { + +class Context { +public: + static Context& the(); + + struct Surface { + EGLSurface egl_surface { 0 }; + }; + + Surface create_surface(int width, int height); + void destroy_surface(Surface surface); + void set_active_surface(Surface surface); + + static OwnPtr create(); + + Context(EGLDisplay egl_display, EGLContext egl_context, EGLConfig egl_config) + : m_egl_display(egl_display) + , m_egl_context(egl_context) + , m_egl_config(egl_config) + { + } + +private: + EGLDisplay m_egl_display { nullptr }; + EGLContext m_egl_context { nullptr }; + EGLConfig m_egl_config { nullptr }; +}; + +} diff --git a/Userland/Libraries/LibAccelGfx/Forward.h b/Userland/Libraries/LibAccelGfx/Forward.h new file mode 100644 index 00000000000..1c5b295aad2 --- /dev/null +++ b/Userland/Libraries/LibAccelGfx/Forward.h @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2023, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +namespace AccelGfx { + +class Canvas; +class Painter; + +} diff --git a/Userland/Libraries/LibAccelGfx/Painter.cpp b/Userland/Libraries/LibAccelGfx/Painter.cpp new file mode 100644 index 00000000000..b67c9a04635 --- /dev/null +++ b/Userland/Libraries/LibAccelGfx/Painter.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2023, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#define GL_GLEXT_PROTOTYPES + +#include "Painter.h" +#include "Canvas.h" +#include +#include +#include + +namespace AccelGfx { + +struct ColorComponents { + float red; + float green; + float blue; + float alpha; +}; + +static ColorComponents gfx_color_to_opengl_color(Gfx::Color color) +{ + ColorComponents components; + components.red = static_cast(color.red()) / 255.0f; + components.green = static_cast(color.green()) / 255.0f; + components.blue = static_cast(color.blue()) / 255.0f; + components.alpha = static_cast(color.alpha()) / 255.0f; + return components; +} + +Gfx::FloatRect Painter::to_clip_space(Gfx::FloatRect const& screen_rect) const +{ + float x = 2.0f * screen_rect.x() / m_canvas.width() - 1.0f; + float y = -1.0f + 2.0f * screen_rect.y() / m_canvas.height(); + + float width = 2.0f * screen_rect.width() / m_canvas.width(); + float height = 2.0f * screen_rect.height() / m_canvas.height(); + + return { x, y, width, height }; +} + +Painter::Painter(Canvas& canvas) + : m_canvas(canvas) +{ + m_state_stack.empend(State()); +} + +Painter::~Painter() +{ + flush(); +} + +void Painter::clear(Gfx::Color color) +{ + auto [red, green, blue, alpha] = gfx_color_to_opengl_color(color); + glClearColor(red, green, blue, alpha); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +} + +void Painter::fill_rect(Gfx::IntRect rect, Gfx::Color color) +{ + fill_rect(rect.to_type(), color); +} + +static GLuint create_shader(GLenum type, char const* source) +{ + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + + int success; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (!success) { + char buffer[512]; + glGetShaderInfoLog(shader, sizeof(buffer), nullptr, buffer); + dbgln("GLSL shader compilation failed: {}", buffer); + VERIFY_NOT_REACHED(); + } + + return shader; +} + +static Array rect_to_vertices(Gfx::FloatRect const& rect) +{ + return { + rect.left(), + rect.top(), + rect.left(), + rect.bottom(), + rect.right(), + rect.bottom(), + rect.right(), + rect.top(), + }; +} + +void Painter::fill_rect(Gfx::FloatRect rect, Gfx::Color color) +{ + // Draw a filled rect (with `color`) using OpenGL after mapping it through the current transform. + + auto vertices = rect_to_vertices(to_clip_space(transform().map(rect))); + + char const* vertex_shader_source = R"( + attribute vec2 position; + void main() { + gl_Position = vec4(position, 0.0, 1.0); + } +)"; + + char const* fragment_shader_source = R"( + precision mediump float; + uniform vec4 uColor; + void main() { + gl_FragColor = uColor; + } +)"; + + auto [red, green, blue, alpha] = gfx_color_to_opengl_color(color); + + GLuint vertex_shader = create_shader(GL_VERTEX_SHADER, vertex_shader_source); + GLuint fragment_shader = create_shader(GL_FRAGMENT_SHADER, fragment_shader_source); + + GLuint program = glCreateProgram(); + + glAttachShader(program, vertex_shader); + glAttachShader(program, fragment_shader); + glLinkProgram(program); + + int linked; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (!linked) { + char buffer[512]; + glGetProgramInfoLog(program, sizeof(buffer), nullptr, buffer); + dbgln("GLSL program linking failed: {}", buffer); + VERIFY_NOT_REACHED(); + } + + glUseProgram(program); + + GLuint position_attribute = glGetAttribLocation(program, "position"); + GLuint color_uniform = glGetUniformLocation(program, "uColor"); + + glUniform4f(color_uniform, red, green, blue, alpha); + + glVertexAttribPointer(position_attribute, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), vertices.data()); + glEnableVertexAttribArray(position_attribute); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + glDeleteProgram(program); +} + +void Painter::save() +{ + m_state_stack.append(state()); +} + +void Painter::restore() +{ + VERIFY(!m_state_stack.is_empty()); + m_state_stack.take_last(); +} + +void Painter::flush() +{ + m_canvas.flush(); +} + +} diff --git a/Userland/Libraries/LibAccelGfx/Painter.h b/Userland/Libraries/LibAccelGfx/Painter.h new file mode 100644 index 00000000000..fe270a08f61 --- /dev/null +++ b/Userland/Libraries/LibAccelGfx/Painter.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace AccelGfx { + +class Painter { + AK_MAKE_NONCOPYABLE(Painter); + AK_MAKE_NONMOVABLE(Painter); + +public: + Painter(Canvas&); + ~Painter(); + + void clear(Gfx::Color); + + void save(); + void restore(); + + [[nodiscard]] Gfx::AffineTransform const& transform() const { return state().transform; } + void set_transform(Gfx::AffineTransform const& transform) { state().transform = transform; } + + void fill_rect(Gfx::FloatRect, Gfx::Color); + void fill_rect(Gfx::IntRect, Gfx::Color); + +private: + void flush(); + + Canvas& m_canvas; + + struct State { + Gfx::AffineTransform transform; + }; + + [[nodiscard]] State& state() { return m_state_stack.last(); } + [[nodiscard]] State const& state() const { return m_state_stack.last(); } + + [[nodiscard]] Gfx::FloatRect to_clip_space(Gfx::FloatRect const& screen_rect) const; + + Vector m_state_stack; +}; + +}