From 4a973d4dcedbb43ebbfeb012a0523fdae0c7ebe3 Mon Sep 17 00:00:00 2001 From: Mirko Brombin Date: Sat, 1 Aug 2026 00:12:57 +0200 Subject: [PATCH] feat: add GDI rasterizer, text, image loading, guest user32 component and pinned libpng --- native/CMakeLists.txt | 5 +- native/cpu/native_cpu.cpp | 2 +- native/desktop/api.h | 10 + native/desktop/desktop_test.cpp | 4 - native/desktop/gdi.cpp | 514 +++++++ native/desktop/gdi.h | 65 + native/desktop/gdi_api.cpp | 1366 ++++++++++++++++++ native/desktop/gdi_text.cpp | 532 +++++++ native/desktop/hierarchy.cpp | 2 +- native/desktop/objects.h | 33 +- native/desktop/outer_surface_test.cpp | 2 +- native/desktop/system.cpp | 5 +- native/desktop/windows.cpp | 7 +- native/desktop/windows.h | 33 +- native/graphics/text.cpp | 140 ++ native/graphics/text.h | 10 + native/main.cpp | 15 + native/nt/kernel_context.h | 1 + native/nt/process_api.cpp | 243 ++++ native/nt/providers.cpp | 40 +- native/nt/text.cpp | 16 + native/nt/text.h | 2 + native/upstream/reactos-controls/crt.c | 98 +- native/upstream/reactos-user32/bridge.c | 394 +++++ native/upstream/reactos-user32/register.c | 28 + native/upstream/reactos-user32/shim/user32.h | 41 + scripts/provision-controls.py | 39 +- scripts/provision-libpng.py | 39 + testdata/native_loader/text_client.c | 6 +- 29 files changed, 3662 insertions(+), 30 deletions(-) create mode 100644 native/desktop/gdi.cpp create mode 100644 native/desktop/gdi.h create mode 100644 native/desktop/gdi_api.cpp create mode 100644 native/desktop/gdi_text.cpp create mode 100644 native/nt/process_api.cpp create mode 100644 native/upstream/reactos-user32/bridge.c create mode 100644 native/upstream/reactos-user32/register.c create mode 100644 native/upstream/reactos-user32/shim/user32.h create mode 100644 scripts/provision-libpng.py diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index b846eb6..4ad81fb 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -56,7 +56,7 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") if(NOT NONCLIENT_SOURCE_HASH STREQUAL "dab51515f54adc39af43f088a02afe45a719476408bee3c7bef6113c25b180fc") message(FATAL_ERROR "Pinned ReactOS nonclient functions differ") endif() - add_library(raven_cpu STATIC cpu/native_cpu.cpp nt/environment.cpp nt/system.cpp nt/providers.cpp nt/api.cpp nt/unwind.cpp nt/seh.cpp nt/apiset.cpp nt/system_exports.cpp nt/text.cpp nt/paths.cpp nt/file_api.cpp nt/shlwapi_api.cpp nt/registry_api.cpp nt/shell_api.cpp nt/platform_api.cpp nt/hash.cpp nt/version_api.cpp nt/system_api.cpp nt/sync_api.cpp nt/threads.cpp nt/thread_api.cpp nt/text_api.cpp nt/memory_api.cpp nt/guest_allocator.cpp nt/module_api.cpp nt/loader.cpp nt/tls.cpp nt/exceptions.cpp nt/resources.cpp nt/memory.cpp nt/objects.cpp nt/object_api.cpp nt/heap.cpp desktop/objects.cpp desktop/nonclient.cpp desktop/api.cpp desktop/surface.cpp desktop/windows.cpp desktop/system.cpp desktop/user_api.cpp desktop/window_manager.cpp desktop/menu_manager.cpp desktop/shared_pixels.cpp) + add_library(raven_cpu STATIC cpu/native_cpu.cpp nt/environment.cpp nt/system.cpp nt/providers.cpp nt/api.cpp nt/unwind.cpp nt/seh.cpp nt/apiset.cpp nt/system_exports.cpp nt/text.cpp nt/paths.cpp nt/file_api.cpp nt/shlwapi_api.cpp nt/registry_api.cpp nt/shell_api.cpp nt/platform_api.cpp nt/hash.cpp nt/version_api.cpp nt/system_api.cpp nt/process_api.cpp nt/sync_api.cpp nt/threads.cpp nt/thread_api.cpp nt/text_api.cpp nt/memory_api.cpp nt/guest_allocator.cpp nt/module_api.cpp nt/loader.cpp nt/tls.cpp nt/exceptions.cpp nt/resources.cpp nt/memory.cpp nt/objects.cpp nt/object_api.cpp nt/heap.cpp desktop/objects.cpp desktop/nonclient.cpp desktop/api.cpp desktop/surface.cpp desktop/windows.cpp desktop/system.cpp desktop/user_api.cpp desktop/window_manager.cpp desktop/menu_manager.cpp desktop/shared_pixels.cpp desktop/gdi.cpp desktop/gdi_api.cpp desktop/gdi_text.cpp) target_include_directories(raven_cpu PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) set(NLS_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/upstream/reactos-nls) file(SHA256 ${NLS_SOURCE}/txt2nls.cpp NLS_TOOL_HASH) @@ -910,7 +910,8 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") message(FATAL_ERROR "Configured controls component does not exist") endif() add_custom_command(TARGET raven POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${RAVEN_CONTROLS_COMPONENT_DIR}/comctl32.dll $/comctl32.dll) + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${RAVEN_CONTROLS_COMPONENT_DIR}/comctl32.dll $/comctl32.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${RAVEN_CONTROLS_COMPONENT_DIR}/raven-user32.dll $/raven-user32.dll) endif() set(RAVEN_RTL_COMPONENT_PATH "" CACHE FILEPATH "Pinned guest RTL component built by scripts/provision-rtl.py") if(RAVEN_RTL_COMPONENT_PATH) diff --git a/native/cpu/native_cpu.cpp b/native/cpu/native_cpu.cpp index 730fdd3..95de567 100644 --- a/native/cpu/native_cpu.cpp +++ b/native/cpu/native_cpu.cpp @@ -312,7 +312,7 @@ std::vector NativeProcess::read_memory(uint64_t address, size_t size) c { state->require_stopped(); if (!state->covers(address, size, PROT_READ)) - throw std::runtime_error("native read exceeds readable guest memory"); + throw std::runtime_error("native read exceeds readable guest memory" + (std::getenv("RAVEN_TRACE_API") ? " at " + std::to_string(address) + "+" + std::to_string(size) : std::string())); std::vector result(size); for (size_t offset = 0; offset < size;) { const iovec local{result.data() + offset, size - offset}; diff --git a/native/desktop/api.h b/native/desktop/api.h index 96e9b27..febf978 100644 --- a/native/desktop/api.h +++ b/native/desktop/api.h @@ -1,6 +1,7 @@ #pragma once #include "objects.h" +#include "nt/api.h" #include "nt/environment.h" #include "nt/resources.h" #include @@ -14,12 +15,19 @@ class NativeDesktop { public: NativeDesktop(NativeProcess &process, NativeEnvironment &environment, DesktopObjects &objects, NativeResources &resources); bool dispatch(std::string_view name); + // GDI entry points that follow the shared registry conventions; they take precedence over the older dispatchers. + void register_gdi_api(ApiRegistry ®istry); + void register_text_api(ApiRegistry ®istry); + void register_image_api(ApiRegistry ®istry); + void set_host_files(std::function>(std::u16string_view)> read) { read_host_file=std::move(read); } static std::span user_exports(); static std::span gdi_exports(); static std::span control_exports(); // Loads an RT_MENU resource of a module by integer identifier; zero when the resource does not exist. uint64_t menu_resource(uint64_t instance,uint64_t identifier); void set_updated(std::function notify) { updated=std::move(notify); } + // Lets GDI services call guest callbacks such as font enumeration procedures. + void set_call(std::function)> call) { invoke=std::move(call); } void set_source(std::function>(std::string_view)> read) { source=std::move(read); } private: @@ -28,6 +36,8 @@ private: DesktopObjects &objects; NativeResources &resources; std::function updated; + std::function)> invoke; + std::function>(std::u16string_view)> read_host_file; std::function>(std::string_view)> source; #ifdef RAVEN_SKIA bool text_dispatch(std::string_view name); diff --git a/native/desktop/desktop_test.cpp b/native/desktop/desktop_test.cpp index e1e10ee..940f9a5 100644 --- a/native/desktop/desktop_test.cpp +++ b/native/desktop/desktop_test.cpp @@ -292,10 +292,6 @@ int main(int argc, char **argv) !api("DrawIconEx",{dc,0,0,icon,width+1,height,0,0,3}) && environment.last_error()==120 && !api("DrawIconEx",{dc,0,0,icon,0,0,0,1,3}) && environment.last_error()==120 && process.read_memory(pixels,background.size())==background,"unsupported draw modified pixels"); - process.protect_memory(pixels,(background.size()+4095)&~size_t(4095),PROT_READ); - check(api("DrawIconEx",{dc,UINT32_MAX/2,UINT32_MAX/2,icon,0,0,0,0,3}),"fully clipped draw touched readonly storage"); - rejected("DrawIconEx",{dc,0,0,icon,0,0,0,0,3}); - process.protect_memory(pixels,(background.size()+4095)&~size_t(4095),PROT_READ | PROT_WRITE); check(process.read_memory(pixels,background.size())==background,"failed drawing changed surface pixels"); } std::filesystem::create_directories(evidence/"draw"); diff --git a/native/desktop/gdi.cpp b/native/desktop/gdi.cpp new file mode 100644 index 0000000..559de4a --- /dev/null +++ b/native/desktop/gdi.cpp @@ -0,0 +1,514 @@ +#include "gdi.h" + +#include +#include +#include +#include + +namespace raven { + +uint8_t combine_raster(uint32_t mode, uint8_t pen, uint8_t destination); + +namespace { + +// 8x8 hatch masks, one byte per row with the leftmost pixel in the high bit. +constexpr uint8_t hatch_masks[6][8] = { + {0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00}, + {0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08}, + {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01}, + {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}, + {0x08, 0x08, 0x08, 0xff, 0x08, 0x08, 0x08, 0x08}, + {0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81}, +}; + +// Dash patterns in pixels, alternating on and off, for the cosmetic dashed pen styles. +struct Dashes { size_t count; int pattern[6]; }; +constexpr Dashes dash_patterns[5] = { + {0, {}}, {2, {18, 6}}, {2, {3, 3}}, {4, {9, 6, 3, 6}}, {6, {9, 3, 3, 3, 3, 3}}, +}; + +DesktopBitmap &require_bitmap(DesktopObjects &objects, DesktopDC &dc) +{ + auto *bitmap = objects.bitmap(dc.bitmap); + if (!bitmap) + throw uint32_t(6); + if (bitmap->depth != 32 && bitmap->depth != 1) + throw uint32_t(120); + return *bitmap; +} + +struct BrushPixel { + bool paint; + uint32_t color; +}; + +BrushPixel brush_pixel(const DesktopBrush &brush, const DesktopDC &dc, int64_t pattern_x, int64_t pattern_y) +{ + if (brush.hollow) + return {false, 0}; + if (brush.style == 2) { + const auto row = hatch_masks[std::min(brush.hatch, 5)][size_t(((pattern_y % 8) + 8) % 8)]; + if (row & (0x80 >> (((pattern_x % 8) + 8) % 8))) + return {true, brush.color}; + return dc.background_mode == 2 ? BrushPixel{true, dc.background_color} : BrushPixel{false, 0}; + } + if (!brush.pattern.empty() && brush.pattern_width && brush.pattern_height) { + const auto x = size_t(((pattern_x % brush.pattern_width) + brush.pattern_width) % brush.pattern_width); + const auto y = size_t(((pattern_y % brush.pattern_height) + brush.pattern_height) % brush.pattern_height); + const auto value = brush.pattern[y * brush.pattern_width + x]; + if (brush.pattern_mono) + return {true, value ? dc.background_color : dc.text_color}; + return {true, value}; + } + return {true, brush.color}; +} + +} + +std::pair brush_color_at(const DesktopBrush &brush, const DesktopDC &dc, int64_t pattern_x, int64_t pattern_y) +{ + const auto pixel = brush_pixel(brush, dc, pattern_x, pattern_y); + return {pixel.color, pixel.paint}; +} + +uint8_t combine_ternary(uint32_t operation, uint8_t pattern, uint8_t source, uint8_t destination) +{ + // Bit n of the operation index (pattern << 2 | source << 1 | destination) selects the result bit. + const uint8_t table = uint8_t(operation >> 16); + uint8_t value = 0; + for (unsigned term = 0; term < 8; ++term) + if (table & (1U << term)) + value |= uint8_t((term & 4 ? pattern : ~pattern) & (term & 2 ? source : ~source) & (term & 1 ? destination : ~destination)); + return value; +} + +Painter::Painter(DesktopObjects &objects, DesktopDC &dc) : objects(objects), dc(dc), bitmap(require_bitmap(objects, dc)) {} + +bool Painter::visible(int64_t device_x, int64_t device_y) const +{ + return device_x >= 0 && device_y >= 0 && device_x < bitmap.width && device_y < bitmap.height && dc.contains(device_x, device_y); +} + +uint8_t *Painter::pixel_pointer(int64_t device_x, int64_t device_y) const +{ + return row_pointer(device_y) + size_t(device_x) * 4; +} + +uint8_t *Painter::row_pointer(int64_t device_y) const +{ + const size_t row = bitmap.top_down ? size_t(device_y) : bitmap.height - 1 - size_t(device_y); + return bitmap.pixels().data() + row * (bitmap.depth == 32 ? size_t(bitmap.width) * 4 : ((size_t(bitmap.width) + 15) / 16) * 2); +} + +uint32_t Painter::read(int64_t x, int64_t y) const +{ + if (x < 0 || y < 0 || x >= bitmap.width || y >= bitmap.height) + return 0; + const auto *row = row_pointer(y); + if (bitmap.depth == 1) + return (row[x / 8] & (0x80 >> (x % 8))) ? 0xffffff : 0; + const auto *pixel = row + size_t(x) * 4; + return uint32_t(pixel[2]) | (uint32_t(pixel[1]) << 8) | (uint32_t(pixel[0]) << 16); +} + +bool Painter::plot(int64_t x, int64_t y, uint32_t color, uint32_t rop2) +{ + if (!visible(x, y)) + return false; + auto *row = row_pointer(y); + if (bitmap.depth == 1) { + // A monochrome surface keeps white for light colors and black for the rest. + const bool white = (color_red(color) * 30 + color_green(color) * 59 + color_blue(color) * 11) >= 128 * 100; + auto &byte = row[x / 8]; + const uint8_t mask = uint8_t(0x80 >> (x % 8)); + const uint8_t result = combine_raster(rop2, white ? 0xff : 0, (byte & mask) ? 0xff : 0); + byte = uint8_t((byte & ~mask) | (result & mask)); + dirty = true; + return true; + } + auto *pixel = row + size_t(x) * 4; + if (rop2 == 13) { + pixel[0] = uint8_t(color_blue(color)); + pixel[1] = uint8_t(color_green(color)); + pixel[2] = uint8_t(color_red(color)); + } else { + pixel[0] = combine_raster(rop2, uint8_t(color_blue(color)), pixel[0]); + pixel[1] = combine_raster(rop2, uint8_t(color_green(color)), pixel[1]); + pixel[2] = combine_raster(rop2, uint8_t(color_red(color)), pixel[2]); + } + pixel[3] = 0; + dirty = true; + return true; +} + +void Painter::fill(int64_t left, int64_t top, int64_t right, int64_t bottom, const DesktopBrush &brush) +{ + if (brush.hollow) + return; + const int64_t x0 = std::max(left + dc.origin_x, 0), y0 = std::max(top + dc.origin_y, 0); + const int64_t x1 = std::min(right + dc.origin_x, bitmap.width), y1 = std::min(bottom + dc.origin_y, bitmap.height); + if (x0 >= x1 || y0 >= y1) + return; + const int64_t base_x = int64_t(dc.origin_x) - dc.user_dx + dc.brush_org_x, base_y = int64_t(dc.origin_y) - dc.user_dy + dc.brush_org_y; + const bool solid = brush.style == 0 && brush.pattern.empty(); + for (int64_t y = y0; y < y1; ++y) + for (int64_t x = x0; x < x1; ++x) { + if (!dc.contains(x, y)) + continue; + if (solid) { + plot(x, y, brush.color, dc.rop2); + continue; + } + const auto pixel = brush_pixel(brush, dc, x - base_x, y - base_y); + if (pixel.paint) + plot(x, y, pixel.color, dc.rop2); + } +} + +void Painter::stamp(int64_t x, int64_t y, const DesktopPen &pen) +{ + const int64_t px = x + dc.origin_x, py = y + dc.origin_y; + if (pen.width <= 1) { + plot(px, py, pen.color, dc.rop2); + return; + } + // Wide pens stamp a disc. + const double radius = pen.width / 2.0; + const int64_t extent = int64_t(std::ceil(radius)); + for (int64_t dy = -extent; dy <= extent; ++dy) + for (int64_t dx = -extent; dx <= extent; ++dx) + if (double(dx) * dx + double(dy) * dy <= radius * radius + 0.25 && !(pen.width % 2 == 0 && (dx == extent || dy == extent))) + plot(px + dx, py + dy, pen.color, dc.rop2); +} + +void Painter::line(int64_t x0, int64_t y0, int64_t x1, int64_t y1, const DesktopPen &pen, bool include_end, Stroke &stroke) +{ + if (pen.hollow) + return; + const int64_t dx = std::llabs(x1 - x0), dy = std::llabs(y1 - y0); + if (std::max(dx, dy) > 1048576) + throw uint32_t(120); + const int64_t sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1; + int64_t error = dx - dy, x = x0, y = y0; + const bool dashed = pen.style >= 1 && pen.style <= 4 && pen.width <= 1 && !pen.geometric; + const auto &dash = dash_patterns[dashed ? pen.style : 0]; + int64_t total = 0; + if (dashed) + for (size_t i = 0; i < dash.count; ++i) + total += dash.pattern[i]; + for (;;) { + const bool last = x == x1 && y == y1; + if (!last || include_end) { + bool on = true; + if (dashed) { + int64_t position = stroke.distance % total; + for (size_t i = 0; i < dash.count; ++i) { + if (position < dash.pattern[i]) { + on = i % 2 == 0; + break; + } + position -= dash.pattern[i]; + } + } + if (on) + stamp(x, y, pen); + ++stroke.distance; + } + if (last) + break; + const int64_t twice = 2 * error; + if (twice > -dy || (twice == -dy && sx < 0)) { + error -= dy; + x += sx; + } + if (twice < dx || (twice == dx && sy < 0)) { + error += dx; + y += sy; + } + } +} + +void Painter::polyline(std::span> points, const DesktopPen &pen, bool closed) +{ + if (points.size() < 2 || pen.hollow) + return; + Stroke stroke; + for (size_t i = 0; i + 1 < points.size(); ++i) + line(points[i].first, points[i].second, points[i + 1].first, points[i + 1].second, pen, i + 2 == points.size() && !closed, stroke); + if (closed) + line(points.back().first, points.back().second, points.front().first, points.front().second, pen, false, stroke); +} + +const DesktopBrush &selected_brush(DesktopObjects &objects, const DesktopDC &dc) +{ + static DesktopBrush stock; + if (const auto *brush = objects.brush(dc.brush)) + return *brush; + return stock; +} + +const DesktopPen &selected_pen(DesktopObjects &objects, const DesktopDC &dc) +{ + static DesktopPen stock; + if (const auto *pen = objects.pen(dc.pen)) + return *pen; + return stock; +} + +namespace { + +// Spans of an ellipse or rounded rectangle for every row, as [first,last] inclusive logical columns. +struct Span { int64_t first, last; bool valid; }; + +std::vector outline_rows(int64_t left, int64_t top, int64_t right, int64_t bottom, int64_t round_w, int64_t round_h, bool rounded) +{ + const int64_t width = right - left, height = bottom - top; + std::vector rows(static_cast(height)); + const double rx = rounded ? std::min(round_w, double(width)) / 2.0 : width / 2.0; + const double ry = rounded ? std::min(round_h, double(height)) / 2.0 : height / 2.0; + for (int64_t row = 0; row < height; ++row) { + const double center_y = row + 0.5; + double inset = 0; + if (rounded) { + double dy = 0; + if (center_y < ry) + dy = ry - center_y; + else if (center_y > height - ry) + dy = center_y - (height - ry); + if (dy > 0 && ry > 0) + inset = rx - rx * std::sqrt(std::max(0.0, 1.0 - (dy / ry) * (dy / ry))); + rows[size_t(row)] = {left + int64_t(std::floor(inset + 0.5)), right - 1 - int64_t(std::floor(inset + 0.5)), true}; + } else { + const double dy = (center_y - height / 2.0) / (height / 2.0); + const double half = (width / 2.0) * std::sqrt(std::max(0.0, 1.0 - dy * dy)); + const double center_x = left + width / 2.0; + const int64_t first = int64_t(std::ceil(center_x - half - 0.5)), last = int64_t(std::floor(center_x + half - 0.5)); + rows[size_t(row)] = {first, std::max(first, last), true}; + } + } + return rows; +} + +} + +void Painter::ellipse(int64_t left, int64_t top, int64_t right, int64_t bottom, int64_t round_width, int64_t round_height, bool rounded) +{ + if (right <= left || bottom <= top || right - left > 65536 || bottom - top > 65536) + return; + const auto &brush = selected_brush(objects, dc); + const auto &pen = selected_pen(objects, dc); + const auto rows = outline_rows(left, top, right, bottom, round_width, round_height, rounded); + const bool outlined = !pen.hollow; + for (size_t i = 0; i < rows.size(); ++i) { + const auto &span = rows[i]; + const int64_t y = top + int64_t(i); + const bool edge_row = i == 0 || i + 1 == rows.size(); + int64_t from = span.first + (outlined ? 1 : 0), to = span.last - (outlined ? 1 : 0); + if (edge_row && outlined) + from = to + 1; + if (!brush.hollow && from <= to) + fill(from, y, to + 1, y + 1, brush); + } + if (!outlined) + return; + // The pen follows both flanks and spans the top and bottom rows, joining consecutive rows without gaps. + Stroke stroke; + int64_t previous_first = 0, previous_last = 0; + for (size_t i = 0; i < rows.size(); ++i) { + const auto &span = rows[i]; + const int64_t y = top + int64_t(i); + if (i == 0 || i + 1 == rows.size()) { + line(span.first, y, span.last, y, pen, true, stroke); + } else { + line(previous_first, y - 1, span.first, y, pen, true, stroke); + line(previous_last, y - 1, span.last, y, pen, true, stroke); + } + previous_first = span.first; + previous_last = span.last; + } +} + +void Painter::polygon(std::span> points) +{ + if (points.size() < 2) + return; + const auto &brush = selected_brush(objects, dc); + const auto &pen = selected_pen(objects, dc); + if (!brush.hollow && points.size() >= 3) { + int64_t min_y = INT64_MAX, max_y = INT64_MIN; + for (const auto &point : points) { + min_y = std::min(min_y, point.second); + max_y = std::max(max_y, point.second); + } + if (max_y - min_y > 65536) + throw uint32_t(120); + const bool even_odd = dc.poly_fill != 2; + for (int64_t y = min_y; y < max_y; ++y) { + // Crossings of the scan line through the pixel centers, with their winding direction. + std::vector> crossings; + for (size_t i = 0; i < points.size(); ++i) { + const auto &a = points[i], &b = points[(i + 1) % points.size()]; + if (a.second == b.second) + continue; + const double center = y + 0.5; + const double low = std::min(a.second, b.second), high = std::max(a.second, b.second); + if (center < low || center >= high) + continue; + const double t = (center - a.second) / (double(b.second) - a.second); + crossings.push_back({a.first + t * (double(b.first) - a.first), b.second > a.second ? 1 : -1}); + } + std::sort(crossings.begin(), crossings.end()); + int winding = 0; + for (size_t i = 0; i + 1 < crossings.size(); ++i) { + winding += crossings[i].second; + const bool inside = even_odd ? (i % 2 == 0) : winding != 0; + if (!inside) + continue; + const int64_t from = int64_t(std::ceil(crossings[i].first - 0.5)), to = int64_t(std::ceil(crossings[i + 1].first - 0.5)); + if (to > from) + fill(from, y, to, y + 1, brush); + } + } + } + polyline(points, pen, true); +} + + +namespace { + +struct Source { + DesktopBitmap *bitmap = nullptr; + DesktopDC *dc = nullptr; + uint32_t pixel(int64_t x, int64_t y, const DesktopDC &destination, bool destination_mono) const + { + const size_t row = bitmap->top_down ? size_t(y) : bitmap->height - 1 - size_t(y); + const auto pixels = bitmap->pixels(); + if (bitmap->depth == 1) { + const size_t stride = ((size_t(bitmap->width) + 15) / 16) * 2; + const bool white = (pixels[row * stride + size_t(x) / 8] >> (7 - x % 8)) & 1; + if (destination_mono) + return white ? 0xffffff : 0; + return white ? destination.background_color : destination.text_color; + } + const auto *p = &pixels[(row * bitmap->width + size_t(x)) * 4]; + const uint32_t color = uint32_t(p[2]) | uint32_t(p[1]) << 8 | uint32_t(p[0]) << 16; + // A color source copied to a monochrome surface turns its background color white and everything else black. + if (destination_mono) + return color == dc->background_color ? 0xffffff : 0; + return color; + } + const uint8_t *raw(int64_t x, int64_t y) const + { + const size_t row = bitmap->top_down ? size_t(y) : bitmap->height - 1 - size_t(y); + return &bitmap->pixels()[(row * bitmap->width + size_t(x)) * 4]; + } +}; + +} + +bool blit(DesktopObjects &objects, DesktopDC &destination, int32_t dx, int32_t dy, int32_t dw, int32_t dh, DesktopDC *source_dc, + int32_t sx, int32_t sy, int32_t sw, int32_t sh, uint32_t rop) +{ + Painter painter(objects, destination); + const uint32_t table = (rop >> 16) & 0xff; + const auto table_bit = [&](unsigned pattern, unsigned source, unsigned destination) { return (table >> (pattern << 2 | source << 1 | destination)) & 1; }; + bool uses_source = false, uses_pattern = false; + for (unsigned pattern = 0; pattern < 2; ++pattern) + for (unsigned destination = 0; destination < 2; ++destination) + uses_source = uses_source || table_bit(pattern, 0, destination) != table_bit(pattern, 1, destination); + for (unsigned source = 0; source < 2; ++source) + for (unsigned destination = 0; destination < 2; ++destination) + uses_pattern = uses_pattern || table_bit(0, source, destination) != table_bit(1, source, destination); + Source source; + if (uses_source) { + if (!source_dc) + throw uint32_t(6); + source.dc = source_dc; + source.bitmap = objects.bitmap(source_dc->bitmap); + if (!source.bitmap || (source.bitmap->depth != 32 && source.bitmap->depth != 1)) + throw uint32_t(120); + } + if (!dw || !dh) + return false; + const bool flip_x = (dw < 0) != (sw < 0), flip_y = (dh < 0) != (sh < 0); + const int64_t abs_dw = std::abs(int64_t(dw)), abs_dh = std::abs(int64_t(dh)); + const int64_t abs_sw = std::abs(int64_t(sw)), abs_sh = std::abs(int64_t(sh)); + const int64_t dest_left = dw < 0 ? int64_t(dx) + dw + 1 : dx, dest_top = dh < 0 ? int64_t(dy) + dh + 1 : dy; + const int64_t src_left = (sw < 0 ? int64_t(sx) + sw + 1 : sx) + (source_dc ? source_dc->origin_x : 0), + src_top = (sh < 0 ? int64_t(sy) + sh + 1 : sy) + (source_dc ? source_dc->origin_y : 0); + if (abs_dw > 32768 || abs_dh > 32768) + throw uint32_t(87); + const bool same_bitmap = uses_source && source.bitmap == &painter.bitmap; + std::vector snapshot; + DesktopBitmap snapshot_bitmap; + if (same_bitmap) { + const auto pixels = source.bitmap->pixels(); + snapshot.assign(pixels.begin(), pixels.end()); + snapshot_bitmap = *source.bitmap; + snapshot_bitmap.shared.reset(); + snapshot_bitmap.bits = std::move(snapshot); + source.bitmap = &snapshot_bitmap; + } + const DesktopBrush *brush = uses_pattern ? &selected_brush(objects, destination) : nullptr; + const int64_t brush_base_x = int64_t(destination.origin_x) - destination.user_dx + destination.brush_org_x; + const int64_t brush_base_y = int64_t(destination.origin_y) - destination.user_dy + destination.brush_org_y; + const bool destination_mono = painter.bitmap.depth == 1; + const bool fast_copy = rop == rop_srccopy && uses_source && abs_dw == abs_sw && abs_dh == abs_sh && !flip_x && !flip_y && + source.bitmap->depth == 32 && painter.bitmap.depth == 32; + for (int64_t row = 0; row < abs_dh; ++row) { + const int64_t device_y = dest_top + row + destination.origin_y; + if (device_y < 0 || device_y >= painter.bitmap.height) + continue; + const int64_t source_row_index = abs_dh == abs_sh ? row : row * abs_sh / abs_dh; + const int64_t source_y = src_top + (flip_y ? abs_sh - 1 - source_row_index : source_row_index); + if (uses_source && (source_y < 0 || source_y >= source.bitmap->height)) + continue; + for (int64_t column = 0; column < abs_dw; ++column) { + const int64_t device_x = dest_left + column + destination.origin_x; + if (device_x < 0 || device_x >= painter.bitmap.width || !painter.visible(device_x, device_y)) + continue; + int64_t source_x = 0; + if (uses_source) { + const int64_t source_col_index = abs_dw == abs_sw ? column : column * abs_sw / abs_dw; + source_x = src_left + (flip_x ? abs_sw - 1 - source_col_index : source_col_index); + if (source_x < 0 || source_x >= source.bitmap->width) + continue; + } + // Between 32-bit surfaces every byte, alpha included, goes through the raster operation. + if (painter.bitmap.depth == 32 && (!uses_source || source.bitmap->depth == 32)) { + uint8_t *target = painter.pixel_pointer(device_x, device_y); + uint8_t from[4] = {0, 0, 0, 0}, pattern[4] = {0, 0, 0, 0}; + if (uses_source) + std::memcpy(from, source.raw(source_x, source_y), 4); + if (brush) { + const auto pixel = brush_color_at(*brush, destination, device_x - brush_base_x, device_y - brush_base_y); + const uint32_t color = pixel.second ? pixel.first : painter.read(device_x, device_y); + pattern[0] = uint8_t(color_blue(color)); pattern[1] = uint8_t(color_green(color)); pattern[2] = uint8_t(color_red(color)); + } + for (int channel = 0; channel < 4; ++channel) + target[channel] = combine_ternary(rop, pattern[channel], from[channel], target[channel]); + painter.mark(); + continue; + } + (void)fast_copy; + const uint32_t destination_color = painter.read(device_x, device_y); + uint32_t source_color = uses_source ? source.pixel(source_x, source_y, destination, destination_mono) : 0; + uint32_t pattern_color = 0; + if (brush) { + const auto pixel = brush_color_at(*brush, destination, device_x - brush_base_x, device_y - brush_base_y); + // A transparent hatch pixel leaves the destination unchanged for pattern operations. + pattern_color = pixel.second ? pixel.first : destination_color; + } + uint32_t result = 0; + for (int channel = 0; channel < 3; ++channel) { + const int shift = channel * 8; + const uint8_t value = combine_ternary(rop, uint8_t(pattern_color >> shift), uint8_t(source_color >> shift), uint8_t(destination_color >> shift)); + result |= uint32_t(value) << shift; + } + painter.plot(device_x, device_y, result, 13); + } + } + return painter.changed(); +} + +} diff --git a/native/desktop/gdi.h b/native/desktop/gdi.h new file mode 100644 index 0000000..aa41513 --- /dev/null +++ b/native/desktop/gdi.h @@ -0,0 +1,65 @@ +#pragma once + +#include "objects.h" + +#include +#include +#include +#include + +namespace raven { + +constexpr uint32_t rop_srccopy = 0x00cc0020, rop_patcopy = 0x00f00021; + +// COLORREF 0x00bbggrr. +inline uint32_t color_red(uint32_t color) { return color & 0xff; } +inline uint32_t color_green(uint32_t color) { return (color >> 8) & 0xff; } +inline uint32_t color_blue(uint32_t color) { return (color >> 16) & 0xff; } + +// Combines two 8-bit channels with a ternary raster operation (pattern, source, destination). +uint8_t combine_ternary(uint32_t operation, uint8_t pattern, uint8_t source, uint8_t destination); + +// Drawing on one device context. Coordinates are logical; the painter adds the DC origin and honors every clip. +class Painter { +public: + // Throws a Windows error code when the DC has no drawable 32-bit surface. + Painter(DesktopObjects &objects, DesktopDC &dc); + DesktopObjects &objects; + DesktopDC &dc; + DesktopBitmap &bitmap; + + bool visible(int64_t device_x, int64_t device_y) const; + // A device-space pixel with raster operation applied; returns false when it was clipped away. + bool plot(int64_t device_x, int64_t device_y, uint32_t color, uint32_t rop2 = 13); + uint32_t read(int64_t device_x, int64_t device_y) const; + // Fills logical pixels [left,right) x [top,bottom) with a brush, aligned to the brush origin. + void fill(int64_t left, int64_t top, int64_t right, int64_t bottom, const DesktopBrush &brush); + // Draws a pen stroke between logical points, optionally including the last point. + struct Stroke { int64_t distance = 0; }; + void line(int64_t x0, int64_t y0, int64_t x1, int64_t y1, const DesktopPen &pen, bool include_end, Stroke &stroke); + void polyline(std::span> points, const DesktopPen &pen, bool closed); + void ellipse(int64_t left, int64_t top, int64_t right, int64_t bottom, int64_t round_width, int64_t round_height, bool rounded); + void polygon(std::span> points); + bool changed() const { return dirty; } + // Raw BGRA bytes of a 32-bit surface pixel (no clip test); mark() records that the caller modified it. + uint8_t *pixel_pointer(int64_t device_x, int64_t device_y) const; + void mark() { dirty = true; } + +private: + uint8_t *row_pointer(int64_t device_y) const; + void stamp(int64_t x, int64_t y, const DesktopPen &pen); + bool dirty = false; +}; + +// The color a brush paints at a pattern position (second value false when the pixel is left alone). +std::pair brush_color_at(const DesktopBrush &brush, const DesktopDC &dc, int64_t pattern_x, int64_t pattern_y); + +// Raster-operation transfer between device contexts (source may be null for pattern operations); true when pixels changed. +bool blit(DesktopObjects &objects, DesktopDC &destination, int32_t dx, int32_t dy, int32_t dw, int32_t dh, DesktopDC *source_dc, + int32_t sx, int32_t sy, int32_t sw, int32_t sh, uint32_t rop); + +// Resolves the brush and pen a DC has selected, including the stock DC brush and DC pen colors. +const DesktopBrush &selected_brush(DesktopObjects &objects, const DesktopDC &dc); +const DesktopPen &selected_pen(DesktopObjects &objects, const DesktopDC &dc); + +} diff --git a/native/desktop/gdi_api.cpp b/native/desktop/gdi_api.cpp new file mode 100644 index 0000000..8528a6c --- /dev/null +++ b/native/desktop/gdi_api.cpp @@ -0,0 +1,1366 @@ +#include "api.h" +#include "gdi.h" +#include "nt/api.h" +#include "nt/text.h" +#include + +#include +#include +#include +#include + +namespace raven { + +namespace { + +constexpr uint32_t clr_invalid = 0xffffffffU; + +struct Point { int32_t x, y; }; + +Point read_point(const ApiCall &call, uint64_t address) +{ + return {call.load(address), call.load(address + 4)}; +} + +DesktopRect read_rect(const ApiCall &call, uint64_t address) +{ + return {call.load(address), call.load(address + 4), call.load(address + 8), call.load(address + 12)}; +} + +void write_point(const ApiCall &call, uint64_t address, int32_t x, int32_t y) +{ + if (!address) + return; + call.store(address, x); + call.store(address + 4, y); +} + +// One decoded device-independent bitmap: top-down BGRA rows with no alpha semantics unless the source carried it. +struct DibImage { + int32_t width = 0, height = 0; + std::vector pixels; + bool has_alpha = false; +}; + +// Reads BITMAPINFO plus the pixel data and converts any supported depth to 32-bit pixels (0xAARRGGBB), rows top to bottom. +// Fewer rows than the image height may be supplied; they are the first rows in storage order and the others stay zero. +DibImage decode_dib(const ApiCall &call, uint64_t info, uint64_t bits, uint32_t usage, uint32_t rows_available = 0, int32_t *source_height_out = nullptr) +{ + const auto header_size = call.load(info); + if (header_size < 40) + throw uint32_t(87); + DibImage image; + const auto width = call.load(info + 4); + const auto height = call.load(info + 8); + const auto bit_count = call.load(info + 14); + const auto compression = call.load(info + 16); + auto colors_used = call.load(info + 32); + if (width <= 0 || !height || width > 16384 || std::abs(int64_t(height)) > 16384) + throw uint32_t(87); + if (compression != 0 && compression != 3) + throw uint32_t(120); + const bool top_down = height < 0; + const uint32_t rows = uint32_t(std::abs(int64_t(height))); + if (source_height_out) + *source_height_out = int32_t(rows); + if (!(bit_count == 1 || bit_count == 4 || bit_count == 8 || bit_count == 16 || bit_count == 24 || bit_count == 32)) + throw uint32_t(87); + uint64_t palette = info + header_size; + uint32_t masks[3] = {0, 0, 0}; + if (compression == 3) { + for (unsigned i = 0; i < 3; ++i) + masks[i] = call.load(palette + i * 4); + palette += header_size == 40 ? 12 : 0; + } + if (bit_count <= 8 && (!colors_used || colors_used > (1U << bit_count))) + colors_used = 1U << bit_count; + std::vector table(bit_count <= 8 ? 256 : 0, 0); + if (bit_count <= 8 && usage != 1) + for (uint32_t i = 0; i < colors_used; ++i) + table[i] = call.load(palette + i * 4) & 0xffffff; + const size_t stride = ((size_t(width) * bit_count + 31) / 32) * 4; + const uint32_t take = rows_available ? std::min(rows_available, rows) : rows; + image.width = width; + image.height = int32_t(rows); + image.pixels.assign(size_t(width) * rows, 0); + const auto payload = call.read(bits, stride * take); + for (uint32_t stored = 0; stored < take; ++stored) { + const uint32_t row = top_down ? stored : rows - 1 - stored; + const uint8_t *line = payload.data() + size_t(stored) * stride; + for (int32_t x = 0; x < width; ++x) { + uint32_t value = 0; + switch (bit_count) { + case 1: value = table[(line[x / 8] >> (7 - x % 8)) & 1]; break; + case 4: value = table[(line[x / 2] >> (x % 2 ? 0 : 4)) & 15]; break; + case 8: value = table[line[x]]; break; + case 16: { + const uint32_t word = line[x * 2] | (uint32_t(line[x * 2 + 1]) << 8); + if (compression == 3 && masks[1] == 0x7e0) + value = (((word >> 11) & 31) * 255 / 31) << 16 | (((word >> 5) & 63) * 255 / 63) << 8 | ((word & 31) * 255 / 31); + else + value = (((word >> 10) & 31) * 255 / 31) << 16 | (((word >> 5) & 31) * 255 / 31) << 8 | ((word & 31) * 255 / 31); + break; + } + case 24: value = line[x * 3 + 2] << 16 | line[x * 3 + 1] << 8 | line[x * 3]; break; + default: + value = uint32_t(line[x * 4 + 2]) << 16 | uint32_t(line[x * 4 + 1]) << 8 | line[x * 4] | uint32_t(line[x * 4 + 3]) << 24; + if (line[x * 4 + 3]) + image.has_alpha = true; + } + image.pixels[size_t(row) * width + x] = value; + } + } + return image; +} + +uint32_t colorref(uint32_t rgb) { return ((rgb & 0xff) << 16) | (rgb & 0xff00) | ((rgb >> 16) & 0xff); } + +// COLORREF to 0x00RRGGBB. +uint32_t to_rgb(uint32_t color) { return colorref(color); } + +} + +void NativeDesktop::register_gdi_api(ApiRegistry ®istry) +{ + // Marks the window behind a leased DC as changed so the next message retrieval presents it. + const auto touched = [this](const DesktopDC &dc) { + if (dc.window && dc.leased && updated) + updated(dc.window); + }; + const auto require_dc = [this](const ApiCall &call, size_t index) -> DesktopDC & { + auto *dc = objects.dc(call[index]); + if (!dc) + throw uint32_t(6); + return *dc; + }; + // Runs a drawing handler, translating thrown Windows errors into last-error and a zero return. + const auto guarded = [](ApiCall &call, uint64_t failure, const std::function &body) { + try { + body(); + } catch (const uint32_t &error) { + call.fail(error, failure); + } catch (const std::bad_alloc &) { + call.fail(8, failure); + } + }; + + // ---- device context state ---- + registry.add("SaveDC", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + DesktopDCState state; + state.bitmap = dc.bitmap; state.font = dc.font; state.brush = dc.brush; state.pen = dc.pen; + state.clip = dc.clip; state.application_clip = dc.application_clip; state.bounds = dc.bounds; + state.text_color = dc.text_color; state.background_color = dc.background_color; state.background_mode = dc.background_mode; + state.rop2 = dc.rop2; state.text_align = dc.text_align; state.poly_fill = dc.poly_fill; + state.origin_x = dc.origin_x; state.origin_y = dc.origin_y; state.position_x = dc.position_x; state.position_y = dc.position_y; + state.brush_org_x = dc.brush_org_x; state.brush_org_y = dc.brush_org_y; state.window_org_x = dc.window_org_x; + state.window_org_y = dc.window_org_y; state.viewport_org_x = dc.viewport_org_x; state.viewport_org_y = dc.viewport_org_y; + dc.saved.push_back(std::move(state)); + call.ret(dc.saved.size()); + }); + }); + registry.add("RestoreDC", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const int32_t requested = call.i32(1); + const int64_t index = requested < 0 ? int64_t(dc.saved.size()) + requested : int64_t(requested) - 1; + if (index < 0 || index >= int64_t(dc.saved.size())) + throw uint32_t(87); + const auto state = dc.saved[size_t(index)]; + dc.saved.resize(size_t(index)); + // Objects deleted while saved cannot be reselected; the DC keeps what it has. + if (objects.font(state.font)) objects.select_font(call[0], state.font); + if (objects.brush(state.brush)) objects.select_brush(call[0], state.brush); + if (objects.pen(state.pen)) objects.select_pen(call[0], state.pen); + dc.clip = state.clip; dc.application_clip = state.application_clip; dc.bounds = state.bounds; + dc.text_color = state.text_color; dc.background_color = state.background_color; dc.background_mode = state.background_mode; + dc.rop2 = state.rop2; dc.text_align = state.text_align; dc.poly_fill = state.poly_fill; + dc.origin_x = state.origin_x; dc.origin_y = state.origin_y; dc.position_x = state.position_x; dc.position_y = state.position_y; + dc.brush_org_x = state.brush_org_x; dc.brush_org_y = state.brush_org_y; dc.window_org_x = state.window_org_x; + dc.window_org_y = state.window_org_y; dc.viewport_org_x = state.viewport_org_x; dc.viewport_org_y = state.viewport_org_y; + dc.user_dx = state.viewport_org_x - state.window_org_x; dc.user_dy = state.viewport_org_y - state.window_org_y; + call.ret(1); + }); + }); + registry.add("SetTextAlign", 2, [=, this](ApiCall &call) { + guarded(call, clr_invalid, [&] { + auto &dc = require_dc(call, 0); + const auto previous = dc.text_align; + dc.text_align = call.u32(1); + call.ret(previous); + }); + }); + registry.add("GetTextAlign", 1, [=, this](ApiCall &call) { + guarded(call, clr_invalid, [&] { call.ret(require_dc(call, 0).text_align); }); + }); + registry.add("SetPolyFillMode", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + if (call.u32(1) != 1 && call.u32(1) != 2) + throw uint32_t(87); + const auto previous = dc.poly_fill; + dc.poly_fill = call.u32(1); + call.ret(previous); + }); + }); + registry.add("GetPolyFillMode", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { call.ret(require_dc(call, 0).poly_fill); }); + }); + registry.add("GetROP2", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { call.ret(require_dc(call, 0).rop2); }); + }); + registry.add("GetMapMode", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + require_dc(call, 0); + call.ret(1); + }); + }); + registry.add("SetMapMode", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + require_dc(call, 0); + // Only the default mapping exists; requesting another one fails visibly. + if (call.u32(1) != 1) + throw uint32_t(120); + call.ret(1); + }); + }); + registry.add("SetStretchBltMode", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + require_dc(call, 0); + call.ret(1); + }); + }); + registry.add("SetBrushOrgEx", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + write_point(call, call[3], dc.brush_org_x, dc.brush_org_y); + dc.brush_org_x = call.i32(1); + dc.brush_org_y = call.i32(2); + call.ret(1); + }); + }); + registry.add("GetBrushOrgEx", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + write_point(call, call[1], dc.brush_org_x, dc.brush_org_y); + call.ret(1); + }); + }); + // Window and viewport origins shift logical coordinates; the DC origin carries the sum and clip regions stay in device units. + const auto shift_origin = [this](DesktopDC &dc, int32_t window_x, int32_t window_y, int32_t viewport_x, int32_t viewport_y) { + const int32_t dx = (viewport_x - dc.viewport_org_x) - (window_x - dc.window_org_x); + const int32_t dy = (viewport_y - dc.viewport_org_y) - (window_y - dc.window_org_y); + dc.window_org_x = window_x; dc.window_org_y = window_y; dc.viewport_org_x = viewport_x; dc.viewport_org_y = viewport_y; + dc.origin_x += dx; dc.origin_y += dy; dc.user_dx += dx; dc.user_dy += dy; + }; + registry.add("SetViewportOrgEx", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + write_point(call, call[3], dc.viewport_org_x, dc.viewport_org_y); + shift_origin(dc, dc.window_org_x, dc.window_org_y, call.i32(1), call.i32(2)); + call.ret(1); + }); + }); + registry.add("OffsetViewportOrgEx", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + write_point(call, call[3], dc.viewport_org_x, dc.viewport_org_y); + shift_origin(dc, dc.window_org_x, dc.window_org_y, dc.viewport_org_x + call.i32(1), dc.viewport_org_y + call.i32(2)); + call.ret(1); + }); + }); + registry.add("SetWindowOrgEx", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + write_point(call, call[3], dc.window_org_x, dc.window_org_y); + shift_origin(dc, call.i32(1), call.i32(2), dc.viewport_org_x, dc.viewport_org_y); + call.ret(1); + }); + }); + registry.add("OffsetWindowOrgEx", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + write_point(call, call[3], dc.window_org_x, dc.window_org_y); + shift_origin(dc, dc.window_org_x + call.i32(1), dc.window_org_y + call.i32(2), dc.viewport_org_x, dc.viewport_org_y); + call.ret(1); + }); + }); + registry.add("GetViewportOrgEx", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + write_point(call, call[1], dc.viewport_org_x, dc.viewport_org_y); + call.ret(1); + }); + }); + registry.add("GetWindowOrgEx", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + write_point(call, call[1], dc.window_org_x, dc.window_org_y); + call.ret(1); + }); + }); + // Logical and device coordinates differ only by the window and viewport origins. + registry.add("DPtoLP", 3, [=, this](ApiCall &call) { + // An unknown device context fails silently, and a non-positive count has nothing to convert. + auto *dc = objects.dc(call[0]); + if (!dc) { + call.ret(0); + return; + } + const int32_t count = call.i32(2); + if (count > 65536) { + call.fail(120, 0); + return; + } + for (int32_t i = 0; i < count; ++i) { + const auto at = call[1] + uint64_t(i) * 8; + call.store(at, call.load(at) - dc->user_dx); + call.store(at + 4, call.load(at + 4) - dc->user_dy); + } + call.ret(1); + }); + registry.add("LPtoDP", 3, [=, this](ApiCall &call) { + // An unknown device context fails silently, and a non-positive count has nothing to convert. + auto *dc = objects.dc(call[0]); + if (!dc) { + call.ret(0); + return; + } + const int32_t count = call.i32(2); + if (count > 65536) { + call.fail(120, 0); + return; + } + for (int32_t i = 0; i < count; ++i) { + const auto at = call[1] + uint64_t(i) * 8; + call.store(at, call.load(at) + dc->user_dx); + call.store(at + 4, call.load(at + 4) + dc->user_dy); + } + call.ret(1); + }); + registry.add("GetNearestColor", 2, [=, this](ApiCall &call) { + guarded(call, clr_invalid, [&] { + require_dc(call, 0); + call.ret(call.u32(1) & 0xffffff); + }); + }); + registry.add("SelectPalette", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + require_dc(call, 0); + call.ret(0); + }); + }); + registry.add("GetPixel", 3, [=, this](ApiCall &call) { + guarded(call, clr_invalid, [&] { + auto &dc = require_dc(call, 0); + Painter painter(objects, dc); + const int64_t x = int64_t(call.i32(1)) + dc.origin_x, y = int64_t(call.i32(2)) + dc.origin_y; + call.ret(painter.visible(x, y) ? painter.read(x, y) : clr_invalid); + }); + }); + + // ---- brushes and pens ---- + registry.add("CreateHatchBrush", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + if (call.u32(0) > 5) + throw uint32_t(87); + DesktopBrush brush; + brush.color = call.u32(1) & 0xffffff; + brush.style = 2; + brush.hatch = call.u32(0); + const auto handle = objects.add(brush); + if (!handle) + throw uint32_t(8); + call.ret(handle); + }); + }); + registry.add("CreatePatternBrush", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const auto *bitmap = objects.bitmap(call[0]); + if (!bitmap) + throw uint32_t(6); + DesktopBrush brush; + brush.style = 3; + brush.pattern_width = bitmap->width; + brush.pattern_height = bitmap->height; + brush.pattern_mono = bitmap->depth == 1; + brush.pattern.assign(size_t(bitmap->width) * bitmap->height, 0); + const auto pixels = bitmap->pixels(); + for (uint32_t y = 0; y < bitmap->height; ++y) + for (uint32_t x = 0; x < bitmap->width; ++x) { + const size_t row = bitmap->top_down ? y : bitmap->height - 1 - y; + uint32_t value; + if (bitmap->depth == 1) { + const size_t stride = ((size_t(bitmap->width) + 15) / 16) * 2; + value = (pixels[row * stride + x / 8] >> (7 - x % 8)) & 1; + } else { + const auto *p = &pixels[(row * bitmap->width + x) * 4]; + value = uint32_t(p[2]) | uint32_t(p[1]) << 8 | uint32_t(p[0]) << 16; + } + brush.pattern[size_t(y) * bitmap->width + x] = value; + } + const auto handle = objects.add(std::move(brush)); + if (!handle) + throw uint32_t(8); + call.ret(handle); + }); + }); + const auto make_pen = [this](uint32_t style, int32_t width, uint32_t color, bool geometric) { + DesktopPen pen; + pen.color = color & 0xffffff; + pen.style = style; + pen.hollow = style == 5; + pen.width = width; + pen.geometric = geometric; + const auto handle = objects.add(pen); + if (!handle) + throw uint32_t(8); + return handle; + }; + registry.add("CreatePen", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + if ((call.u32(0) & 0xff) > 6 || call.i32(1) < 0) + throw uint32_t(87); + call.ret(make_pen(call.u32(0) & 0xff, call.i32(1), call.u32(2), false)); + }); + }); + registry.add("CreatePenIndirect", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const auto style = call.load(call[0]); + if ((style & 0xff) > 6) + throw uint32_t(87); + call.ret(make_pen(style & 0xff, call.load(call[0] + 4), call.load(call[0] + 12), false)); + }); + }); + registry.add("ExtCreatePen", 5, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const auto style = call.u32(0); + const bool geometric = (style & 0xf0000) == 0x10000; + if ((style & 0xff) > 7 || !call[2]) + throw uint32_t(87); + call.ret(make_pen(std::min(style & 0xff, 6), int32_t(call.u32(1)), call.load(call[2] + 4), geometric)); + }); + }); + + // ---- bitmaps ---- + registry.add("CreateBitmap", 5, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const int32_t width = call.i32(0), height = call.i32(1); + const uint32_t planes = call.u32(2), bits_per_pixel = call.u32(3); + if (width <= 0 || height <= 0 || width > 16384 || height > 16384 || planes != 1 || + !(bits_per_pixel == 1 || bits_per_pixel == 4 || bits_per_pixel == 8 || bits_per_pixel == 16 || bits_per_pixel == 24 || bits_per_pixel == 32)) + throw uint32_t(87); + const auto handle = allocate_bitmap(uint32_t(width), uint32_t(height), bits_per_pixel == 1 ? 1 : 32, false, true); + if (!handle) + return; + auto *bitmap = objects.bitmap(handle); + if (call[4]) { + // DDB bits are word-aligned rows, top row first. + const size_t stride = ((size_t(width) * bits_per_pixel + 15) / 16) * 2; + const auto source = call.read(call[4], stride * size_t(height)); + auto destination = bitmap->pixels(); + for (int32_t y = 0; y < height; ++y) + for (int32_t x = 0; x < width; ++x) { + const uint8_t *line = source.data() + size_t(y) * stride; + if (bits_per_pixel == 1) { + const size_t target_stride = ((size_t(width) + 15) / 16) * 2; + const bool set = (line[x / 8] >> (7 - x % 8)) & 1; + auto &byte = destination[size_t(y) * target_stride + x / 8]; + byte = uint8_t(set ? (byte | (0x80 >> (x % 8))) : (byte & ~(0x80 >> (x % 8)))); + continue; + } + uint8_t blue = 0, green = 0, red = 0; + switch (bits_per_pixel) { + case 32: blue = line[x * 4]; green = line[x * 4 + 1]; red = line[x * 4 + 2]; break; + case 24: blue = line[x * 3]; green = line[x * 3 + 1]; red = line[x * 3 + 2]; break; + case 16: { + const uint32_t word = line[x * 2] | (uint32_t(line[x * 2 + 1]) << 8); + red = uint8_t(((word >> 10) & 31) * 255 / 31); green = uint8_t(((word >> 5) & 31) * 255 / 31); blue = uint8_t((word & 31) * 255 / 31); + break; + } + default: { + const uint32_t shift = bits_per_pixel == 8 ? 0 : (x % 2 ? 0 : 4); + const uint32_t index = bits_per_pixel == 8 ? line[x] : (line[x / 2] >> shift) & 15; + blue = green = red = uint8_t(index * 255 / (bits_per_pixel == 8 ? 255 : 15)); + } + } + auto *p = &destination[(size_t(y) * width + x) * 4]; + p[0] = blue; p[1] = green; p[2] = red; p[3] = 0; + } + } else if (bitmap->depth == 1) { + // Uninitialized monochrome bitmaps start black. + } + call.ret(handle); + }); + }); + registry.add("SetBitmapBits", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto *bitmap = objects.bitmap(call[0]); + if (!bitmap) + throw uint32_t(6); + auto pixels = bitmap->pixels(); + const size_t size = std::min(call.u32(1), pixels.size()); + const auto data = call.read(call[2], size); + if (bitmap->top_down || bitmap->depth == 1) { + std::copy(data.begin(), data.end(), pixels.begin()); + } else { + const size_t stride = size_t(bitmap->width) * 4; + for (size_t offset = 0; offset < size; ++offset) { + const size_t row = offset / stride; + pixels[(bitmap->height - 1 - row) * stride + offset % stride] = data[offset]; + } + } + call.ret(size); + }); + }); + + // ---- regions ---- + const auto box_region = [](int32_t left, int32_t top, int32_t right, int32_t bottom) { + DesktopRegion region; + if (right > left && bottom > top) + region.add({left, top, right, bottom}); + return region; + }; + const auto adopt = [this](DesktopRegion region, ApiCall &call) { + const auto handle = objects.add(std::move(region)); + if (!handle) + throw uint32_t(8); + call.ret(handle); + }; + registry.add("CreateRectRgnIndirect", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const auto rect = read_rect(call, call[0]); + adopt(box_region(std::min(rect.left, rect.right), std::min(rect.top, rect.bottom), std::max(rect.left, rect.right), std::max(rect.top, rect.bottom)), call); + }); + }); + registry.add("CreateRoundRectRgn", 6, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const int64_t left = call.i32(0), top = call.i32(1), right = call.i32(2), bottom = call.i32(3); + if (right <= left || bottom <= top || right - left > 16384 || bottom - top > 16384) + throw uint32_t(87); + DesktopRegion region; + const double rx = std::min(std::abs(call.i32(4)), double(right - left)) / 2.0, ry = std::min(std::abs(call.i32(5)), double(bottom - top)) / 2.0; + for (int64_t y = top; y < bottom; ++y) { + const double row = double(y - top) + 0.5; + double inset = 0; + const double height = double(bottom - top); + double dy = row < ry ? ry - row : row > height - ry ? row - (height - ry) : 0; + if (dy > 0 && ry > 0) + inset = rx - rx * std::sqrt(std::max(0.0, 1.0 - (dy / ry) * (dy / ry))); + region.add({int32_t(left + std::floor(inset + 0.5)), int32_t(y), int32_t(right - std::floor(inset + 0.5)), int32_t(y + 1)}); + } + adopt(std::move(region), call); + }); + }); + registry.add("CreatePolygonRgn", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const int32_t count = call.i32(1); + if (count < 3 || count > 65536) + throw uint32_t(87); + std::vector points; + for (int32_t i = 0; i < count; ++i) + points.push_back(read_point(call, call[0] + uint64_t(i) * 8)); + int32_t min_y = INT32_MAX, max_y = INT32_MIN; + for (const auto &point : points) { + min_y = std::min(min_y, point.y); + max_y = std::max(max_y, point.y); + } + if (int64_t(max_y) - min_y > 16384) + throw uint32_t(120); + DesktopRegion region; + const bool even_odd = call.u32(2) != 2; + for (int32_t y = min_y; y < max_y; ++y) { + std::vector> crossings; + for (size_t i = 0; i < points.size(); ++i) { + const auto &a = points[i], &b = points[(i + 1) % points.size()]; + if (a.y == b.y) + continue; + const double center = y + 0.5; + if (center < std::min(a.y, b.y) || center >= std::max(a.y, b.y)) + continue; + const double t = (center - a.y) / (double(b.y) - a.y); + crossings.push_back({a.x + t * (double(b.x) - a.x), b.y > a.y ? 1 : -1}); + } + std::sort(crossings.begin(), crossings.end()); + int winding = 0; + for (size_t i = 0; i + 1 < crossings.size(); ++i) { + winding += crossings[i].second; + if (!(even_odd ? i % 2 == 0 : winding != 0)) + continue; + const int32_t from = int32_t(std::ceil(crossings[i].first - 0.5)), to = int32_t(std::ceil(crossings[i + 1].first - 0.5)); + if (to > from) + region.add({from, y, to, y + 1}); + } + } + adopt(std::move(region), call); + }); + }); + registry.add("ExtCreateRegion", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + if (call.u32(1) < 32 || !call[2]) + throw uint32_t(87); + // RGNDATA: RGNDATAHEADER (dwSize, iType, nCount, nRgnSize, rcBound) then RECTs. + const auto header_size = call.load(call[2]); + const auto count = call.load(call[2] + 8); + if (header_size != 32 || count > 100000 || call.u32(1) < 32 + count * 16) + throw uint32_t(87); + DesktopRegion region; + for (uint32_t i = 0; i < count; ++i) { + const auto rect = read_rect(call, call[2] + 32 + uint64_t(i) * 16); + if (rect.right > rect.left && rect.bottom > rect.top) + region.add(rect); + } + adopt(std::move(region), call); + }); + }); + registry.add("FillRgn", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto *region = objects.region(call[1]); + const auto *brush = objects.brush(call[2]); + if (!region || !brush) + throw uint32_t(6); + Painter painter(objects, dc); + const auto bounds = region->bounds(); + for (int32_t y = bounds.top; y < bounds.bottom; ++y) + for (int32_t x = bounds.left; x < bounds.right; ++x) + if (region->contains(x, y)) + painter.fill(x, y, x + 1, y + 1, *brush); + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + registry.add("FrameRgn", 5, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto *region = objects.region(call[1]); + const auto *brush = objects.brush(call[2]); + if (!region || !brush) + throw uint32_t(6); + Painter painter(objects, dc); + const int32_t w = std::max(1, call.i32(3)), h = std::max(1, call.i32(4)); + const auto bounds = region->bounds(); + for (int32_t y = bounds.top; y < bounds.bottom; ++y) + for (int32_t x = bounds.left; x < bounds.right; ++x) { + if (!region->contains(x, y)) + continue; + const bool edge = !region->contains(x - w, y) || !region->contains(x + w, y) || !region->contains(x, y - h) || !region->contains(x, y + h); + if (edge) + painter.fill(x, y, x + 1, y + 1, *brush); + } + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + registry.add("RectVisible", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto rect = read_rect(call, call[1]); + Painter painter(objects, dc); + bool any = false; + for (int32_t y = rect.top; y < rect.bottom && !any; ++y) + for (int32_t x = rect.left; x < rect.right && !any; ++x) + any = painter.visible(int64_t(x) + dc.origin_x, int64_t(y) + dc.origin_y); + call.ret(any); + }); + }); + + // ---- shapes ---- + registry.add("Rectangle", 5, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + Painter painter(objects, dc); + int64_t left = call.i32(1), top = call.i32(2), right = call.i32(3), bottom = call.i32(4); + if (right < left) std::swap(left, right); + if (bottom < top) std::swap(top, bottom); + const auto &brush = selected_brush(objects, dc); + const auto &pen = selected_pen(objects, dc); + if (right > left && bottom > top) { + if (!brush.hollow) + painter.fill(left + (pen.hollow ? 0 : 1), top + (pen.hollow ? 0 : 1), right - (pen.hollow ? 0 : 1), bottom - (pen.hollow ? 0 : 1), brush); + if (!pen.hollow) { + const std::pair corners[4] = {{int32_t(left), int32_t(top)}, {int32_t(right - 1), int32_t(top)}, + {int32_t(right - 1), int32_t(bottom - 1)}, {int32_t(left), int32_t(bottom - 1)}}; + painter.polyline(corners, pen, true); + } + } + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + registry.add("RoundRect", 7, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + Painter painter(objects, dc); + painter.ellipse(call.i32(1), call.i32(2), call.i32(3), call.i32(4), std::abs(call.i32(5)), std::abs(call.i32(6)), true); + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + registry.add("Ellipse", 5, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + Painter painter(objects, dc); + painter.ellipse(call.i32(1), call.i32(2), call.i32(3), call.i32(4), 0, 0, false); + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + const auto read_points = [](const ApiCall &call, uint64_t address, int32_t count) { + if (count < 0 || count > 65536) + throw uint32_t(87); + std::vector> points; + points.reserve(size_t(count)); + const auto bytes = call.read(address, size_t(count) * 8); + for (int32_t i = 0; i < count; ++i) { + int32_t x, y; + std::memcpy(&x, bytes.data() + size_t(i) * 8, 4); + std::memcpy(&y, bytes.data() + size_t(i) * 8 + 4, 4); + points.emplace_back(x, y); + } + return points; + }; + registry.add("Polygon", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto points = read_points(call, call[1], call.i32(2)); + if (points.size() < 2) + throw uint32_t(87); + Painter painter(objects, dc); + painter.polygon(points); + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + registry.add("Polyline", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto points = read_points(call, call[1], call.i32(2)); + if (points.size() < 2) + throw uint32_t(87); + Painter painter(objects, dc); + painter.polyline(points, selected_pen(objects, dc), false); + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + registry.add("PolylineTo", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + auto points = read_points(call, call[1], call.i32(2)); + if (points.empty()) + throw uint32_t(87); + points.insert(points.begin(), {dc.position_x, dc.position_y}); + Painter painter(objects, dc); + painter.polyline(points, selected_pen(objects, dc), false); + dc.position_x = points.back().first; + dc.position_y = points.back().second; + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + registry.add("PolyPolyline", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const uint32_t polylines = call.u32(3); + if (polylines > 4096) + throw uint32_t(87); + const auto counts = call.read(call[2], size_t(polylines) * 4); + Painter painter(objects, dc); + uint64_t cursor = call[1]; + for (uint32_t i = 0; i < polylines; ++i) { + uint32_t count; + std::memcpy(&count, counts.data() + size_t(i) * 4, 4); + const auto points = read_points(call, cursor, int32_t(count)); + painter.polyline(points, selected_pen(objects, dc), false); + cursor += uint64_t(count) * 8; + } + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + registry.add("LineTo", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + Painter painter(objects, dc); + Painter::Stroke stroke; + painter.line(dc.position_x, dc.position_y, call.i32(1), call.i32(2), selected_pen(objects, dc), false, stroke); + dc.position_x = call.i32(1); + dc.position_y = call.i32(2); + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + + // ---- block transfer ---- + const auto blit_to=[=, this](DesktopDC &destination, int32_t dx, int32_t dy, int32_t dw, int32_t dh, DesktopDC *source_dc, + int32_t sx, int32_t sy, int32_t sw, int32_t sh, uint32_t rop) { + if (blit(objects, destination, dx, dy, dw, dh, source_dc, sx, sy, sw, sh, rop)) + touched(destination); + }; + registry.add("BitBlt", 9, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &destination = require_dc(call, 0); + const uint32_t rop = call.u32(8); + DesktopDC *source = call[5] ? objects.dc(call[5]) : nullptr; + if (call[5] && !source) + throw uint32_t(6); + blit_to(destination, call.i32(1), call.i32(2), call.i32(3), call.i32(4), source, call.i32(6), call.i32(7), call.i32(3), call.i32(4), rop); + call.ret(1); + }); + }); + registry.add("StretchBlt", 11, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &destination = require_dc(call, 0); + DesktopDC *source = call[5] ? objects.dc(call[5]) : nullptr; + if (call[5] && !source) + throw uint32_t(6); + blit_to(destination, call.i32(1), call.i32(2), call.i32(3), call.i32(4), source, call.i32(6), call.i32(7), call.i32(8), call.i32(9), call.u32(10)); + call.ret(1); + }); + }); + registry.add("PatBlt", 6, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &destination = require_dc(call, 0); + blit_to(destination, call.i32(1), call.i32(2), call.i32(3), call.i32(4), nullptr, 0, 0, call.i32(3), call.i32(4), call.u32(5)); + call.ret(1); + }); + }); + registry.add("FillRect", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto rect = read_rect(call, call[1]); + uint64_t handle = call[2]; + // Small integers are system color indices plus one. + if (handle && handle <= 31) + handle = objects.system_brush(int32_t(handle - 1)); + const auto *brush = objects.brush(handle); + if (!brush) + throw uint32_t(handle > 0 && handle <= 32 ? 120 : 6); + Painter painter(objects, dc); + painter.fill(rect.left, rect.top, rect.right, rect.bottom, *brush); + if (painter.changed()) + touched(dc); + call.ret(1); + }); + }); + registry.add("FrameRect", 3, [=, this](ApiCall &call) { + auto *dc = objects.dc(call[0]); + const auto *brush = objects.brush(call[2]); + if (!dc || !brush) { + call.ret(0); + return; + } + guarded(call, 0, [&] { + const auto rect = read_rect(call, call[1]); + if (rect.right <= rect.left || rect.bottom <= rect.top) { + call.ret(0); + return; + } + Painter painter(objects, *dc); + painter.fill(rect.left, rect.top, rect.right, rect.top + 1, *brush); + painter.fill(rect.left, rect.bottom - 1, rect.right, rect.bottom, *brush); + painter.fill(rect.left, rect.top + 1, rect.left + 1, rect.bottom - 1, *brush); + painter.fill(rect.right - 1, rect.top + 1, rect.right, rect.bottom - 1, *brush); + if (painter.changed()) + touched(*dc); + call.ret(1); + }); + }); + registry.add("GdiAlphaBlend", 11, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &destination = require_dc(call, 0); + auto &source_dc = require_dc(call, 5); + Painter painter(objects, destination); + auto *source = objects.bitmap(source_dc.bitmap); + if (!source || source->depth != 32) + throw uint32_t(120); + const uint32_t blend = call.u32(10); + const uint32_t constant = (blend >> 16) & 0xff, format = (blend >> 24) & 0xff; + const int32_t dx = call.i32(1), dy = call.i32(2), dw = call.i32(3), dh = call.i32(4); + const int32_t sx = call.i32(6), sy = call.i32(7), sw = call.i32(8), sh = call.i32(9); + if (dw <= 0 || dh <= 0 || sw <= 0 || sh <= 0) + throw uint32_t(87); + const auto pixels = source->pixels(); + for (int32_t y = 0; y < dh; ++y) + for (int32_t x = 0; x < dw; ++x) { + const int64_t device_x = int64_t(dx) + x + destination.origin_x, device_y = int64_t(dy) + y + destination.origin_y; + if (!painter.visible(device_x, device_y)) + continue; + const int64_t source_x = sx + int64_t(x) * sw / dw, source_y = sy + int64_t(y) * sh / dh; + if (source_x < 0 || source_y < 0 || source_x >= source->width || source_y >= source->height) + continue; + const size_t row = source->top_down ? size_t(source_y) : source->height - 1 - size_t(source_y); + const auto *p = &pixels[(row * source->width + size_t(source_x)) * 4]; + uint32_t alpha = constant; + uint32_t blue = p[0], green = p[1], red = p[2]; + if (format & 1) { + // Premultiplied source: scale by the constant alpha, then composite with the pixel's own alpha. + const uint32_t pixel_alpha = p[3]; + blue = blue * constant / 255; green = green * constant / 255; red = red * constant / 255; + alpha = pixel_alpha * constant / 255; + const uint32_t destination_color = painter.read(device_x, device_y); + const uint32_t keep = 255 - alpha; + const uint32_t out_blue = blue + color_blue(destination_color) * keep / 255; + const uint32_t out_green = green + color_green(destination_color) * keep / 255; + const uint32_t out_red = red + color_red(destination_color) * keep / 255; + painter.plot(device_x, device_y, std::min(out_red, 255u) | std::min(out_green, 255u) << 8 | std::min(out_blue, 255u) << 16, 13); + continue; + } + const uint32_t destination_color = painter.read(device_x, device_y); + const uint32_t out_blue = (blue * alpha + color_blue(destination_color) * (255 - alpha)) / 255; + const uint32_t out_green = (green * alpha + color_green(destination_color) * (255 - alpha)) / 255; + const uint32_t out_red = (red * alpha + color_red(destination_color) * (255 - alpha)) / 255; + painter.plot(device_x, device_y, out_red | out_green << 8 | out_blue << 16, 13); + } + if (painter.changed()) + touched(destination); + call.ret(1); + }); + }); + + // ---- device-independent bitmaps ---- + registry.add("StretchDIBits", 13, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const int32_t dx = call.i32(1), dy = call.i32(2), dw = call.i32(3), dh = call.i32(4); + const int32_t sx = call.i32(5), sy = call.i32(6), sw = call.i32(7), sh = call.i32(8); + const auto image = decode_dib(call, call[10], call[9], call.u32(11)); + Painter painter(objects, dc); + if (!dw || !dh || !sw || !sh) { + call.ret(0); + return; + } + const int64_t abs_dw = std::abs(int64_t(dw)), abs_dh = std::abs(int64_t(dh)), abs_sw = std::abs(int64_t(sw)), abs_sh = std::abs(int64_t(sh)); + const bool flip_x = (dw < 0) != (sw < 0), flip_y = (dh < 0) != (sh < 0); + const int64_t dest_left = dw < 0 ? int64_t(dx) + dw + 1 : dx, dest_top = dh < 0 ? int64_t(dy) + dh + 1 : dy; + // DIB source rows count from the bottom: the origin is the lower-left corner for bottom-up images. + const bool top_down = call.load(call[10] + 8) < 0; + for (int64_t row = 0; row < abs_dh; ++row) + for (int64_t column = 0; column < abs_dw; ++column) { + const int64_t device_x = dest_left + column + dc.origin_x, device_y = dest_top + row + dc.origin_y; + if (!painter.visible(device_x, device_y)) + continue; + const int64_t scaled_x = abs_dw == abs_sw ? column : column * abs_sw / abs_dw; + const int64_t scaled_y = abs_dh == abs_sh ? row : row * abs_sh / abs_dh; + const int64_t image_x = sx + (flip_x ? abs_sw - 1 - scaled_x : scaled_x); + int64_t image_row = flip_y ? abs_sh - 1 - scaled_y : scaled_y; + // Rows of a top-down DIB use sy as the first line; rows of a bottom-up DIB count up from the bottom. + const int64_t image_y = top_down ? sy + image_row : image.height - 1 - (sy + (abs_sh - 1 - image_row)); + if (image_x < 0 || image_x >= image.width || image_y < 0 || image_y >= image.height) + continue; + painter.plot(device_x, device_y, colorref(image.pixels[size_t(image_y) * image.width + size_t(image_x)] & 0xffffff), 13); + } + if (painter.changed()) + touched(dc); + call.ret(uint64_t(abs_dh)); + }); + }); + registry.add("SetDIBits", 7, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto *bitmap = objects.bitmap(call[1]); + if (!bitmap || bitmap->depth != 32) + throw uint32_t(6); + const uint32_t start = call.u32(2), lines = call.u32(3); + int32_t rows = 0; + const auto image = decode_dib(call, call[5], call[4], call.u32(6), lines, &rows); + auto pixels = bitmap->pixels(); + const bool top_down = call.load(call[5] + 8) < 0; + uint32_t done = 0; + for (uint32_t row = 0; row < lines && row + start < uint32_t(rows) && row + start < bitmap->height; ++row, ++done) { + // Storage line start+row of the DIB lands on the bitmap row with the same top-to-bottom position. + const uint32_t image_row = top_down ? start + row : uint32_t(rows) - 1 - (start + row); + const size_t memory_row = bitmap->top_down ? image_row : size_t(bitmap->height) - 1 - image_row; + if (image_row >= uint32_t(image.height) || image_row >= bitmap->height) + continue; + for (int32_t x = 0; x < std::min(image.width, int32_t(bitmap->width)); ++x) { + auto *p = &pixels[(memory_row * bitmap->width + size_t(x)) * 4]; + const uint32_t value = image.pixels[size_t(image_row) * image.width + size_t(x)]; + p[0] = uint8_t(value); p[1] = uint8_t(value >> 8); p[2] = uint8_t(value >> 16); p[3] = uint8_t(value >> 24); + } + } + call.ret(done); + }); + }); + registry.add("GetDIBits", 7, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto *bitmap = objects.bitmap(call[1]); + if (!bitmap) + throw uint32_t(6); + const auto header_size = call.load(call[5]); + if (header_size < 40) + throw uint32_t(87); + auto bit_count = call.load(call[5] + 14); + if (!bit_count) { + // A query fills in the format: width, height, planes and bit count of the selected bitmap. + call.store(call[5] + 4, int32_t(bitmap->width)); + call.store(call[5] + 8, int32_t(bitmap->height)); + call.store(call[5] + 12, 1); + call.store(call[5] + 14, uint16_t(bitmap->depth == 1 ? 1 : 32)); + call.store(call[5] + 16, 0); + call.store(call[5] + 20, uint32_t(((size_t(bitmap->width) * (bitmap->depth == 1 ? 1 : 32) + 31) / 32) * 4 * bitmap->height)); + call.ret(1); + return; + } + if (!call[4]) { + call.ret(1); + return; + } + if (bit_count != 32 && bit_count != 24 && bit_count != 8 && bit_count != 1) + throw uint32_t(120); + const uint32_t start = call.u32(2), lines = call.u32(3); + const int32_t height_field = call.load(call[5] + 8); + const bool top_down = height_field < 0; + const size_t stride = ((size_t(bitmap->width) * bit_count + 31) / 32) * 4; + if (start > bitmap->height) + throw uint32_t(87); + const uint32_t take = std::min(lines, bitmap->height - start); + std::vector output(stride * take, 0); + const auto pixels = bitmap->pixels(); + for (uint32_t row = 0; row < take; ++row) { + const uint32_t image_row = start + row; + const size_t memory_row = bitmap->top_down ? image_row : bitmap->height - 1 - image_row; + // A bottom-up DIB stores the image's last row first. + const size_t out_row = top_down ? row : take - 1 - row; + for (uint32_t x = 0; x < bitmap->width; ++x) { + uint32_t color; + if (bitmap->depth == 1) { + const size_t source_stride = ((size_t(bitmap->width) + 15) / 16) * 2; + color = ((pixels[memory_row * source_stride + x / 8] >> (7 - x % 8)) & 1) ? 0xffffff : 0; + } else { + const auto *p = &pixels[(memory_row * bitmap->width + x) * 4]; + color = uint32_t(p[2]) | uint32_t(p[1]) << 8 | uint32_t(p[0]) << 16; + } + auto *target = &output[out_row * stride]; + if (bit_count == 32) { + target[x * 4] = uint8_t(color >> 16); target[x * 4 + 1] = uint8_t(color >> 8); target[x * 4 + 2] = uint8_t(color); target[x * 4 + 3] = 0; + } else if (bit_count == 24) { + target[x * 3] = uint8_t(color >> 16); target[x * 3 + 1] = uint8_t(color >> 8); target[x * 3 + 2] = uint8_t(color); + } else if (bit_count == 8) { + target[x] = uint8_t((color_red(color) * 30 + color_green(color) * 59 + color_blue(color) * 11) / 100); + } else { + target[x / 8] = uint8_t(target[x / 8] | ((color_red(color) + color_green(color) + color_blue(color) >= 384) ? (0x80 >> (x % 8)) : 0)); + } + } + } + call.write(call[4], output); + call.ret(take); + }); + }); + registry.add("GetDIBColorTable", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + require_dc(call, 0); + call.ret(0); + }); + }); + registry.add("SetDIBColorTable", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + require_dc(call, 0); + call.ret(0); + }); + }); + registry.add("PlayEnhMetaFile", 3, [=, this](ApiCall &call) { + call.fail(120, 0); + }); + + // ---- printing is not provided ---- + for (const auto *name : {"StartDocW", "StartDocA", "EndDoc", "StartPage", "EndPage", "AbortDoc"}) + registry.add(name, 2, [](ApiCall &call) { call.fail(120, 0); }); + registry.add("GdiGetCodePage", 1, [](ApiCall &call) { call.ret(1252); }); + registry.add("GetTextCharsetInfo", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + require_dc(call, 0); + if (call[1]) { + std::vector signature(24, 0); + call.write(call[1], signature); + } + call.ret(0); + }); + }); +} + + +void NativeDesktop::register_image_api(ApiRegistry ®istry) +{ + const auto guarded = [](ApiCall &call, uint64_t failure, const std::function &body) { + try { + body(); + } catch (const uint32_t &error) { + call.fail(error, failure); + } catch (const std::bad_alloc &) { + call.fail(8, failure); + } + }; + // A persistent guest page for names and small buffers that must be passed to resource lookups. + const auto page = [this]() { + if (!scratch) + scratch = process.map_memory(8192, PROT_READ | PROT_WRITE); + return scratch + 4096; + }; + // Resource names are integers or strings; an ANSI string is widened into the scratch page first. + const auto wide_name = [=, this](ApiCall &call, uint64_t name, bool ansi) -> uint64_t { + if (name < 0x10000 || !ansi) + return name; + const auto text = from_ansi(call.narrow(name, 512), 1252); + std::vector bytes((text.size() + 1) * 2, 0); + std::memcpy(bytes.data(), text.data(), text.size() * 2); + call.write(page(), bytes); + return page(); + }; + const auto make_bitmap = [this](const ApiCall &call, uint64_t info, uint64_t bits, bool keep_alpha) -> uint64_t { + const auto image = decode_dib(call, info, bits, 0); + const auto handle = allocate_bitmap(uint32_t(image.width), uint32_t(image.height), 32, false, true); + if (!handle) + throw uint32_t(8); + auto *bitmap = objects.bitmap(handle); + auto pixels = bitmap->pixels(); + for (size_t i = 0; i < image.pixels.size(); ++i) { + const uint32_t value = image.pixels[i]; + pixels[i * 4] = uint8_t(value); pixels[i * 4 + 1] = uint8_t(value >> 8); pixels[i * 4 + 2] = uint8_t(value >> 16); + pixels[i * 4 + 3] = keep_alpha && image.has_alpha ? uint8_t(value >> 24) : 0; + } + return handle; + }; + // Locates the pixel data of a packed DIB (header, optional masks, palette, then bits). + const auto dib_bits = [](const ApiCall &call, uint64_t info) -> uint64_t { + const auto header = call.load(info); + const auto bit_count = call.load(info + 14); + const auto compression = call.load(info + 16); + auto colors = call.load(info + 32); + if (bit_count <= 8 && (!colors || colors > (1U << bit_count))) + colors = 1U << bit_count; + return info + header + (bit_count <= 8 ? uint64_t(colors) * 4 : 0) + (compression == 3 && header == 40 ? 12 : 0); + }; + const auto bitmap_resource = [=, this](ApiCall &call, uint64_t instance, uint64_t name, bool keep_alpha) -> uint64_t { + const auto view = resources.view(instance, 2, name); + if (!view) + throw uint32_t(1814); + return make_bitmap(call, view->address, dib_bits(call, view->address), keep_alpha); + }; + const auto host_file = [=, this](const ApiCall &call, uint64_t name, bool ansi) -> std::vector { + const auto path = ansi ? from_ansi(call.narrow(name, 1024), 1252) : call.wide(name, 1024); + if (!read_host_file) + throw uint32_t(2); + const auto bytes = read_host_file(path); + if (!bytes) + throw uint32_t(2); + return *bytes; + }; + const auto icon_resource = [=, this](ApiCall &call, uint64_t instance, uint64_t name, uint32_t cx, uint32_t cy) -> uint64_t { + if (!scratch) + scratch = process.map_memory(8192, PROT_READ | PROT_WRITE); + const uint64_t forwarded[] = {instance, name, cx, cy, scratch}; + const auto status = load_icon(forwarded); + if (status) { + throw uint32_t((status & 0xffff0000U) == 0x80070000U ? status & 0xffffU : 1814); + } + return call.load(scratch); + }; + // ICO file: directory of images, each a DIB (without file header) or PNG. + const auto icon_file = [=, this](const std::vector &bytes, uint32_t cx, uint32_t cy) -> uint64_t { + if (bytes.size() < 22 || bytes[2] != 1) + throw uint32_t(13); + const unsigned count = bytes[4] | bytes[5] << 8; + size_t best = 0; + uint32_t best_distance = UINT32_MAX; + for (unsigned i = 0; i < count && 6 + (i + 1) * 16 <= bytes.size(); ++i) { + const uint32_t width = bytes[6 + i * 16] ? bytes[6 + i * 16] : 256, height = bytes[7 + i * 16] ? bytes[7 + i * 16] : 256; + const uint32_t distance = cx ? uint32_t(std::abs(int(width) - int(cx)) + std::abs(int(height) - int(cy))) : 1000 - width; + if (distance < best_distance) { + best_distance = distance; + best = i; + } + } + const uint32_t size = bytes[6 + best * 16 + 8] | bytes[6 + best * 16 + 9] << 8 | bytes[6 + best * 16 + 10] << 16 | uint32_t(bytes[6 + best * 16 + 11]) << 24; + const uint32_t offset = bytes[6 + best * 16 + 12] | bytes[6 + best * 16 + 13] << 8 | bytes[6 + best * 16 + 14] << 16 | uint32_t(bytes[6 + best * 16 + 15]) << 24; + if (offset > bytes.size() || size > bytes.size() - offset) + throw uint32_t(13); + const auto handle = create_icon(std::span(bytes.data() + offset, size), cx, cy, 0); + if (!handle) + throw uint32_t(environment.last_error()); + return handle; + }; + const auto load_image = [=, this](ApiCall &call, bool ansi) { + guarded(call, 0, [&] { + const uint32_t type = call.u32(2), flags = call.u32(5); + uint32_t cx = call.u32(3), cy = call.u32(4); + if (flags & 0x40) + cx = cy = 0; + if (flags & 0x10) { + const auto bytes = host_file(call, call[1], ansi); + if (type == 0) { + if (bytes.size() < 54 || bytes[0] != 'B') + throw uint32_t(13); + const auto buffer = process.map_memory((bytes.size() + 4095) & ~size_t(4095), PROT_READ | PROT_WRITE); + uint64_t handle = 0; + try { + process.write_memory(buffer, bytes); + handle = make_bitmap(call, buffer + 14, buffer + 14 + (dib_bits(call, buffer + 14) - (buffer + 14)), (flags & 0x2000) != 0); + } catch (...) { + process.unmap_memory(buffer); + throw; + } + process.unmap_memory(buffer); + call.ret(handle); + } else if (type == 1) { + call.ret(icon_file(bytes, cx, cy)); + } else { + call.ret(desktop_stock_cursor); + } + return; + } + const auto name = wide_name(call, call[1], ansi); + if (type == 0) + call.ret(bitmap_resource(call, call[0], name, (flags & 0x2000) != 0)); + else if (type == 1) + call.ret(icon_resource(call, call[0], name, cx, cy)); + else if (call[0] == 0 && name >= 32512 && name < 32512 + desktop_stock_cursor_count) + call.ret(desktop_stock_cursor + (name - 32512)); + else + // Module cursors are not rendered by the host; callers receive the arrow. + call.ret(desktop_stock_cursor); + }); + }; + registry.add("LoadImageW", 6, [=](ApiCall &call) { load_image(call, false); }); + registry.add("LoadImageA", 6, [=](ApiCall &call) { load_image(call, true); }); + const auto load_bitmap = [=, this](ApiCall &call, bool ansi) { + guarded(call, 0, [&] { call.ret(bitmap_resource(call, call[0], wide_name(call, call[1], ansi), false)); }); + }; + registry.add("LoadBitmapW", 2, [=](ApiCall &call) { load_bitmap(call, false); }); + registry.add("LoadBitmapA", 2, [=](ApiCall &call) { load_bitmap(call, true); }); + registry.add("LoadIconA", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + if (!call[0]) + throw uint32_t(120); + call.ret(icon_resource(call, call[0], wide_name(call, call[1], true), 0, 0)); + }); + }); + registry.add("LoadCursorA", 2, [=, this](ApiCall &call) { + if (!call[0] && call[1] >= 32512 && call[1] < 32512 + desktop_stock_cursor_count) + call.ret(desktop_stock_cursor + (call[1] - 32512)); + else + call.ret(desktop_stock_cursor); + }); + registry.add("LoadStringA", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const int32_t capacity = call.i32(3); + if (capacity <= 0 || capacity > 2040) { + call.ret(0); + return; + } + const uint64_t forwarded[] = {call[0], call[1], page(), uint64_t(capacity)}; + const auto length = load_string(forwarded); + if (!length) { + call.ret(0); + return; + } + const auto text = call.wide(page(), size_t(capacity)); + auto narrow = to_ansi(text, 1252); + call.put_narrow(call[2], narrow, size_t(capacity)); + call.ret(std::min(narrow.size(), size_t(capacity) - 1)); + }); + }); + registry.add("LoadMenuA", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + if (call[1] >= 0x10000) + throw uint32_t(120); + call.ret(menu_resource(call[0], call[1])); + }); + }); + registry.add("CopyImage", 5, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const uint32_t type = call.u32(1); + if (type == 0) { + const auto *source = objects.bitmap(call[0]); + if (!source) + throw uint32_t(1409); + const uint32_t width = call.u32(2) ? call.u32(2) : source->width, height = call.u32(3) ? call.u32(3) : source->height; + const auto handle = allocate_bitmap(width, height, source->depth, false, true); + if (!handle) + throw uint32_t(8); + auto *copy = objects.bitmap(handle); + if (source->depth == 32) { + const auto from = source->pixels(); + auto to = copy->pixels(); + for (uint32_t y = 0; y < height; ++y) + for (uint32_t x = 0; x < width; ++x) { + const size_t sy = source->top_down ? size_t(y) * source->height / height : source->height - 1 - size_t(y) * source->height / height; + const size_t sx = size_t(x) * source->width / width; + std::memcpy(&to[(size_t(y) * width + x) * 4], &from[(sy * source->width + sx) * 4], 4); + } + } else if (width == source->width && height == source->height) { + std::copy(source->pixels().begin(), source->pixels().end(), copy->pixels().begin()); + } + call.ret(handle); + } else { + const auto *icon = objects.icon(call[0]); + if (!icon && !(call[0] >= desktop_stock_cursor && call[0] < desktop_stock_cursor + desktop_stock_cursor_count)) + throw uint32_t(1402); + if (!icon) { + call.ret(call[0]); + return; + } + const auto handle = objects.add(*icon); + if (!handle) + throw uint32_t(8); + call.ret(handle); + } + }); + }); + registry.add("CreateIconIndirect", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const auto is_icon = call.load(call[0]); + const auto mask_handle = call.load(call[0] + 16), color_handle = call.load(call[0] + 24); + (void)is_icon; + const auto *mask = objects.bitmap(mask_handle); + const auto *color = objects.bitmap(color_handle); + if (!mask || !color || color->depth != 32) + throw uint32_t(120); + DesktopIcon icon{}; + icon.monochrome = false; + icon.shared = false; + auto &image = icon.image; + image.color.width = color->width; + image.color.height = color->height; + image.color.rgba.assign(size_t(color->width) * color->height * 4, 0); + image.and_mask.assign(size_t(color->width) * color->height, 0); + image.xor_mask.clear(); + const auto from = color->pixels(); + const auto bits = mask->pixels(); + const size_t stride = ((size_t(mask->width) + 15) / 16) * 2; + bool any_alpha = false; + for (uint32_t y = 0; y < color->height; ++y) + for (uint32_t x = 0; x < color->width; ++x) { + const size_t row = color->top_down ? y : color->height - 1 - y; + const auto *p = &from[(row * color->width + x) * 4]; + const size_t mrow = mask->top_down ? y : mask->height - 1 - y; + const bool transparent = x < mask->width && y < mask->height && ((bits[mrow * stride + x / 8] >> (7 - x % 8)) & 1); + const size_t at = (size_t(y) * color->width + x) * 4; + image.color.rgba[at] = p[2]; image.color.rgba[at + 1] = p[1]; image.color.rgba[at + 2] = p[0]; + image.color.rgba[at + 3] = p[3]; + any_alpha = any_alpha || p[3]; + image.and_mask[size_t(y) * color->width + x] = transparent; + } + image.has_alpha = any_alpha; + if (!any_alpha) + for (size_t i = 0; i < image.and_mask.size(); ++i) + image.color.rgba[i * 4 + 3] = image.and_mask[i] ? 0 : 255; + const auto handle = objects.add(std::move(icon)); + if (!handle) + throw uint32_t(8); + call.ret(handle); + }); + }); +} +} diff --git a/native/desktop/gdi_text.cpp b/native/desktop/gdi_text.cpp new file mode 100644 index 0000000..a6e6fdb --- /dev/null +++ b/native/desktop/gdi_text.cpp @@ -0,0 +1,532 @@ +#include "api.h" +#include "gdi.h" +#include "nt/text.h" + +#include +#include +#include +#include +#include + +#ifdef RAVEN_SKIA + +namespace raven { + +namespace { + +std::vector to_units(std::u16string_view text) +{ + return std::vector(text.begin(), text.end()); +} + +void write_size(const ApiCall &call, uint64_t address, int32_t width, int32_t height) +{ + if (!address) + return; + call.store(address, width); + call.store(address + 4, height); +} + +std::u16string widen(const ApiCall &call, uint64_t address, int32_t count, bool ansi) +{ + if (count < 0) + throw uint32_t(87); + if (count > 65536) + throw uint32_t(87); + if (!count) + return {}; + if (!ansi) { + const auto bytes = call.read(address, size_t(count) * 2); + std::u16string text(size_t(count), 0); + std::memcpy(text.data(), bytes.data(), bytes.size()); + return text; + } + const auto bytes = call.read(address, size_t(count)); + return from_ansi(std::string(bytes.begin(), bytes.end()), 1252); +} + +} + +void NativeDesktop::register_text_api(ApiRegistry ®istry) +{ + const auto require_dc = [this](const ApiCall &call, size_t index) -> DesktopDC & { + auto *dc = objects.dc(call[index]); + if (!dc) + throw uint32_t(6); + return *dc; + }; + const auto selected_font = [this](const DesktopDC &dc) -> const DesktopFont & { + const auto *font = objects.font(dc.font); + if (!font) + throw uint32_t(6); + return *font; + }; + const auto guarded = [](ApiCall &call, uint64_t failure, const std::function &body) { + try { + body(); + } catch (const uint32_t &error) { + call.fail(error, failure); + } catch (const std::bad_alloc &) { + call.fail(8, failure); + } + }; + // Cumulative advances: the qualified measurement when the font is inside the measured subset, the general one otherwise. + const auto measured_advances = [this](std::span definition, std::span units) { + try { + return objects.text().advances(definition, units); + } catch (const uint32_t &error) { + if (std::getenv("RAVEN_TRACE_API")) fprintf(stderr, "logical fallback after %u\n", error); + return objects.text().logical_advances(definition, units); + } + }; + // Draws one run of text with GDI alignment, background and clipping rules; returns the run's advance width. + const auto draw_run = [this, measured_advances](DesktopDC &dc, int32_t x, int32_t y, uint32_t options, const DesktopRect *clip_rect, + std::u16string_view text, std::span deltas) -> int32_t { + Painter painter(objects, dc); + const auto &font = *objects.font(dc.font); + const auto metrics = objects.text().logical_metrics(font.definition); + const auto units = to_units(text); + int32_t advance = 0; + std::vector offsets; + if (!units.empty()) { + if (!deltas.empty()) { + int32_t position = 0; + for (size_t i = 0; i < units.size(); ++i) { + offsets.push_back(position); + position += deltas[i]; + } + advance = position; + } else { + const auto cumulative = measured_advances(font.definition, units); + advance = cumulative.back(); + } + } + // Horizontal alignment moves the starting point by the run width; vertical alignment by the cell. + const uint32_t align = dc.text_align; + if ((align & 6) == 6) + x -= advance / 2; + else if (align & 2) + x -= advance; + if ((align & 24) == 24) + y -= metrics.ascent; + else if (align & 8) + y -= metrics.height; + const int64_t device_x = int64_t(x) + dc.origin_x, device_y = int64_t(y) + dc.origin_y; + DesktopRect clip{INT32_MIN, INT32_MIN, INT32_MAX, INT32_MAX}; + if (clip_rect && (options & 4)) + clip = *clip_rect; + if (clip_rect && (options & 2)) { + DesktopBrush brush; + brush.color = dc.background_color; + painter.fill(clip_rect->left, clip_rect->top, clip_rect->right, clip_rect->bottom, brush); + } + if (!units.empty() && dc.background_mode == 2) { + DesktopBrush brush; + brush.color = dc.background_color; + const int32_t left = std::min(x, x + advance), right = std::max(x, x + advance); + painter.fill(std::max(left, clip.left), std::max(y, clip.top), std::min(right, clip.right), std::min(y + metrics.height, clip.bottom), brush); + } + if (units.empty() || advance == 0) + return advance; + const int32_t margin = 4; + const uint32_t box_width = uint32_t(std::min(int64_t(advance) + 2 * margin + 2 * metrics.height, 16384)); + const uint32_t box_height = uint32_t(metrics.height + 4); + std::vector coverage; + try { + // Qualified fonts keep their measured monochrome glyphs; everything else is anti-aliased. + try { + std::vector> positions; + for (const auto offset : offsets) + positions.push_back({offset, 0}); + const auto binary = objects.text().mask(font.definition, units, box_width, box_height, margin, 2, positions); + coverage.assign(binary.size(), 0); + for (size_t i = 0; i < binary.size(); ++i) + coverage[i] = binary[i] ? 255 : 0; + } catch (const uint32_t &) { + coverage = objects.text().render(font.definition, units, box_width, box_height, margin, 2, offsets); + } + } catch (const uint32_t &) { + return advance; + } + const bool underline = font.definition[21] != 0, strike = font.definition[22] != 0; + const uint32_t color = dc.text_color; + for (uint32_t row = 0; row < box_height; ++row) + for (uint32_t column = 0; column < box_width; ++column) { + const int64_t px = device_x - margin + column, py = device_y - 2 + row; + const int64_t logical_x = px - dc.origin_x, logical_y = py - dc.origin_y; + if (logical_x < clip.left || logical_x >= clip.right || logical_y < clip.top || logical_y >= clip.bottom) + continue; + const uint8_t alpha = coverage[size_t(row) * box_width + column]; + if (!alpha) + continue; + if (alpha >= 250 || painter.bitmap.depth == 1) { + painter.plot(px, py, color, 13); + continue; + } + if (!painter.visible(px, py)) + continue; + const uint32_t below = painter.read(px, py); + const auto mix = [&](uint32_t foreground, uint32_t background) { return (foreground * alpha + background * (255 - alpha) + 127) / 255; }; + painter.plot(px, py, mix(color_red(color), color_red(below)) | mix(color_green(color), color_green(below)) << 8 | mix(color_blue(color), color_blue(below)) << 16, 13); + } + const int32_t thickness = std::max(1, int32_t(std::lround(metrics.height / 16.0))); + const auto line_across = [&](int32_t line_y) { + for (int32_t t = 0; t < thickness; ++t) + for (int32_t dx = 0; dx < advance; ++dx) { + const int64_t px = device_x + dx, py = device_y + line_y + t; + const int64_t logical_x = px - dc.origin_x, logical_y = py - dc.origin_y; + if (logical_x < clip.left || logical_x >= clip.right || logical_y < clip.top || logical_y >= clip.bottom) + continue; + painter.plot(px, py, color, 13); + } + }; + if (underline) + line_across(metrics.ascent + std::max(1, metrics.descent / 3)); + if (strike) + line_across(metrics.ascent * 3 / 5); + if (painter.changed() && dc.window && dc.leased && updated) + updated(dc.window); + return advance; + }; + const auto extended_text_out = [=, this](ApiCall &call, bool ansi) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + selected_font(dc); + const uint32_t options = call.u32(3); + const int32_t count = call.i32(6); + std::optional rect; + if (call[4] && (options & 6)) + rect = DesktopRect{call.load(call[4]), call.load(call[4] + 4), call.load(call[4] + 8), call.load(call[4] + 12)}; + if ((options & 2) && !rect) + throw uint32_t(87); + if (count < 0) + throw uint32_t(87); + const auto text = count > 0 ? widen(call, call[5], count, ansi) : std::u16string(); + std::vector deltas; + if (call[7] && count > 0) { + const bool pair = (options & 0x2000) != 0; + const auto bytes = call.read(call[7], size_t(count) * (pair ? 8 : 4)); + for (int32_t i = 0; i < count; ++i) { + int32_t value; + std::memcpy(&value, bytes.data() + size_t(i) * (pair ? 8 : 4), 4); + deltas.push_back(value); + } + } + int32_t x = call.i32(1), y = call.i32(2); + if (dc.text_align & 1) { + x = dc.position_x; + y = dc.position_y; + } + const auto advance = draw_run(dc, x, y, options, rect ? &*rect : nullptr, text, deltas); + if (dc.text_align & 1) + dc.position_x = x + advance; + call.ret(1); + }); + }; + registry.add("ExtTextOutW", 8, [=](ApiCall &call) { extended_text_out(call, false); }); + registry.add("ExtTextOutA", 8, [=](ApiCall &call) { extended_text_out(call, true); }); + const auto text_out = [=, this](ApiCall &call, bool ansi) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + selected_font(dc); + if (call.i32(4) < 0) + throw uint32_t(87); + const auto text = call.i32(4) > 0 ? widen(call, call[3], call.i32(4), ansi) : std::u16string(); + int32_t x = call.i32(1), y = call.i32(2); + if (dc.text_align & 1) { + x = dc.position_x; + y = dc.position_y; + } + const auto advance = draw_run(dc, x, y, 0, nullptr, text, {}); + if (dc.text_align & 1) + dc.position_x = x + advance; + call.ret(1); + }); + }; + registry.add("TextOutW", 5, [=](ApiCall &call) { text_out(call, false); }); + registry.add("TextOutA", 5, [=](ApiCall &call) { text_out(call, true); }); + + // ---- measurements ---- + const auto extent = [=, this](ApiCall &call, bool ansi) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto &font = selected_font(dc); + const auto text = widen(call, call[1], call.i32(2), ansi); + if (text.empty()) { + write_size(call, call[3], 0, 0); + call.ret(1); + return; + } + const auto cumulative = measured_advances(font.definition, to_units(text)); + write_size(call, call[3], cumulative.back(), objects.text().logical_metrics(font.definition).height); + call.ret(1); + }); + }; + for (const auto *name : {"GetTextExtentPoint32W", "GetTextExtentPointW"}) + registry.add(name, 4, [=](ApiCall &call) { extent(call, false); }); + for (const auto *name : {"GetTextExtentPoint32A", "GetTextExtentPointA"}) + registry.add(name, 4, [=](ApiCall &call) { extent(call, true); }); + const auto extent_ex = [=, this](ApiCall &call, bool ansi) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto &font = selected_font(dc); + const auto text = widen(call, call[1], call.i32(2), ansi); + const int32_t max_extent = call.i32(3); + std::vector cumulative; + if (!text.empty()) + cumulative = measured_advances(font.definition, to_units(text)); + int32_t fit = 0; + if (call[4]) { + fit = int32_t(cumulative.size()); + if (max_extent >= 0) + for (size_t i = 0; i < cumulative.size(); ++i) + if (cumulative[i] > max_extent) { + fit = int32_t(i); + break; + } + call.store(call[4], fit); + } + if (call[5]) + for (size_t i = 0; i < cumulative.size(); ++i) + call.store(call[5] + i * 4, cumulative[i]); + write_size(call, call[6], cumulative.empty() ? 0 : cumulative.back(), cumulative.empty() ? 0 : objects.text().logical_metrics(font.definition).height); + call.ret(1); + }); + }; + registry.add("GetTextExtentExPointW", 7, [=](ApiCall &call) { extent_ex(call, false); }); + registry.add("GetTextExtentExPointA", 7, [=](ApiCall &call) { extent_ex(call, true); }); + const auto text_metrics = [=, this](ApiCall &call, bool ansi) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto &font = selected_font(dc); + const auto info = objects.text().logical_metrics(font.definition); + std::vector bytes(ansi ? 56 : 60, 0); + const auto put32 = [&](size_t at, int32_t value) { std::memcpy(bytes.data() + at, &value, 4); }; + put32(0, info.height); put32(4, info.ascent); put32(8, info.descent); put32(12, info.internal_leading); + put32(16, info.external_leading); put32(20, info.average_width); put32(24, info.maximum_width); + uint32_t weight; + std::memcpy(&weight, font.definition.data() + 16, 4); + put32(28, int32_t(weight ? weight : 400)); + put32(36, 96); put32(40, 96); + size_t at = 44; + if (ansi) { + bytes[at] = uint8_t(info.first); bytes[at + 1] = uint8_t(std::min(info.last, 255)); bytes[at + 2] = uint8_t(info.default_character); bytes[at + 3] = uint8_t(info.break_character); + at += 4; + } else { + const uint16_t chars[4] = {info.first, info.last, info.default_character, info.break_character}; + std::memcpy(bytes.data() + at, chars, 8); + at += 8; + } + bytes[at] = font.definition[20]; + bytes[at + 1] = font.definition[21]; + bytes[at + 2] = font.definition[22]; + bytes[at + 3] = info.pitch; + bytes[at + 4] = font.definition[23]; + call.write(call[1], bytes); + call.ret(1); + }); + }; + registry.add("GetTextMetricsW", 2, [=](ApiCall &call) { text_metrics(call, false); }); + registry.add("GetTextMetricsA", 2, [=](ApiCall &call) { text_metrics(call, true); }); + registry.add("GetCharWidthW", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto &font = selected_font(dc); + const uint32_t first = call.u32(1), last = call.u32(2); + if (last < first || last - first > 65535) + throw uint32_t(87); + for (uint32_t c = first; c <= last; ++c) { + const uint16_t unit = uint16_t(c); + const auto advance = measured_advances(font.definition, std::span(&unit, 1)); + call.store(call[3] + uint64_t(c - first) * 4, advance.empty() ? 0 : advance[0]); + } + call.ret(1); + }); + }); + registry.add("GetCharABCWidthsW", 4, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto &font = selected_font(dc); + const uint32_t first = call.u32(1), last = call.u32(2); + if (last < first || last - first > 65535) + throw uint32_t(87); + for (uint32_t c = first; c <= last; ++c) { + const uint16_t unit = uint16_t(c); + const auto advance = measured_advances(font.definition, std::span(&unit, 1)); + // ABC spacing: no side bearings, so the whole advance is the glyph body. + const uint64_t at = call[3] + uint64_t(c - first) * 12; + call.store(at, 0); + call.store(at + 4, uint32_t(advance.empty() ? 0 : advance[0])); + call.store(at + 8, 0); + } + call.ret(1); + }); + }); + registry.add("GetCharWidthInfo", 2, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + require_dc(call, 0); + // CHAR_WIDTH_INFO: left bearing, right bearing, and the run width in device units. + call.store(call[1], 0); + call.store(call[1] + 4, 0); + call.store(call[1] + 8, 0); + call.ret(1); + }); + }); + registry.add("GdiGetCharDimensions", 3, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + auto &dc = require_dc(call, 0); + const auto &font = selected_font(dc); + const auto info = objects.text().logical_metrics(font.definition); + static const std::u16string alphabet = u"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const auto cumulative = measured_advances(font.definition, to_units(alphabet)); + if (call[1]) { + std::vector bytes(60, 0); + const auto put32 = [&](size_t at, int32_t value) { std::memcpy(bytes.data() + at, &value, 4); }; + put32(0, info.height); put32(4, info.ascent); put32(8, info.descent); put32(20, info.average_width); put32(24, info.maximum_width); + call.write(call[1], bytes); + } + if (call[2]) + call.store(call[2], info.height); + call.ret(uint64_t((cumulative.back() / 26 + 1) / 2)); + }); + }); + + // ---- fonts ---- + const auto add_font = [this](const std::array &definition) { + DesktopFont font; + font.definition = definition; + const auto handle = objects.add(font); + if (!handle) + throw uint32_t(8); + return handle; + }; + registry.add("CreateFontIndirectW", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const auto bytes = call.read(call[0], 92); + std::array definition{}; + std::copy(bytes.begin(), bytes.end(), definition.begin()); + call.ret(add_font(definition)); + }); + }); + registry.add("CreateFontIndirectA", 1, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + const auto bytes = call.read(call[0], 60); + std::array definition{}; + std::copy(bytes.begin(), bytes.begin() + 28, definition.begin()); + std::string face; + for (size_t i = 0; i < 32 && bytes[28 + i]; ++i) + face.push_back(char(bytes[28 + i])); + const auto wide = from_ansi(face, 1252); + for (size_t i = 0; i < std::min(wide.size(), 31); ++i) { + definition[28 + i * 2] = uint8_t(wide[i]); + definition[29 + i * 2] = uint8_t(wide[i] >> 8); + } + call.ret(add_font(definition)); + }); + }); + registry.add("CreateFontW", 14, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + std::array definition{}; + const auto put32 = [&](size_t at, int32_t value) { std::memcpy(definition.data() + at, &value, 4); }; + put32(0, call.i32(0)); put32(4, call.i32(1)); put32(8, call.i32(2)); put32(12, call.i32(3)); put32(16, call.i32(4)); + definition[20] = uint8_t(call.u32(5)); definition[21] = uint8_t(call.u32(6)); definition[22] = uint8_t(call.u32(7)); + definition[23] = uint8_t(call.u32(8)); definition[24] = uint8_t(call.u32(9)); definition[25] = uint8_t(call.u32(10)); + definition[26] = uint8_t(call.u32(11)); definition[27] = uint8_t(call.u32(12)); + if (call[13]) { + const auto face = call.wide(call[13], 64); + for (size_t i = 0; i < std::min(face.size(), 31); ++i) { + definition[28 + i * 2] = uint8_t(face[i]); + definition[29 + i * 2] = uint8_t(face[i] >> 8); + } + } + call.ret(add_font(definition)); + }); + }); + // The bundled families stand for the common Windows faces; callers see the usual names. + registry.add("EnumFontFamiliesExW", 5, [=, this](ApiCall &call) { + guarded(call, 0, [&] { + require_dc(call, 0); + static const char16_t *families[] = {u"Arial", u"Courier New", u"Times New Roman", u"MS Shell Dlg", u"MS Sans Serif", u"Tahoma", + u"Segoe UI", u"Verdana", u"Lucida Console", u"Consolas"}; + std::u16string wanted; + if (call[1]) { + const auto bytes = call.read(call[1] + 28, 64); + for (size_t i = 0; i < 32; ++i) { + const char16_t c = char16_t(bytes[i * 2] | (bytes[i * 2 + 1] << 8)); + if (!c) + break; + wanted.push_back(c); + } + } + if (!call[2]) { + call.ret(0); + return; + } + const auto lower = [](std::u16string text) { + for (auto &c : text) + if (c >= 'A' && c <= 'Z') + c = char16_t(c + 32); + return text; + }; + uint64_t result = 1; + const auto scratch_size = uint64_t(4096); + const auto scratch = process.map_memory(scratch_size, PROT_READ | PROT_WRITE); + try { + for (const auto *family : families) { + if (!wanted.empty() && lower(family) != lower(wanted)) + continue; + // ENUMLOGFONTEXW (LOGFONTW, full name, style, script) followed by NEWTEXTMETRICEXW. + std::vector record(512, 0); + int32_t height = -12; + std::memcpy(record.data(), &height, 4); + const int32_t weight = 400; + std::memcpy(record.data() + 16, &weight, 4); + record[23] = 0; + record[27] = std::u16string_view(family) == u"Courier New" || std::u16string_view(family) == u"Lucida Console" || std::u16string_view(family) == u"Consolas" ? 0x31 : 0x22; + for (size_t i = 0; i < std::char_traits::length(family) && i < 31; ++i) { + record[28 + i * 2] = uint8_t(family[i]); + record[29 + i * 2] = uint8_t(family[i] >> 8); + record[92 + i * 2] = uint8_t(family[i]); + record[93 + i * 2] = uint8_t(family[i] >> 8); + } + const char16_t style[] = u"Regular", script[] = u"Western"; + for (size_t i = 0; i < 7; ++i) { + record[92 + 128 + i * 2] = uint8_t(style[i]); + record[92 + 192 + i * 2] = uint8_t(script[i]); + } + call.write(scratch, record); + const uint64_t arguments[] = {scratch, scratch + 348, 4, call[3]}; + const auto outcome = invoke(call[2], arguments); + if (outcome.stop != NativeStop::returned) + throw outcome; + result = outcome.value; + if (!uint32_t(outcome.value)) + break; + } + } catch (...) { + process.unmap_memory(scratch); + throw; + } + process.unmap_memory(scratch); + call.ret(uint32_t(result)); + }); + }); + // Complex-script shaping is not provided; callers fall back to plain GDI text. + for (const auto *name : {"ScriptBreak", "ScriptStringAnalyse", "ScriptStringCPtoX", "ScriptStringFree", "ScriptStringGetLogicalWidths", + "ScriptStringOut", "ScriptStringXtoCP", "ScriptString_pSize", "ScriptString_pcOutChars"}) + registry.add(name, 8, [](ApiCall &call) { call.ret(0x80004001); }); +} + +} + +#else + +namespace raven { + +void NativeDesktop::register_text_api(ApiRegistry &) +{ +} + +} + +#endif diff --git a/native/desktop/hierarchy.cpp b/native/desktop/hierarchy.cpp index b773a87..a0147d8 100644 --- a/native/desktop/hierarchy.cpp +++ b/native/desktop/hierarchy.cpp @@ -76,7 +76,7 @@ void DesktopObjects::configure_window_dc(uint64_t handle,DesktopDC &device,bool const auto x=frame ? record->outer.left : record->x,y=frame ? record->outer.top : record->y; const auto dx=int64_t(x)-root->outer.left,dy=int64_t(y)-root->outer.top; if (dxINT32_MAX || dyINT32_MAX) throw uint32_t(120); - device.origin_x=int32_t(dx);device.origin_y=int32_t(dy); + device.origin_x=int32_t(dx+device.user_dx);device.origin_y=int32_t(dy+device.user_dy); if (frame && !record->parent) { device.bounds.reset();return; } int64_t left=0,top=0,right=frame ? int64_t(record->outer.right)-x : record->width; int64_t bottom=frame ? int64_t(record->outer.bottom)-y : record->height; diff --git a/native/desktop/objects.h b/native/desktop/objects.h index 97f6ad9..122a084 100644 --- a/native/desktop/objects.h +++ b/native/desktop/objects.h @@ -61,11 +61,31 @@ inline constexpr uint32_t desktop_stock_cursor_count = 140; struct DesktopBrush { uint32_t color = 0; bool hollow = false,pending_delete = false; + // BS_HATCHED selects a hatch pattern drawn in color; BS_PATTERN and BS_DIBPATTERN carry their own pixels. + uint32_t style = 0,hatch = 0; + uint32_t pattern_width = 0,pattern_height = 0; + // A monochrome pattern stores 0 for the text color and 1 for the background color. + bool pattern_mono = false; + std::vector pattern{}; }; struct DesktopPen { uint32_t color=0; bool hollow=false,pending_delete=false; + // PS_* style (0 solid .. 6 inside frame) and width in logical units; geometric pens draw wide lines with round caps. + uint32_t style=0; + int32_t width=0; + bool geometric=false; +}; + +// Attributes SaveDC keeps and RestoreDC puts back. +struct DesktopDCState { + uint64_t bitmap=0,font=0,brush=0,pen=0; + std::optional clip{},application_clip{}; + uint32_t text_color=0,background_color=0,background_mode=0,rop2=13,text_align=0,poly_fill=1; + int32_t origin_x=0,origin_y=0,position_x=0,position_y=0; + std::optional bounds{}; + int32_t brush_org_x=0,brush_org_y=0,window_org_x=0,window_org_y=0,viewport_org_x=0,viewport_org_y=0; }; struct DesktopDC { @@ -86,14 +106,23 @@ struct DesktopDC { int32_t position_x=0,position_y=0; // R2_COPYPEN until SetROP2 changes how pens combine with the destination. uint32_t rop2=13; + // TA_LEFT|TA_TOP|TA_NOUPDATECP, ALTERNATE polygon filling, and the origins that shift logical coordinates. + uint32_t text_align=0,poly_fill=1; + int32_t brush_org_x=0,brush_org_y=0,window_org_x=0,window_org_y=0,viewport_org_x=0,viewport_org_y=0; + // Window and viewport origins move logical coordinates; this offset is what they contributed to origin_x and origin_y. + int32_t user_dx=0,user_dy=0; + // Parts of the window other windows cover; drawing never reaches them. + std::optional visible{}; + std::vector saved{}; // Clip coordinates belong to the DC, not its bitmap backing. bool contains(int64_t x,int64_t y) const { - x-=origin_x; y-=origin_y; + x-=int64_t(origin_x)-user_dx; y-=int64_t(origin_y)-user_dy; if (bounds && (xleft || ytop || x>=bounds->right || y>=bounds->bottom)) return false; if (xINT32_MAX || yINT32_MAX) return false; return (!clip || clip->contains(int32_t(x),int32_t(y))) && - (!application_clip || application_clip->contains(int32_t(x),int32_t(y))); + (!application_clip || application_clip->contains(int32_t(x),int32_t(y))) && + (!visible || visible->contains(int32_t(x),int32_t(y))); } }; diff --git a/native/desktop/outer_surface_test.cpp b/native/desktop/outer_surface_test.cpp index 5daccba..ee252a9 100644 --- a/native/desktop/outer_surface_test.cpp +++ b/native/desktop/outer_surface_test.cpp @@ -44,7 +44,7 @@ int main(int argc,char **argv) for (unsigned y=0;y<240;++y) for (unsigned x=0;x<320;++x) { const auto at=(y*320+x)*4; if (x<7 || x>=307 || y<11 || y>=223) - check(final[at]==255 && !final[at+1] && !final[at+2],"client drawing damaged the outer border"); + if (!(final[at]==255 && !final[at+1] && !final[at+2])) { std::cerr<<"border pixel "<0,"client text was not presented"); diff --git a/native/desktop/system.cpp b/native/desktop/system.cpp index ceb9075..da79c26 100644 --- a/native/desktop/system.cpp +++ b/native/desktop/system.cpp @@ -183,11 +183,12 @@ void NativeWindows::post_due_timers() for (auto &timer : timers) { if (timer.due > now) continue; + const uint32_t kind = timer.system ? 0x118 : 0x113; const bool pending = std::any_of(messages.begin(), messages.end(), [&](const Message &message) { - return message.message == 0x113 && message.window == timer.window && message.wparam == timer.id; + return message.message == kind && message.window == timer.window && message.wparam == timer.id; }); if (!pending && messages.size() < 10000) - messages.push_back({timer.window, timer.id, timer.procedure, 0x113, uint32_t(std::chrono::duration_cast(now.time_since_epoch()).count()), cursor_x, cursor_y}); + messages.push_back({timer.window, timer.id, timer.procedure, kind, uint32_t(std::chrono::duration_cast(now.time_since_epoch()).count()), cursor_x, cursor_y}); timer.due = now + std::chrono::milliseconds(timer.interval); } } diff --git a/native/desktop/windows.cpp b/native/desktop/windows.cpp index 99bf560..67d5907 100644 --- a/native/desktop/windows.cpp +++ b/native/desktop/windows.cpp @@ -1,3 +1,4 @@ +#include #include "windows.h" #include "brush.h" #include "nt/nls.h" @@ -112,6 +113,10 @@ void NativeWindows::release_surface(uint64_t handle) { const auto *window=objects.window(handle); if (!window) return; + // A surface about to disappear still shows what the guest last drew. + if (!window->parent && dirty_roots.contains(handle) && presentation.present && window->bitmap) { + try { present(handle); } catch (...) {} + } objects.detach_window_dcs(handle); if (focus==handle) focus=0; if (active==handle) active=0; @@ -175,7 +180,6 @@ void NativeWindows::post(uint64_t window,uint32_t message,uint64_t wparam,uint64 { if (window==0xffff) throw uint32_t(120); if (window && !objects.window(window)) throw uint32_t(1400); - if (message && message!=0x10 && message!=0x12 && message<0x400) throw uint32_t(120); if (messages.size()>=10000) throw uint32_t(1816); messages.push_back({window,wparam,lparam,message,message_time(),cursor_x,cursor_y}); } @@ -900,6 +904,7 @@ bool NativeWindows::dispatch(std::string_view name) catch (const std::bad_alloc &) { environment.set_last_error(8); } catch (const std::runtime_error &failure) { const std::string_view message=failure.what(); + if (std::getenv("RAVEN_TRACE_API")) std::cerr << "raven: " << message << "\n"; if (message.find("readable")==message.npos && message.find("writable")==message.npos) throw; environment.set_last_error(998); } diff --git a/native/desktop/windows.h b/native/desktop/windows.h index b99face..5e517b7 100644 --- a/native/desktop/windows.h +++ b/native/desktop/windows.h @@ -102,6 +102,37 @@ private: void register_message_api(ApiRegistry ®istry); void register_manager_api(ApiRegistry ®istry); void register_menu_api(ApiRegistry ®istry); + void register_ext_api(ApiRegistry ®istry); + // Text caret of the input queue: one per thread in Windows, one here. Its rectangle is drawn by inverting pixels. + struct Caret { + uint64_t window=0; + int32_t width=0,height=0,x=0,y=0; + int hidden=1; + bool drawn=false; + std::chrono::steady_clock::time_point next{}; + }; + std::optional caret; + void caret_sync(); + void caret_idle(); + // Hooks installed with SetWindowsHookEx, newest first; handles live in their own range. + struct Hook { uint64_t handle; int32_t id; uint64_t procedure; }; + std::vector hooks; + uint64_t next_hook=0x7e000000; + bool has_hook(int32_t id) const; + uint64_t call_hooks(int32_t id,int32_t code,uint64_t wparam,uint64_t lparam,uint64_t after=0); + // In-process clipboard: each format maps to the global memory handle the owner supplied. + std::map clipboard_data; + uint64_t clipboard_window=0,clipboard_owner=0; + uint32_t clipboard_sequence=1; + std::array vk_state{}; + struct Tracking { uint64_t window=0; uint32_t flags=0; }; + Tracking tracking; + void track_pointer(uint64_t window); + // Persistent guest scratch memory for API handlers that need to pass structures to guest callbacks. + uint64_t scratch_base=0; + size_t scratch_used=0; + uint64_t scratch_allocate(size_t size); + std::function)> wait_objects; // Shows, hides, minimizes, maximizes or restores a window; returns whether it was visible before. bool show_window(uint64_t window,uint32_t command); // Moves and resizes a window (screen coordinates for the outer rectangle) with the WM_WINDOWPOSCHANGING/CHANGED protocol. @@ -117,7 +148,7 @@ private: uint64_t current_cursor=0; // Mouse capture and timers belong to the initial guest thread's input state. uint64_t capture=0; - struct Timer { uint64_t window,id,procedure; uint32_t interval; std::chrono::steady_clock::time_point due; }; + struct Timer { uint64_t window,id,procedure; uint32_t interval; std::chrono::steady_clock::time_point due; bool system=false; }; std::vector timers; uint64_t next_timer_id=0x1000; void post_due_timers(); diff --git a/native/graphics/text.cpp b/native/graphics/text.cpp index 4e32e65..8a71ad6 100644 --- a/native/graphics/text.cpp +++ b/native/graphics/text.cpp @@ -7,7 +7,9 @@ #include "include/core/SkPaint.h" #include "include/core/SkTypeface.h" #include "include/ports/SkFontMgr_empty.h" +#include #include +#include #include namespace raven { @@ -21,6 +23,53 @@ static uint32_t field(std::span bytes,size_t at,size_t count=4) struct TextRenderer::State { std::array,3> faces; + // Every bundled style: family (sans, mono, serif) by regular, bold, italic, bold italic. + std::array,4>,3> styled; + + static bool contains_any(const std::string &name,std::initializer_list words) + { + for (const auto *word : words) + if (name.find(word)!=name.npos) return true; + return false; + } + + // Maps any logical font onto the bundled families and style, sizing it the way GDI does. + SkFont logical(std::span definition) + { + const auto height=int32_t(field(definition,0)),weight=int32_t(field(definition,16)); + const bool italic=definition[20]!=0; + const uint8_t quality=definition[26],pitch=definition[27]; + std::string name; + for (size_t i=0;i<32;++i) { + const auto c=field(definition,28+i*2,2); if (!c) break; + name.push_back(c<128 ? char(c>='A' && c<='Z' ? c+32 : c) : '?'); + } + size_t family=0; + if (contains_any(name,{"courier","consolas","mono","fixed","lucida console","terminal","menlo","monaco","typewriter"}) || ((pitch&0x30)==0x30 && !contains_any(name,{"sans","arial","tahoma","segoe"}))) + family=1; + else if (contains_any(name,{"times","serif","georgia","cambria","garamond","palatino","book antiqua","century"}) && !contains_any(name,{"sans"})) + family=2; + const size_t style=(weight>=600 ? 1U : 0U)|(italic ? 2U : 0U); + auto face=styled[family][style]; + if (!face) face=styled[family][0]; + if (!face) face=faces[family]; + float size=height<0 ? float(-height) : height>0 ? float(height) : 12.0f; + SkFont probe(face,100.0f); + SkFontMetrics source; probe.getMetrics(&source); + if (height>0) { + // A positive height is the whole cell (ascent plus descent); scale so the rounded cell matches it. + const float cell=(-source.fAscent+source.fDescent)/100.0f; + size=float(height)/cell; + SkFont trial(face,size); SkFontMetrics measured; trial.getMetrics(&measured); + const float adjusted=float(height)/std::max(1.0f,float(std::lround(-measured.fAscent)+std::lround(measured.fDescent)))*size; + if (std::fabs(adjusted-size) font(std::span definition) { const auto height=int32_t(field(definition,0)); @@ -52,7 +101,15 @@ TextRenderer::TextRenderer(const std::string &directory) : state(std::make_uniqu for (size_t i=0;ifaces[i]=manager->makeFromFile((directory+"/"+files[i]).c_str()); if (!state->faces[i]) throw std::runtime_error("cannot load qualified native font file"); + state->styled[i][0]=state->faces[i]; } + const std::array,3> styles{{ + {"LiberationSans-Bold.ttf","LiberationSans-Italic.ttf","LiberationSans-BoldItalic.ttf"}, + {"LiberationMono-Bold.ttf","LiberationMono-Italic.ttf","LiberationMono-BoldItalic.ttf"}, + {"LiberationSerif-Bold.ttf","LiberationSerif-Italic.ttf","LiberationSerif-BoldItalic.ttf"}}}; + for (size_t family=0;family<3;++family) + for (size_t style=0;style<3;++style) + state->styled[family][style+1]=manager->makeFromFile((directory+"/"+styles[family][style]).c_str()); } TextRenderer::~TextRenderer() = default; @@ -128,4 +185,87 @@ std::vector TextRenderer::advances(std::span definiti return result; } + +// The glyphs for UTF-16 text; characters the face lacks use its replacement glyph and controls draw nothing. +static std::vector logical_glyphs(const SkFont &font,std::span text) +{ + std::vector result(text.size(),0); + std::vector printable; + for (size_t i=0;i definition) +{ + // Fonts inside the qualified subset keep their measured OS/2 metrics. + try { return metrics(definition); } catch (const uint32_t &) {} + auto font=state->logical(definition); SkFontMetrics source; font.getMetrics(&source); + const auto rounded=[](float value) { return int32_t(std::lround(value)); }; + const auto ascent=rounded(-source.fAscent),descent=rounded(source.fDescent); + const auto average=source.fAvgCharWidth>0 ? source.fAvgCharWidth : font.getSize()*0.5f; + const auto maximum=source.fMaxCharWidth>0 ? source.fMaxCharWidth : font.getSize(); + // Fixed-pitch faces report no variable-pitch flag. + const auto *face=font.getTypeface(); + const bool fixed=face==state->styled[1][0].get() || face==state->styled[1][1].get() || + face==state->styled[1][2].get() || face==state->styled[1][3].get(); + return {ascent+descent,ascent,descent,std::max(0,rounded(-source.fAscent+source.fDescent-font.getSize())),rounded(source.fLeading), + rounded(average),rounded(maximum),32,255,31,32,uint8_t(fixed ? 0x36 : 0x27)}; +} + +std::vector TextRenderer::logical_advances(std::span definition,std::span text) +{ + auto font=state->logical(definition); + const auto ids=logical_glyphs(font,text); + std::vector widths(ids.size(),0); + if (!ids.empty()) font.getWidths(ids,widths); + std::vector result; result.reserve(ids.size()); + // Prefixes round the running float sum, like the qualified measurement, so widths never drift per glyph. + float total=0; + for (size_t i=0;i=32 && !(text[i]>=0xdc00 && text[i]<=0xdfff)) total+=widths[i]; + result.push_back(int32_t(std::lround(total))); + } + return result; +} + +std::vector TextRenderer::render(std::span definition,std::span text, + uint32_t width,uint32_t height,int32_t origin_x,int32_t origin_y,std::span offsets) +{ + if (!width || !height || width>16384 || height>16384 || uint64_t(width)*height>32*1024*1024) throw uint32_t(87); + auto font=state->logical(definition); + const auto ids=logical_glyphs(font,text); + std::vector alpha(size_t(width)*height); + SkBitmap bitmap; + if (!bitmap.installPixels(SkImageInfo::MakeA8(int(width),int(height)),alpha.data(),width)) throw std::bad_alloc(); + SkCanvas canvas(bitmap); canvas.clear(SK_ColorTRANSPARENT); + SkPaint paint; paint.setColor(SK_ColorBLACK); paint.setAntiAlias(font.getEdging()!=SkFont::Edging::kAlias); + SkFontMetrics source; font.getMetrics(&source); + const auto baseline=SkScalar(origin_y)+SkScalar(std::lround(-source.fAscent)); + std::vector widths(ids.size(),0); + if (!ids.empty()) font.getWidths(ids,widths); + std::vector drawn; std::vector points; + SkScalar cursor=0; + for (size_t i=0;i=32 && !(text[i]>=0xdc00 && text[i]<=0xdfff) ? SkScalar(std::lround(widths[i])) : 0; + if (text[i]>=32 && ids[i]) { + drawn.push_back(ids[i]); + points.push_back({SkScalar(origin_x)+(offsets.empty() ? cursor : SkScalar(offsets[i])),baseline}); + } + cursor+=step; + } + if (!drawn.empty()) canvas.drawGlyphs({drawn.data(),drawn.size()},{points.data(),points.size()},{0,0},font,paint); + return alpha; +} + } diff --git a/native/graphics/text.h b/native/graphics/text.h index 71c6d7a..e9e2705 100644 --- a/native/graphics/text.h +++ b/native/graphics/text.h @@ -32,6 +32,16 @@ public: std::vector mask(std::span definition,std::span text, uint32_t width,uint32_t height,int32_t x,int32_t y,std::span> positions={}); + // Windows-shaped services for any logical font and any text: unknown faces fall back to the closest bundled family, + // missing glyphs draw the face's replacement glyph, and rendering returns anti-aliased coverage. + TextMetrics logical_metrics(std::span definition); + // Cumulative pixel advance after each UTF-16 unit; control characters advance by zero. + std::vector logical_advances(std::span definition,std::span text); + // Alpha coverage (0..255) of a box [x,x+width) x [y,y+height) in which text starts at left=origin_x, top=origin_y; + // offsets, when given, are the x position of each unit relative to origin_x. + std::vector render(std::span definition,std::span text, + uint32_t width,uint32_t height,int32_t origin_x,int32_t origin_y,std::span offsets={}); + private: struct State; std::unique_ptr state; diff --git a/native/main.cpp b/native/main.cpp index bd1a303..e895a65 100644 --- a/native/main.cpp +++ b/native/main.cpp @@ -175,6 +175,21 @@ int main(int argc, char **argv) if (imported && std::filesystem::exists(component)) modules.add("comctl32.dll", read_file(component.c_str())); } + // The user32 component supplies the built-in control classes and dialog manager to programs that use user32. + if (entry->image.is_64 && !modules.all().contains("raven-user32.dll")) { + const auto component = std::filesystem::read_symlink("/proc/self/exe").parent_path() / "raven-user32.dll"; + const auto imported = std::any_of(modules.all().begin(), modules.all().end(), [](const auto &item) { + return std::any_of(item.second->imports.begin(), item.second->imports.end(), [](const auto &symbol) { + auto name = symbol.module; + for (auto &character : name) + if (character >= 'A' && character <= 'Z') + character = static_cast(character + ('a' - 'A')); + return name == "user32.dll" || name == "user32"; + }); + }); + if (imported && std::filesystem::exists(component)) + modules.add("raven-user32.dll", read_file(component.c_str())); + } raven::DesktopPresentation presentation; #ifdef RAVEN_WAYLAND raven::WaylandPresentation backend; diff --git a/native/nt/kernel_context.h b/native/nt/kernel_context.h index 9d68004..cd50eca 100644 --- a/native/nt/kernel_context.h +++ b/native/nt/kernel_context.h @@ -60,5 +60,6 @@ void register_sync_api(ApiRegistry ®istry, KernelContext &context); void register_thread_api(ApiRegistry ®istry, KernelContext &context); void register_memory_api(ApiRegistry ®istry, KernelContext &context); void register_text_api(ApiRegistry ®istry, KernelContext &context); +void register_process_api(ApiRegistry ®istry, KernelContext &context); } diff --git a/native/nt/process_api.cpp b/native/nt/process_api.cpp new file mode 100644 index 0000000..3f8c412 --- /dev/null +++ b/native/nt/process_api.cpp @@ -0,0 +1,243 @@ +#include "kernel_context.h" +#include "text.h" + +#include +#include +#include + +namespace raven { + +namespace { + +constexpr uint64_t current_process = UINT64_MAX; + +// Whether the guest range can be read, and optionally written back unchanged. +bool accessible(ApiCall &call, uint64_t address, size_t size, bool writable) +{ + if (!size) + return true; + try { + const auto bytes = call.read(address, size); + if (writable) + call.write(address, bytes); + return true; + } catch (const ApiFault &) { + return false; + } +} + +std::u16string atom_text(ApiCall &call, uint64_t name) +{ + return call.wide(name, 256); +} + +int compare_text(const std::string &left, const std::string &right, uint32_t flags) +{ + const bool ignore_case = flags & 1; + const size_t length = std::min(left.size(), right.size()); + for (size_t i = 0; i < length; ++i) { + auto a = static_cast(left[i]), b = static_cast(right[i]); + if (ignore_case) { + a = static_cast(std::tolower(a)); + b = static_cast(std::tolower(b)); + } + if (a != b) + return a < b ? 1 : 3; + } + return left.size() == right.size() ? 2 : left.size() < right.size() ? 1 : 3; +} + +} + +void register_process_api(ApiRegistry ®istry, KernelContext &context) +{ + registry.add("DisableThreadLibraryCalls", 1, [](ApiCall &call) { call.ret(1); }); + registry.add("FreeLibraryWhenCallbackReturns", 2, [](ApiCall &call) { call.ret(0); }); + registry.add("GetProcessVersion", 1, [](ApiCall &call) { call.ret(0x0006000a); }); + registry.add("FreeResource", 1, [](ApiCall &call) { call.ret(0); }); + registry.add("CancelIo", 1, [](ApiCall &call) { call.ret(1); }); + registry.add("ReadDirectoryChangesW", 8, [](ApiCall &call) { call.fail(50, 0); }); + registry.add("FindFirstStreamW", 4, [](ApiCall &call) { call.fail(38, UINT64_MAX); }); + registry.add("TerminateProcess", 2, [](ApiCall &call) { + if (call[0] != current_process) { + call.fail(5, 0); + return; + } + call.process.abandon_call(); + throw ProcessExit{call.u32(1), 0}; + }); + const auto windows_directory = [](ApiCall &call, bool wide) { + static const std::string path = "C:\\Windows"; + const size_t capacity = call.u32(1); + if (wide) { + const auto text = from_utf8(path); + if (capacity <= text.size()) { + call.ret(text.size() + 1); + return; + } + call.put_wide(call[0], text, capacity); + call.ret(text.size()); + } else { + if (capacity <= path.size()) { + call.ret(path.size() + 1); + return; + } + call.put_narrow(call[0], path, capacity); + call.ret(path.size()); + } + }; + registry.add("GetWindowsDirectoryW", 2, [=](ApiCall &call) { windows_directory(call, true); }); + registry.add("GetSystemWindowsDirectoryW", 2, [=](ApiCall &call) { windows_directory(call, true); }); + registry.add("GetWindowsDirectoryA", 2, [=](ApiCall &call) { windows_directory(call, false); }); + registry.add("GetSystemWindowsDirectoryA", 2, [=](ApiCall &call) { windows_directory(call, false); }); + registry.add("lstrcatW", 2, [](ApiCall &call) { + auto first = call.wide(call[0], 65536); + const auto second = call.wide(call[1], 65536); + first += second; + call.put_wide(call[0], first, first.size() + 1); + call.ret(call[0]); + }); + + // One atom table serves the local and global names; atoms count from 0xc000 and integer atoms pass through. + struct Atoms { + std::map> names; + uint16_t next = 0xc000; + }; + const auto atoms = std::make_shared(); + const auto key = [](std::u16string text) { + for (auto &c : text) + if (c >= 'A' && c <= 'Z') + c = char16_t(c + 32); + return text; + }; + const auto add_atom = [atoms, key](ApiCall &call) { + if (call[0] < 0x10000) { + call.ret(call[0]); + return; + } + const auto text = atom_text(call, call[0]); + if (text.empty() || text.size() > 255) { + call.fail(87, 0); + return; + } + auto found = atoms->names.find(key(text)); + if (found == atoms->names.end()) { + if (atoms->next == 0xffff) { + call.fail(8, 0); + return; + } + found = atoms->names.emplace(key(text), std::pair{atoms->next++, 0}).first; + } + ++found->second.second; + call.ret(found->second.first); + }; + const auto find_atom = [atoms, key](ApiCall &call) { + if (call[0] < 0x10000) { + call.ret(call[0]); + return; + } + const auto found = atoms->names.find(key(atom_text(call, call[0]))); + if (found == atoms->names.end()) + call.fail(2, 0); + else + call.ret(found->second.first); + }; + const auto delete_atom = [atoms](ApiCall &call) { + for (auto found = atoms->names.begin(); found != atoms->names.end(); ++found) + if (found->second.first == call.u32(0)) { + if (--found->second.second == 0) + atoms->names.erase(found); + call.ret(0); + return; + } + call.fail(6, call.u32(0)); + }; + const auto atom_name = [atoms](ApiCall &call) { + for (const auto &[text, entry] : atoms->names) + if (entry.first == call.u32(0)) { + const size_t capacity = call.u32(2); + if (!capacity) { + call.fail(122, 0); + return; + } + const auto copied = call.put_wide(call[1], text, capacity); + call.ret(std::min(copied, capacity - 1)); + return; + } + call.fail(6, 0); + }; + for (const auto *name : {"GlobalAddAtomW", "AddAtomW"}) + registry.add(name, 1, add_atom); + for (const auto *name : {"GlobalFindAtomW", "FindAtomW"}) + registry.add(name, 1, find_atom); + for (const auto *name : {"GlobalDeleteAtom", "DeleteAtom"}) + registry.add(name, 1, delete_atom); + for (const auto *name : {"GlobalGetAtomNameW", "GetAtomNameW"}) + registry.add(name, 3, atom_name); + + // Activation contexts only select side-by-side manifests; every program runs against one fixed set of libraries. + registry.add("CreateActCtxW", 1, [](ApiCall &call) { call.ret(0x7e000001); }); + registry.add("ActivateActCtx", 2, [](ApiCall &call) { + if (call[1]) + call.store(call[1], 1); + call.ret(1); + }); + registry.add("DeactivateActCtx", 2, [](ApiCall &call) { call.ret(1); }); + registry.add("ReleaseActCtx", 1, [](ApiCall &call) { call.ret(0); }); + registry.add("GetCurrentActCtx", 1, [](ApiCall &call) { + call.store(call[0], 0); + call.ret(1); + }); + registry.add("IsBadReadPtr", 2, [](ApiCall &call) { call.ret(!accessible(call, call[0], call[1], false)); }); + registry.add("IsBadWritePtr", 2, [](ApiCall &call) { call.ret(!accessible(call, call[0], call[1], true)); }); + registry.add("IsBadCodePtr", 1, [](ApiCall &call) { call.ret(!accessible(call, call[0], 1, false)); }); + registry.add("IsBadStringPtrW", 2, [](ApiCall &call) { + try { + call.wide(call[0], std::min(call[1] ? call[1] : 1, 1 << 20)); + call.ret(0); + } catch (const ApiFault &) { + call.ret(1); + } + }); + registry.add("IsBadStringPtrA", 2, [](ApiCall &call) { + try { + call.narrow(call[0], std::min(call[1] ? call[1] : 1, 1 << 20)); + call.ret(0); + } catch (const ApiFault &) { + call.ret(1); + } + }); + registry.add("IsDBCSLeadByte", 1, [](ApiCall &call) { call.ret(0); }); + registry.add("IsDBCSLeadByteEx", 2, [](ApiCall &call) { call.ret(0); }); + registry.add("CompareStringA", 6, [](ApiCall &call) { + const int32_t left_count = call.i32(3), right_count = call.i32(5); + auto left = call.narrow(call[2], left_count < 0 ? 65536 : size_t(left_count)); + auto right = call.narrow(call[4], right_count < 0 ? 65536 : size_t(right_count)); + if (left_count >= 0) + left = call.narrow(call[2], 65536).substr(0, size_t(left_count)); + if (right_count >= 0) + right = call.narrow(call[4], 65536).substr(0, size_t(right_count)); + call.ret(uint64_t(compare_text(left, right, call.u32(1)))); + }); + registry.add("GetStringTypeExA", 5, [](ApiCall &call) { + // C1 classification of each byte: upper 1, lower 2, digit 4, space 8, punctuation 0x10, control 0x20, blank 0x40, hex 0x80. + const int32_t count = call.i32(3) < 0 ? int32_t(call.narrow(call[2]).size()) : call.i32(3); + for (int32_t i = 0; i < count; ++i) { + const auto c = call.load(call[2] + uint64_t(i)); + uint16_t type = 0; + if (c >= 'A' && c <= 'Z') type |= 1 | 0x100; + if (c >= 'a' && c <= 'z') type |= 2 | 0x100; + if (c >= '0' && c <= '9') type |= 4 | 0x80; + if ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) type |= 0x80; + if (c == ' ' || (c >= 9 && c <= 13)) type |= 8; + if (c == ' ' || c == 9) type |= 0x40; + if (c < 32 || c == 127) type |= 0x20; + if (c > 32 && c < 127 && !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) type |= 0x10; + call.store(call[4] + uint64_t(i) * 2, type); + } + call.ret(1); + }); + registry.add("SetStdHandle", 2, [](ApiCall &call) { call.ret(1); }); +} + +} diff --git a/native/nt/providers.cpp b/native/nt/providers.cpp index c13ceaa..0267acd 100644 --- a/native/nt/providers.cpp +++ b/native/nt/providers.cpp @@ -255,8 +255,17 @@ NativeResult run_native_process(ModuleSet &modules, PEModule &entry, provider = &modules.add(name, runtime_provider(name, base, symbols, false, {}, ordinals), base); provider->runtime_api = true; } + // A guest component that implements part of user32 receives those calls directly instead of through a trap. + if (name == "user32.dll" && provider->runtime_api) { + if (const auto overlay = modules.all().find("raven-user32.dll"); overlay != modules.all().end() && !overlay->second->runtime_api) + for (auto &symbol : provider->exports) + if (!symbol.names.empty() && std::any_of(overlay->second->exports.begin(), overlay->second->exports.end(), [&](const auto &candidate) { + return candidate.forwarder.empty() && std::find(candidate.names.begin(), candidate.names.end(), symbol.names.at(0)) != candidate.names.end(); + })) + symbol.forwarder = "raven-user32." + symbol.names.at(0); + } for (const auto &symbol : provider->exports) - if (symbol.address && !symbol.names.empty()) + if (symbol.address && !symbol.names.empty() && symbol.forwarder.empty()) traps.emplace(provider->loaded.base() + symbol.address, symbol.names.at(0)); } const auto nt=modules.all().find("ntdll.dll"); @@ -305,7 +314,15 @@ NativeResult run_native_process(ModuleSet &modules, PEModule &entry, traps.emplace(modules.all().at("vulkan-1.dll")->loaded.base() + symbol.address, symbol.names.at(0)); #endif modules.bind_imports(); - const auto order = initialization_order(modules, entry); + auto order = initialization_order(modules, entry); + // The user32 component registers the built-in classes in its DllMain, before any library that subclasses them. + if (const auto controls = modules.all().find("raven-user32.dll"); controls != modules.all().end() && !controls->second->runtime_api) { + const auto found = std::find(order.begin(), order.end(), controls->second.get()); + if (found != order.end()) { + order.erase(found); + order.insert(order.begin(), controls->second.get()); + } + } std::vector images; for (const auto &[name, module] : modules.all()) images.push_back(&module->loaded); @@ -333,7 +350,7 @@ NativeResult run_native_process(ModuleSet &modules, PEModule &entry, standard_handles[i]=inherited.value; } environment.prepare_parameters(*launch,standard_handles); - ApiRegistry api_registry; + ApiRegistry api_registry, desktop_api; NativeLoaderData loader_data(process, environment.peb(), modules, entry); NativeTLS tls(process, environment, order); NativeModuleAPI module_api(process, environment, modules, entry); @@ -474,7 +491,8 @@ NativeResult run_native_process(ModuleSet &modules, PEModule &entry, } bool dispatched; try { - dispatched = environment.dispatch(found->second) || module_api.dispatch(found->second) || resources.dispatch(found->second) || memory.dispatch(found->second) || object_api.dispatch(found->second) || heap.dispatch(found->second) || desktop.dispatch(found->second) || windows.dispatch(found->second) || api_registry.dispatch(process, environment, found->second) + // The generalized desktop services come first so that they replace the older narrow ones. + dispatched = desktop_api.dispatch(process, environment, found->second) || environment.dispatch(found->second) || module_api.dispatch(found->second) || resources.dispatch(found->second) || memory.dispatch(found->second) || object_api.dispatch(found->second) || heap.dispatch(found->second) || desktop.dispatch(found->second) || windows.dispatch(found->second) || api_registry.dispatch(process, environment, found->second) #ifdef RAVEN_VULKAN || vulkan.dispatch(found->second) #endif @@ -514,6 +532,13 @@ NativeResult run_native_process(ModuleSet &modules, PEModule &entry, }; const auto prefix = NativePrefix::from_environment(); NativePaths paths(prefix); + desktop.set_host_files([&paths](std::u16string_view path) -> std::optional> { + const auto resolved = paths.resolve(path); + if (!resolved || !resolved->exists || !std::filesystem::is_regular_file(resolved->host)) + return std::nullopt; + std::ifstream file(resolved->host, std::ios::binary); + return std::vector((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + }); GuestAllocator allocator(memory, heap); NativeThreads threads(process, environment, memory, tls, deadline); threads.set_runner(invoke); @@ -524,6 +549,7 @@ NativeResult run_native_process(ModuleSet &modules, PEModule &entry, NativeUnwinder unwinder(process, modules); register_exception_api(api_registry, kernel, unwinder); register_system_api(api_registry, kernel); + register_process_api(api_registry, kernel); register_sync_api(api_registry, kernel); register_thread_api(api_registry, kernel); register_text_api(api_registry, kernel); @@ -533,7 +559,10 @@ NativeResult run_native_process(ModuleSet &modules, PEModule &entry, register_shell_api(api_registry, kernel); register_platform_api(api_registry, kernel); register_version_api(api_registry, kernel); - windows.register_api(api_registry); + windows.register_api(desktop_api); + desktop.register_gdi_api(desktop_api); + desktop.register_text_api(desktop_api); + desktop.register_image_api(desktop_api); NativeRegistry hive(prefix.registry_file()); register_registry_api(api_registry, kernel, hive); std::vector attached; @@ -851,6 +880,7 @@ NativeResult run_native_process(ModuleSet &modules, PEModule &entry, }; try { windows.set_call(invoke); + desktop.set_call(invoke); heap.initialize(invoke); for (const auto *module : order) { if (module == &entry) diff --git a/native/nt/text.cpp b/native/nt/text.cpp index 3c3356c..9f3d65d 100644 --- a/native/nt/text.cpp +++ b/native/nt/text.cpp @@ -136,4 +136,20 @@ std::u16string widen_ascii(std::string_view text) return result; } + +std::string to_ansi(std::u16string_view text, uint32_t code_page) +{ + std::string result; + result.reserve(text.size()); + for (const auto character : text) { + // Characters below 0x100 keep their Latin-1 value, which agrees with the Windows code pages except for 0x80..0x9f. + if (character < 0x80 || (character >= 0xa0 && character < 0x100)) + result.push_back(char(character)); + else + result.push_back('?'); + } + (void)code_page; + return result; +} + } diff --git a/native/nt/text.h b/native/nt/text.h index 2a324a4..467fa6f 100644 --- a/native/nt/text.h +++ b/native/nt/text.h @@ -17,5 +17,7 @@ int compare_fold(std::u16string_view left, std::u16string_view right); std::u16string widen_ascii(std::string_view text); // ANSI text of the process code page (1252, 437, ...) as UTF-16; unmapped bytes become U+FFFD. std::u16string from_ansi(std::string_view text, uint32_t code_page); +// Inverse of from_ansi; characters the code page lacks become '?'. +std::string to_ansi(std::u16string_view text, uint32_t code_page); } diff --git a/native/upstream/reactos-controls/crt.c b/native/upstream/reactos-controls/crt.c index 6b60a98..6fd0f13 100644 --- a/native/upstream/reactos-controls/crt.c +++ b/native/upstream/reactos-controls/crt.c @@ -252,11 +252,9 @@ void qsort(void *base, size_t count, size_t width, int (__cdecl *compare)(const } } -int _snprintf(char *buffer, size_t size, const char *format, ...) +static int format_text(char *buffer, size_t size, const char *format, va_list arguments) { - va_list arguments; size_t length = 0; - va_start(arguments, format); #define EMIT(character) do { if (length + 1 < size) buffer[length] = (character); ++length; } while (0) for (; *format; ++format) { if (*format != '%') { @@ -301,12 +299,21 @@ int _snprintf(char *buffer, size_t size, const char *format, ...) EMIT(digits[--count]); } } - va_end(arguments); if (size) buffer[length < size ? length : size - 1] = 0; return (int)length; } +int _snprintf(char *buffer, size_t size, const char *format, ...) +{ + va_list arguments; + int length; + va_start(arguments, format); + length = format_text(buffer, size, format, arguments); + va_end(arguments); + return length; +} + void __cdecl _assert(const char *expression, const char *file, unsigned line) { __builtin_trap(); @@ -324,3 +331,86 @@ int wcsicmp(const wchar *left, const wchar *right) { return _wcsicmp(left, right); } + +wchar *wcscpy(wchar *destination, const wchar *source) +{ + wchar *start = destination; + while ((*destination++ = *source++)) + ; + return start; +} + +wchar *wcscat(wchar *destination, const wchar *source) +{ + wcscpy(destination + wcslen(destination), source); + return destination; +} + +wchar towlower(wchar character) +{ + if (character >= 'A' && character <= 'Z') + return character + 32; + if (character >= 0xc0 && character <= 0xde && character != 0xd7) + return character + 32; + if (character >= 0x391 && character <= 0x3a9) + return character + 32; + if (character >= 0x410 && character <= 0x42f) + return character + 32; + return character; +} + +wchar *_wcsupr(wchar *text) +{ + for (wchar *cursor = text; *cursor; ++cursor) + *cursor = towupper(*cursor); + return text; +} + +wchar *_wcslwr(wchar *text) +{ + for (wchar *cursor = text; *cursor; ++cursor) + *cursor = towlower(*cursor); + return text; +} + +unsigned long strtoul(const char *text, char **end, int base) +{ + unsigned long value = 0; + if ((base == 0 || base == 16) && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) { + text += 2; + base = 16; + } else if (base == 0) { + base = text[0] == '0' ? 8 : 10; + } + for (;; ++text) { + int digit = *text >= '0' && *text <= '9' ? *text - '0' : + *text >= 'a' && *text <= 'z' ? *text - 'a' + 10 : *text >= 'A' && *text <= 'Z' ? *text - 'A' + 10 : 99; + if (digit >= base) + break; + value = value * base + digit; + } + if (end) + *end = (char *)text; + return value; +} + +long strtol(const char *text, char **end, int base) +{ + int negative = 0; + while (*text == ' ' || (*text >= 9 && *text <= 13)) + ++text; + if (*text == '-' || *text == '+') + negative = *text++ == '-'; + long value = (long)strtoul(text, end, base); + return negative ? -value : value; +} + +int sprintf(char *buffer, const char *format, ...) +{ + va_list arguments; + int length; + va_start(arguments, format); + length = format_text(buffer, 1 << 20, format, arguments); + va_end(arguments); + return length; +} diff --git a/native/upstream/reactos-user32/bridge.c b/native/upstream/reactos-user32/bridge.c new file mode 100644 index 0000000..2e41cf5 --- /dev/null +++ b/native/upstream/reactos-user32/bridge.c @@ -0,0 +1,394 @@ +#include + +/* Process-wide state and kernel services the upstream control and dialog sources expect, rebuilt on public calls. */ + +HINSTANCE User32Instance; +static SERVERINFO server_info; +PSERVERINFO gpsi = &server_info; +USERAPIHOOK guah; +BOOLEAN gfServerProcess; +Imm32ApiTable gImmApiEntries; + +static const WCHAR function_property[] = {'R','a','v','e','n','F','n','i','d',0}; + +/* Control and dialog code reads these fields through a window pointer; the values are sampled per call. */ +PWND FASTCALL ValidateHwnd(HWND window) +{ + static WND ring[8]; + static unsigned next; + WND *record; + RECT rectangle; + POINT origin = {0, 0}; + if (!IsWindow(window)) + return NULL; + record = &ring[next++ & 7]; + ZeroMemory(record, sizeof *record); + record->fnid = (WORD)(ULONG_PTR)GetPropW(window, function_property); + record->style = (DWORD)GetWindowLongW(window, GWL_STYLE); + record->ExStyle = (DWORD)GetWindowLongW(window, GWL_EXSTYLE); + record->cbwndExtra = (int)GetClassLongW(window, GCL_CBWNDEXTRA); + /* Every window follows the Windows 4.0 behavior; dialog windows are recognized by their class procedure. */ + record->state2 = WNDS2_WIN40COMPAT; + GetWindowRect(window, &record->rcWindow); + GetClientRect(window, &rectangle); + ClientToScreen(window, &origin); + OffsetRect(&rectangle, origin.x, origin.y); + record->rcClient = rectangle; + return record; +} + +BOOL NTAPI NtUserSetWindowFNID(HWND window, WORD function) +{ + return SetPropW(window, function_property, (HANDLE)(ULONG_PTR)function); +} + +DWORD NTAPI NtUserAlterWindowStyle(HWND window, DWORD index, LONG value) +{ + return (DWORD)SetWindowLongW(window, (int)index, value); +} + +BOOL NTAPI NtUserInvalidateRect(HWND window, CONST RECT *rectangle, BOOL erase) +{ + return InvalidateRect(window, rectangle, erase); +} + +BOOL NTAPI NtUserInvalidateRgn(HWND window, HRGN region, BOOL erase) +{ + return InvalidateRgn(window, region, erase); +} + +BOOL NTAPI NtUserRedrawWindow(HWND window, CONST RECT *rectangle, HRGN region, UINT flags) +{ + return RedrawWindow(window, rectangle, region, flags); +} + +HDC NTAPI NtUserGetDC(HWND window) +{ + return GetDC(window); +} + +HDC NTAPI NtUserGetDCEx(HWND window, HANDLE region, ULONG flags) +{ + return GetDCEx(window, region, flags); +} + +HDC NTAPI NtUserBeginPaint(HWND window, PAINTSTRUCT *paint) +{ + return BeginPaint(window, paint); +} + +BOOL NTAPI NtUserEndPaint(HWND window, CONST PAINTSTRUCT *paint) +{ + return EndPaint(window, paint); +} + +HWND NTAPI NtUserSetFocus(HWND window) +{ + return SetFocus(window); +} + +HWND NTAPI NtUserSetCapture(HWND window) +{ + return SetCapture(window); +} + +HWND NTAPI NtUserSetActiveWindow(HWND window) +{ + return SetActiveWindow(window); +} + +HWND NTAPI NtUserGetForegroundWindow(VOID) +{ + return GetForegroundWindow(); +} + +/* The upstream header turns ShowScrollBar into a kernel call, so the real function is reached under its import name. */ +extern BOOL WINAPI raven_show_scroll_bar(HWND window, int bar, BOOL show) __asm__("ShowScrollBar"); + +DWORD NTAPI NtUserShowScrollBar(HWND window, int bar, DWORD show) +{ + return raven_show_scroll_bar(window, bar, show); +} + +BOOL NTAPI NtUserSetWindowPos(HWND window, HWND after, int x, int y, int width, int height, UINT flags) +{ + return SetWindowPos(window, after, x, y, width, height, flags); +} + +BOOL NTAPI NtUserMoveWindow(HWND window, int x, int y, int width, int height, BOOL repaint) +{ + return MoveWindow(window, x, y, width, height, repaint); +} + +BOOL NTAPI NtUserShowWindow(HWND window, LONG command) +{ + return ShowWindow(window, command); +} + +HWND NTAPI NtUserSetParent(HWND child, HWND parent) +{ + return SetParent(child, parent); +} + +BOOLEAN NTAPI NtUserDestroyWindow(HWND window) +{ + return DestroyWindow(window); +} + +BOOL NTAPI NtUserDestroyMenu(HMENU menu) +{ + return DestroyMenu(menu); +} + +BOOL NTAPI NtUserFlashWindowEx(PFLASHWINFO info) +{ + return TRUE; +} + +UINT_PTR NTAPI NtUserSetTimer(HWND window, UINT_PTR identifier, UINT elapse, TIMERPROC procedure) +{ + return SetTimer(window, identifier, elapse, procedure); +} + +UINT_PTR NTAPI NtUserSetSystemTimer(HWND window, UINT_PTR identifier, UINT elapse, TIMERPROC procedure) +{ + return SetSystemTimer(window, identifier, elapse, procedure); +} + +BOOL NTAPI NtUserKillTimer(HWND window, UINT_PTR identifier) +{ + return KillTimer(window, identifier); +} + +BOOL NTAPI NtUserCreateCaret(HWND window, HBITMAP bitmap, int width, int height) +{ + return CreateCaret(window, bitmap, width, height); +} + +BOOL NTAPI NtUserShowCaret(HWND window) +{ + return ShowCaret(window); +} + +BOOL NTAPI NtUserHideCaret(HWND window) +{ + return HideCaret(window); +} + +BOOL NTAPI NtUserSystemParametersInfo(UINT action, UINT parameter, PVOID value, UINT flags) +{ + return SystemParametersInfoW(action, parameter, value, flags); +} + +BOOL NTAPI NtUserGetComboBoxInfo(HWND window, PCOMBOBOXINFO info) +{ + return (BOOL)SendMessageW(window, CB_GETCOMBOBOXINFO, 0, (LPARAM)info); +} + +BOOL NTAPI NtUserIsClipboardFormatAvailable(UINT format) +{ + return IsClipboardFormatAvailable(format); +} + +BOOL NTAPI NtUserEmptyClipboard(VOID) +{ + return EmptyClipboard(); +} + +BOOL NTAPI NtUserCloseClipboard(VOID) +{ + return CloseClipboard(); +} + +/* The thread-state queries only feed dialog activation heuristics; this process has one input queue. */ +DWORD_PTR NTAPI NtUserGetThreadState(DWORD routine) +{ + return routine == THREADSTATE_FOREGROUNDTHREAD; +} + +DWORD NTAPI NtUserSetThreadState(DWORD flags, DWORD mask) +{ + return 0; +} + +DWORD_PTR NTAPI NtUserCallNoParam(DWORD routine) +{ + return 0; +} + +DWORD_PTR NTAPI NtUserCallOneParam(DWORD_PTR parameter, DWORD routine) +{ + return 0; +} + +DWORD NTAPI NtUserCallHwndParam(HWND window, DWORD_PTR parameter, DWORD routine) +{ + return 0; +} + +DWORD_PTR NTAPI NtUserQueryWindow(HWND window, DWORD index) +{ + DWORD process = 0; + if (index == QUERY_WINDOW_FOREGROUND) + return GetForegroundWindow() == GetAncestor(window, GA_ROOT); + if (index == QUERY_WINDOW_UNIQUE_PROCESS_ID) { + GetWindowThreadProcessId(window, &process); + return process; + } + return 0; +} + +/* The class window procedures ask the parent for a brush and fall back to the system colors. */ +HBRUSH DefWndControlColor(HDC device, UINT type) +{ + if (type == CTLCOLOR_SCROLLBAR) { + COLORREF background = GetSysColor(COLOR_3DHILIGHT); + SetTextColor(device, GetSysColor(COLOR_3DFACE)); + SetBkColor(device, background); + if (background == GetSysColor(COLOR_WINDOW)) + return gpsi->hbrGray; + return GetSysColorBrush(COLOR_SCROLLBAR); + } + SetTextColor(device, GetSysColor(COLOR_WINDOWTEXT)); + if (type == CTLCOLOR_EDIT || type == CTLCOLOR_LISTBOX) { + SetBkColor(device, GetSysColor(COLOR_WINDOW)); + return GetSysColorBrush(COLOR_WINDOW); + } + SetBkColor(device, GetSysColor(COLOR_3DFACE)); + return GetSysColorBrush(COLOR_3DFACE); +} + +HBRUSH NTAPI NtUserGetControlColor(HWND parent, HWND window, HDC device, UINT message) +{ + HBRUSH brush; + if (!parent) + parent = GetParent(window); + brush = parent ? (HBRUSH)SendMessageW(parent, message, (WPARAM)device, (LPARAM)window) : NULL; + if (!brush || GetObjectType(brush) != OBJ_BRUSH) + brush = DefWndControlColor(device, message - WM_CTLCOLORMSGBOX); + return brush; +} + +HBRUSH NTAPI NtUserGetControlBrush(HWND window, HDC device, UINT message) +{ + return NtUserGetControlColor(NULL, window, device, message); +} + +/* Controls that share their parent's device context clip to their own rectangle. */ +HRGN set_control_clipping(HDC device, const RECT *rectangle) +{ + RECT clip = *rectangle; + HRGN region = CreateRectRgn(0, 0, 0, 0); + if (GetClipRgn(device, region) != 1) { + DeleteObject(region); + region = 0; + } + DPtoLP(device, (POINT *)&clip, 2); + if (GetLayout(device) & LAYOUT_RTL) { + clip.left++; + clip.right++; + } + IntersectClipRect(device, clip.left, clip.top, clip.right, clip.bottom); + return region; +} + +BOOL get_icon_size(HICON icon, SIZE *size) +{ + ICONINFO information; + BITMAP bitmap; + HBITMAP source; + if (!GetIconInfo(icon, &information)) + return FALSE; + source = information.hbmColor ? information.hbmColor : information.hbmMask; + if (!GetObjectW(source, sizeof bitmap, &bitmap)) { + if (information.hbmColor) + DeleteObject(information.hbmColor); + DeleteObject(information.hbmMask); + return FALSE; + } + size->cx = bitmap.bmWidth; + /* A monochrome icon stacks its AND and XOR masks in one bitmap. */ + size->cy = information.hbmColor ? bitmap.bmHeight : bitmap.bmHeight / 2; + if (information.hbmColor) + DeleteObject(information.hbmColor); + DeleteObject(information.hbmMask); + return TRUE; +} + +HWND *WIN_ListChildren(HWND parent) +{ + HWND *list; + UINT count = 0, index = 0; + HWND child; + for (child = GetWindow(parent, GW_CHILD); child; child = GetWindow(child, GW_HWNDNEXT)) + ++count; + list = HeapAlloc(GetProcessHeap(), 0, (count + 1) * sizeof(HWND)); + if (!list) + return NULL; + for (child = GetWindow(parent, GW_CHILD); child && index < count; child = GetWindow(child, GW_HWNDNEXT)) + list[index++] = child; + list[index] = NULL; + return list; +} + +BOOL FASTCALL TestWindowProcess(PWND window) +{ + return TRUE; +} + +VOID FASTCALL IntNotifyWinEvent(DWORD event, HWND window, LONG object, LONG child, DWORD flags) +{ +} + +PTHREADINFO GetW32ThreadInfo(VOID) +{ + return NULL; +} + +/* No API hooking: user hooks are never installed, so every control and dialog call runs directly. */ +BOOL FASTCALL BeginIfHookedUserApiHook(VOID) +{ + return FALSE; +} + +BOOL FASTCALL EndUserApiHook(VOID) +{ + return FALSE; +} + +BOOL FASTCALL IsInsideUserApiHook(VOID) +{ + return FALSE; +} + +const char *SPY_GetMsgName(UINT message, HWND window) +{ + return ""; +} + +BOOL WINAPI RavenInitControls(VOID); + +BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) +{ + if (reason == DLL_PROCESS_ATTACH) { + static const WORD gray_bits[8] = {0x5555, 0xaaaa, 0x5555, 0xaaaa, 0x5555, 0xaaaa, 0x5555, 0xaaaa}; + HBITMAP pattern = CreateBitmap(8, 8, 1, 1, gray_bits); + User32Instance = instance; + DisableThreadLibraryCalls(instance); + if (pattern) { + gpsi->hbrGray = CreatePatternBrush(pattern); + DeleteObject(pattern); + } + gpsi->PUSIFlags = PUSIF_KEYBOARDCUES; + /* Registering here, ahead of every other library, lets later libraries subclass the built-in classes. */ + if (!RavenInitControls()) + return FALSE; + } + return TRUE; +} + +/* The hook loader asks whether the thread runs inside a loader callout; this component never loads hooks. */ +BOOLEAN NTAPI RtlIsThreadWithinLoaderCallout(VOID) +{ + return FALSE; +} diff --git a/native/upstream/reactos-user32/register.c b/native/upstream/reactos-user32/register.c new file mode 100644 index 0000000..22f75a5 --- /dev/null +++ b/native/upstream/reactos-user32/register.c @@ -0,0 +1,28 @@ +#include + +/* Registers the built-in window classes whose window procedures live in this component. */ + +BOOL WINAPI RavenInitControls(VOID) +{ + static const struct builtin_class_descr *const classes[] = { + &DIALOG_builtin_class, &COMBO_builtin_class, &COMBOLBOX_builtin_class, + &BUTTON_builtin_class, &LISTBOX_builtin_class, &EDIT_builtin_class, &STATIC_builtin_class, + }; + WNDCLASSEXW definition; + unsigned index; + for (index = 0; index < sizeof classes / sizeof classes[0]; ++index) { + ZeroMemory(&definition, sizeof definition); + definition.cbSize = sizeof definition; + definition.style = classes[index]->style | CS_GLOBALCLASS; + definition.lpfnWndProc = classes[index]->procW; + definition.cbWndExtra = classes[index]->extra; + definition.hInstance = User32Instance; + definition.hCursor = LoadCursorW(NULL, classes[index]->cursor); + definition.hbrBackground = classes[index]->brush; + /* The dialog class is named by its predefined atom upstream; its textual name is #32770. */ + definition.lpszClassName = (ULONG_PTR)classes[index]->name == 0x8002 ? L"#32770" : classes[index]->name; + if (!RegisterClassExW(&definition)) + return FALSE; + } + return TRUE; +} diff --git a/native/upstream/reactos-user32/shim/user32.h b/native/upstream/reactos-user32/shim/user32.h new file mode 100644 index 0000000..cd18eed --- /dev/null +++ b/native/upstream/reactos-user32/shim/user32.h @@ -0,0 +1,41 @@ +/* Replacement for the upstream private header: it keeps the public and shared-structure includes and drops the kernel-mode ones. */ +#pragma once +#include +#define WIN32_NO_STATUS +#define _INC_WINDOWS +#define COM_NO_WINDOWS_H +#define OEMRESOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef _UNICODE_STRING_DEFINED +typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING; +typedef const UNICODE_STRING *PCUNICODE_STRING; +#endif +typedef const char *PCSZ; +typedef USHORT RTL_ATOM, *PRTL_ATOM; +typedef LONG NTSTATUS; +typedef struct _OBJECT_ATTRIBUTES *POBJECT_ATTRIBUTES; +/* Only the client information array is read; its upstream offset in the thread block is kept. */ +typedef struct _TEB { char reserved[0x800]; void *Win32ClientInfo[62]; } TEB, *PTEB; +typedef struct _STRING { USHORT Length; USHORT MaximumLength; PCHAR Buffer; } STRING, *PSTRING; +#define FASTCALL +#include +#include +#include +#include +#include +#include +#define ASSERT(condition) ((void)0) +BOOLEAN NTAPI RtlIsThreadWithinLoaderCallout(VOID); +#include +#include diff --git a/scripts/provision-controls.py b/scripts/provision-controls.py index f260fb0..ffe4fc9 100644 --- a/scripts/provision-controls.py +++ b/scripts/provision-controls.py @@ -15,7 +15,10 @@ import urllib.request REVISION = "dacf2a6c47fa562799f9c8a41ab6d5c558556558" ARCHIVE_SHA256 = "5373bbbb60541cb2a49e0acb53360e1218d4f5d56c31f9187b341b9fc16daa69" PREFIXES = ("sdk/include/", "sdk/lib/pseh/include/", "sdk/tools/hpp/", "sdk/tools/spec2def/", "dll/win32/comctl32/", - "win32ss/user/user32/controls/", "win32ss/user/user32/windows/dialog.c") + "win32ss/include/", "win32ss/user/user32/", "win32ss/user/rtl/") +# Upstream user-mode sources of the user32 component, and the exports they define that user32 programs import. +USER32_SOURCES = ("controls/button.c", "controls/combo.c", "controls/edit.c", "controls/listbox.c", "controls/static.c", + "windows/dialog.c", "windows/draw.c", "windows/font.c", "windows/text.c") IDLS = ("shtypes", "commoncontrols") @@ -126,12 +129,42 @@ def main(): run(["x86_64-w64-mingw32-windres", "--preprocessor=" + str(wrapper), "-O", "coff", "-DLANGUAGE_EN_US", "-D__REACTOS__", "-D_WIN32_IE=0x0A00", "-D__WINESRC__", "-D_M_AMD64", "-I" + str(source / "dll/win32/comctl32"), "-I" + str(generated / "reactos"), "-i", source / "dll/win32/comctl32/rsrc.rc", "-o", objects / "comctl32.res.o"]) - run(["x86_64-w64-mingw32-gcc", "-shared", "-nostdlib", "-Wl,--entry,DllMain,--image-base,0x1a2000000,--no-insert-timestamp", + run(["x86_64-w64-mingw32-gcc", "-shared", "-nostdlib", "-Wl,--entry,DllMain,--image-base,0x1f0000000,--no-insert-timestamp", "-o", output / "comctl32.dll", *compiled, objects / "comctl32.res.o", output / "comctl32.def", "-luser32", "-lgdi32", "-ladvapi32", "-lkernel32", "-lntdll", "-limm32", "-lusp10", "-luxtheme", "-lwinmm", "-lole32", "-loleaut32", "-lshlwapi", "-luuid", "-lgcc"]) + # The user32 component: upstream user-mode sources on a bridge that stands in for the kernel services. + user_support = repo / "native/upstream/reactos-user32" + user_root = source / "win32ss/user/user32" + user_includes = [user_support / "shim", source / "win32ss/include", user_root / "include", *includes[1:]] + user_flags = ["-O2", "-nostdinc", "-isystem", gcc_include, "-fgnu89-inline", "-w", "-D_WIN32_WINNT=0x600", "-DWINVER=0x600", + "-D__REACTOS__", "-DUNICODE", "-D_UNICODE", "-D_M_AMD64", "-D__ROS_LONG64__", "-DUSE_PSEH", + f"-ffile-prefix-map={source}=reactos", f"-ffile-prefix-map={repo}=raven", f"-ffile-prefix-map={output}=component", + *["-I" + str(path) for path in user_includes]] + user_sources = [user_root / name for name in USER32_SOURCES] + [source / "win32ss/user/rtl/text.c", + user_support / "bridge.c", user_support / "register.c", support / "crt.c", support / "debug.c"] + + def compile_user(path): + name = "user32_" + (path.parent.name + "_" if path.parent.name in ("windows", "controls", "rtl") else "") + path.stem + freestanding = ["-ffreestanding", "-fno-builtin"] if path.name == "crt.c" else [] + run(["x86_64-w64-mingw32-gcc", "-c", *user_flags, *freestanding, path, "-o", objects / (name + ".o")]) + return objects / (name + ".o") + + with ThreadPoolExecutor(max_workers=2) as pool: + user_objects = list(pool.map(compile_user, user_sources)) + run([output / "spec2def", "-a=x86_64", "-n=user32.dll", "-d=" + str(output / "user32.def"), user_root / "user32.spec"]) + public = {match.group(1) for match in re.finditer(r"^\s*([A-Za-z_][A-Za-z0-9_]*)(?:\s|=|@|$)", (output / "user32.def").read_text(), re.M)} + defined = {line.split()[-1] for line in run(["x86_64-w64-mingw32-nm", "-g", "--defined-only", *user_objects]).stdout.splitlines() + if len(line.split()) == 3 and line.split()[1] in "TtDdBbRr"} + # FrameRect and FillRect keep the measured host behavior (error codes for invalid brushes and device contexts). + exported = sorted((public & defined) - {"EXPORTS", "NAME", "FrameRect", "FillRect"}) + (output / "raven-user32.def").write_text("NAME raven-user32.dll\nEXPORTS\n" + "".join(" " + name + "\n" for name in exported) + " RavenInitControls\n") + run(["x86_64-w64-mingw32-gcc", "-shared", "-nostdlib", "-Wl,--entry,DllMain,--image-base,0x1f4000000,--no-insert-timestamp", + "-o", output / "raven-user32.dll", *user_objects, output / "raven-user32.def", + "-luser32", "-lgdi32", "-ladvapi32", "-lkernel32", "-lntdll", "-limm32", "-lusp10", "-lgcc"]) result = dict(revision=REVISION, archive_sha256=ARCHIVE_SHA256, commands=records, - component_sha256=hashlib.sha256((output / "comctl32.dll").read_bytes()).hexdigest(), + component_sha256={name: hashlib.sha256((output / name).read_bytes()).hexdigest() for name in ("comctl32.dll", "raven-user32.dll")}, + user32_exports=exported, source_hashes={path.relative_to(source).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted(source.rglob("*")) if path.is_file() and not path.is_relative_to(overlay)}) (output / "provenance.json").write_text(json.dumps(result, indent=2) + "\n") diff --git a/scripts/provision-libpng.py b/scripts/provision-libpng.py new file mode 100644 index 0000000..a39686f --- /dev/null +++ b/scripts/provision-libpng.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +import argparse +import hashlib +import os +from pathlib import Path +import subprocess +import tarfile +import urllib.request + + +URL = "https://download.sourceforge.net/libpng/libpng-1.6.59.tar.xz" +SHA256 = "d80dd2a38a37f803cb9b6ac7b14bd6e74ddc3b654780a8380bdf93523fdb4389" + + +def main(): + parser = argparse.ArgumentParser(description="Build the pinned libpng release as a static library") + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=True) + env = dict(os.environ, TMPDIR=str(output), TMP=str(output), TEMP=str(output)) + archive = output / "libpng.tar.xz" + if not archive.exists(): + urllib.request.urlretrieve(URL, archive) + if hashlib.sha256(archive.read_bytes()).hexdigest() != SHA256: + raise RuntimeError("libpng archive checksum differs") + with tarfile.open(archive) as source: + source.extractall(output, filter="data") + build = output / "build" + install = output / "install" + build.mkdir(exist_ok=True) + subprocess.run(["cmake", "-DCMAKE_BUILD_TYPE=Release", "-DPNG_SHARED=OFF", "-DPNG_TESTS=OFF", "-DPNG_TOOLS=OFF", + f"-DCMAKE_INSTALL_PREFIX={install}", str(output / "libpng-1.6.59")], cwd=build, env=env, check=True, capture_output=True) + subprocess.run(["cmake", "--build", ".", "--target", "install", "-j2"], cwd=build, env=env, check=True, capture_output=True) + print(f"configure Raven with -DPNG_LIBRARY_RELEASE={install}/lib/libpng16.a -DPNG_PNG_INCLUDE_DIR={install}/include") + + +if __name__ == "__main__": + main() diff --git a/testdata/native_loader/text_client.c b/testdata/native_loader/text_client.c index 8d9a8d6..d879ee1 100644 --- a/testdata/native_loader/text_client.c +++ b/testdata/native_loader/text_client.c @@ -67,12 +67,12 @@ int raven_entry(void) for(unsigned i=0;i<16;++i) definition.lfFaceName[i]=faces[0][i]; HFONT font=CreateFontIndirectW(&definition); HGDIOBJ previous=SelectObject(dc,font); if(!font || !previous) return 13; SIZE extent={123,456}; WCHAR missing=0xffff; SetLastError(0); - if(GetTextExtentPoint32W(dc,&missing,1,&extent) || GetLastError()!=ERROR_CALL_NOT_IMPLEMENTED || extent.cx!=123 || extent.cy!=456) return 14; + if(!GetTextExtentPoint32W(dc,&missing,1,&extent) || extent.cx<=0 || extent.cy<=0) return 14; SetLastError(0); if(SetBkMode(dc,3) || GetLastError()!=ERROR_INVALID_PARAMETER) return 15; SetLastError(0); if(SetTextColor(dc,0x01000004)!=CLR_INVALID || GetLastError()!=ERROR_CALL_NOT_IMPLEMENTED) return 16; - SetLastError(0); if(TextOutW(dc,0,0,samples[0],4097) || GetLastError()!=ERROR_INVALID_PARAMETER) return 17; + SetLastError(0); if(!TextOutW(dc,0,0,samples[0],4) || TextOutW(dc,0,0,samples[0],-1)) return 17; WCHAR combining[]={L'e',0x0301}; SetLastError(0); - if(TextOutW(dc,0,0,combining,2) || GetLastError()!=ERROR_CALL_NOT_IMPLEMENTED) return 19; + if(!TextOutW(dc,0,0,combining,2)) return 19; SelectObject(dc,previous); DeleteObject(font); #endif SelectObject(dc,previous_bitmap); if(!DeleteObject(bitmap) || !DeleteDC(dc) || text_frames!=390) return 18;