ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SerenityOS HighDPI 设计指南:从 1x 到 2x 的窗口系统缩放架构与资源加载方案

SerenityOS HighDPI 设计指南:从 1x 到 2x 的窗口系统缩放架构与资源加载方案 SerenityOS HighDPI 设计指南从 1x 到 2x 的窗口系统缩放架构与资源加载方案【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity导读本文基于 Documentation/HighDPI.md 系统梳理 SerenityOS 在窗口系统层面引入高分辨率HighDPI支持的整体设计包括坐标系从像素到逻辑坐标的迁移、按比例缩放的位图与字体资源加载策略、以及从窗口服务器到各应用逐步迁移的增量实施计划。通过对照当前仓库源码LibGfx 位图加载、WindowServer 的 MultiScaleBitmaps 与 Screen 缩放逻辑读者可以完整理解 SerenityOS 在保留整数倍缩放这一核心原则的前提下如何组织-2x后缀资源文件并让系统在 1x 与 2x 之间动态切换。背景主流操作系统如何解决高分辨率缩放HighDPI 的核心矛盾在于显示器物理像素密度不断提高而应用界面坐标体系通常以点或逻辑像素为单位设计。SerenityOS 在规划自己的方案之前先对比了三个主流平台的既有做法macOS应用层面只支持整数倍缩放仅在最终合成帧缓冲composited framebuffer时进行拉伸以适配非整数倍的显示器缩放。优点是编程模型简单末段缩放能保证最终图像连贯缺点是即使只需 1.5x 缩放也要付出 4 倍内存且缩放发生在最末端最终图像细节损失较多。Android提供若干离散的缩放档位——ldpi0.75x、mdpi1x、hdpi1.5x、xhdpi2x、xxhdpi3x、xxxhdpi4x。Windows允许在 100% 到 500% 之间自由填写缩放因子但官方标注不推荐not recommended。对比结论是整数倍缩放无论如何都是必须的因此 SerenityOS 决定先把它做对——并且短期内只聚焦 2x 这一档。期望的最终状态逻辑坐标优先物理坐标显式命名文档为 HighDPI 改造定义了清晰的坐标系治理目标所有矩形Window 与 Widget 的 rect、鼠标光标、甚至位图尺寸尽量使用逻辑坐标logical coordinates在 1x 缩放下它与像素一一对应。任何必须以物理像素表达的量其名称必须以physical_开头物理坐标应尽可能不跨越 API 边界传播。逻辑坐标是否继续使用整数类型尚未最终定论Jurys still out。大概率会保持整数但这意味着鼠标光标等对象只有点级分辨率而非像素级分辨率。系统应当具备一个能保存同一图像/字体在不同缩放档位下的多个可惰性加载的位图与字体的容器在绘制时按当前缩放因子选择正确的资源。这一目标直接催生了本文后面讨论的MultiScaleBitmaps之类的集合类以及-2x后缀的资源命名约定。资源加载HighDPI 下位图与字体如何按缩放档位取用图标、光标、位图字体等资源是缩放相关的进入 HighDPI 模式后系统必须能加载对应的更清晰版本。艺术指导2x 资源该长什么样文档对 2x 资源的绘制提出了非常具体的艺术规范一张 2x 资源应当看起来与对应的 1x 资源一致只是锯齿更少。1x 中 1 像素宽的横线或竖线在 2x 中应为 2 像素宽。对黑白图像的一个实用准则先用最近邻nearest-neighbor算法把 1x 位图放大到 200%然后在保持黑色像素总数不变的前提下微调黑色像素的位置来柔化对角线边缘。如果实在做不到宁可把图标做得更小也不要更大。调试技巧在 HighDPI 模式下使用Ctrl-Shift-Super-I快捷键可以在低分辨率图标与高分辨率图标之间即时切换方便对比两套资源的效果。一个重要的概念澄清1x 的 32x32 位图与 2x 的 16x16 位图虽然像素数相同都是 32x32但不应被要求长得一样。2x 的 16x16 应当精确复现 1x 的 16x16 外观、只是边缘更平滑而 1x 的 32x32 资源则可以另行选择不同的取景crop。文档以瓢虫ladybugemoji 为例当前 1x 的 7x10 瓢虫图因为空间原因只画了壳2x 版本也应只画壳如果未来新增 1x 的 14x20 高分辨率瓢虫图则可以补上腿——而对应的 14x20 的 2x 版本再把壳腿都做得不那么锯齿。目录结构四种候选方案与最终选择文档回顾了当时res/目录的组织形态res/ cursors/ arrowx2y2.png ... emoji/ (currently all 7x10 px) U1F346.png ... fonts/ CsillaRegular10.font ... graphics/ brand-banner.png ... icons/ 16x16/ 小应用图标、小文件类型图标、工具栏图标、窗口按钮…… 32x32/ 大应用图标、大文件类型图标、消息框图标 各应用专属的 UI 图目录文档标注 XXX或考虑迁入 apps 子目录 themes/ Coffee/ 16x16/ 自定义窗口按钮 更多主题 ... wallpapers/ 桌面壁纸每一类资源理论上都可能长出 2x 变体未来若有更多缩放档位还会有更多变体。文档讨论了四种候选目录/命名结构在 res 根下做 1x/2x 大分叉内部镜像原目录树res/1x/cursors/…、res/2x/cursors/…。优点一眼看清系统支持哪些缩放档位结构上也美观。在每个叶子目录内部分 1x/2x 子目录res/cursors/1x/arrowx2y2.png、res/cursors/2x/arrowx2y2.png。使用文件名后缀而非目录类似 macOS 的2x约定如arrowx2y2.png与arrowx2y22x.png并列。优点能直观看出哪些图标缺少高分辨率版本代价是图标目录会被塞得更满而且只需要看文件 basename 就能得到位图的固有缩放因子无需依赖目录层级。给目录名加后缀如新增cursors-2x/与cursors/平级。文档的权衡结论是根级大分叉能直观看出有哪些缩放档位、主观上也赏心悦目文件后缀便于发现缺失的高分辨率图标并且如果未来资源系统引入更多维度如 UI 语言、明暗模式、屏幕尺寸等Android 就是这样做的后缀体系比嵌套目录更容易扩展。虽然文档也承认最终选哪种其实差别不大但最终决定采用文件名加-2x后缀的方案。当前仓库完全印证了这一决定。在 Base/res 下可以找到大量遵循-2x命名的资源例如光标Base/res/cursor-themes/Default/arrow.x2y2-2x.png、Base/res/cursor-themes/Default/i-beam-2x.png、Base/res/cursor-themes/Default/move-2x.png、Base/res/cursor-themes/Default/wait.f14t100-2x.png等Dark 主题下也有对应的-2x版本图标Base/res/icons/16x16/window-close-2x.png、Base/res/icons/16x16/window-restore-2x.png、Base/res/icons/16x16/upward-triangle-2x.png等图形Base/res/graphics/brand-banner-2x.png。资源加载策略的三种候选及其权衡文档比较了三种加载策略策略实现方式优点缺点① 主动加载单一缩放档位缩放因子变化时重载显式代码处理内存最省每个资源只有一份心智模型简单Bitmap 仍是 BitmapFont 仍是 Font无需集合类需要显式代码当前 LibGfx 中散落着加载图标的代码而缩放因子变化事件更像是 LibGUI 层面的概念事件如何穿透到那里尚不清晰② BitmapCollection 持有高/低分辨率路径按需惰性加载框架承载复杂度复杂度收进框架应用无需关心可透明地以 1x 与 2x 分别绘制到不同 backbuffer例如多屏幕缩放因子不同首次绘制时要在 UI 线程做同步磁盘访问或改为各档位都急切加载压缩数据、仅解码惰性化但那样仍有 UI 线程阻塞式解码且 2x 压缩数据即使永远用不到也常驻内存③ 两个档位都急切加载绘制时按需取用类似GUI::Icon概念上最容易理解但仍需某种集合类复杂度更少地收进框架同样支持多屏幕多缩放 backbuffer在 1x 模式下有约 400% 的内存开销好在多数图标很小文档的结论是这个取舍当时还没有最终定论但窗口服务器中的若干位置已先行采用方案①按当前缩放因子加载、切换时重载。有趣的是仓库最终同时落地了方案②的雏形WindowServer中出现了专门承载一份图像多档位位图的集合类MultiScaleBitmaps见 Userland/Services/WindowServer/MultiScaleBitmaps.h 与 Userland/Services/WindowServer/MultiScaleBitmaps.cpp。其核心是HashMapint, NonnullRefPtrGfx::Bitmap m_bitmapsbitmap(int scale_factor)精确命中给定缩放档位找不到则回退到 1x再找不到就取集合中任意一张VERIFY_NOT_REACHED兜底。load(filename, default_filename)通过Screen::for_each_scale_factor_in_use枚举当前实际使用的所有缩放因子对每个因子调用Gfx::Bitmap::load_from_file(path, scale_factor)加载对应档位若全部失败且提供了default_filename则改用默认文件名再试一轮。多档位位图的BitmapFormat必须一致否则会打印格式不一致的日志并优雅忽略。WindowServer的光标类Cursor也采用了同样的精确档位优先、回退 1x的查找逻辑见 Userland/Services/WindowServer/Cursor.h内部同样是HashMapint, NonnullRefPtrGfx::Bitmap const。这说明文档设想的collection 存储多档位资源已经在窗口服务器侧落地。资源加载 API 的演进文档给出了改造前后的 API 形态。当时典型的资源加载代码// 图标LibGUI 层 auto app_icon GUI::Icon::default_icon(app-gml-playground); // 位图LibGfx 层 s_unfilled_circle_bitmap Bitmap::load_from_file(/res/icons/serenity/unfilled-radio-circle.png); // 字体LibGfx 层 header.set_font(Gfx::Font::load_from_file(/res/fonts/PebbletonBold14.font));而将来的 API 形态在文档写作时被标注为FIXME (depends on loading strategy decision a bit?)即依赖前述加载策略的决策。对照当前源码这个 FIXME 已经得到了解决Gfx::Bitmap的加载接口全面引入了scale_factor参数。见 Userland/Libraries/LibGfx/Bitmap.h[[nodiscard]] static ErrorOrNonnullRefPtrBitmap load_from_file(StringView path, int scale_factor 1, OptionalIntSize ideal_size {}); [[nodiscard]] static ErrorOrNonnullRefPtrBitmap load_from_uri(StringView uri, int scale_factor 1, OptionalIntSize ideal_size {});其底层实现load_scaled_bitmap见 Userland/Libraries/LibGfx/Bitmap.cpp完全按照文档选定的-2x后缀约定工作用LexicalPath拆出路径的目录、主文件名与扩展名拼出高分辨率候选路径{dirname}/{title}-{scale}x.{extension}例如/res/icons/serenity/unfilled-radio-circle-2x.png尝试加载该路径成功后校验位图宽高都能被scale_factor整除否则报错Bitmap::load_from_file: HighDPI image size should be divisible by scale factor把位图逻辑尺寸除以scale_factor并记录m_scale scale_factor——即位图以逻辑尺寸对外暴露物理像素藏在内部。load_from_fileBitmap.cpp则约定仅当scale_factor 1且路径以/res/开头时才先尝试加载缩放版本若因文件不存在ENOENT失败就静默回退到基础 1x 版本其他错误则打印Couldnt load scaled bitmap并同样回退。对resource://URI 也有完全对称的逻辑load_from_uri。这一优先高分辨率、缺失即回退的机制正是文档目录结构讨论中后缀方案便于发现哪些图标缺少高分辨率版本的实际运用。实施计划从 Painter 缩放到全应用 HighDPI 的六步路线文档给出了从现状到所有应用都使用 HighDPI backbuffer的增量实施计划步骤 0先给Painter加缩放支持——在绘制时对所有内容做 2x 最近邻缩放。步骤 1给 WindowServer 引入 scale factor 概念。WindowServer 拥有缩放的帧缓冲/backbuffer其余位图无论是 WindowServer 内部还是客户端侧一律以 1x 存储绘制到帧缓冲时才放大。此时 2x 下观感尚可但会偏像素风不过窗口渐变已经可以是平滑的。步骤 2让 DisplaySettings 能切换 WindowServer 的 scale factor从而可以用 UI 动态开关 HighDPI。步骤 3建立按缩放档位取位图/字体资源的体系并用它让窗口服务器使用高分辨率光标位图与高分辨率菜单栏文字。此时菜单文字与光标不再发糊窗口边框也理应受益。步骤 4允许应用逐个选择加入opt in高分辨率窗口帧缓冲并逐一改造所有应用。步骤 5所有应用都支持后移除 opt-in 开关。文档同时标注了写作时的进度Were currently in the middle of point 3.——即处于第 3 步的中途窗口服务器的一部分图标已经是高分辨率但字体尚未高分辨率化且窗口服务器内部拥有独立 backing store 的部件如菜单也还未完成。仓库现状实施计划在源码中的对应物从当前源码看这套路线图的多个环节都已有实体实现scale factor 成为 ScreenLayout 的一等公民Userland/Services/WindowServer/ScreenLayout.h 的屏幕配置结构直接包含int scale_factor字段虚拟矩形由物理分辨率除以缩放因子得到{ resolution.width() / scale_factor, resolution.height() / scale_factor }。Screen 维护当前在用的缩放因子集合Userland/Services/WindowServer/Screen.cpp 通过Vectorint, default_scale_factors_in_use_count s_scale_factors_in_use与update_scale_factors_in_use()汇总当前所有屏幕的缩放因子并在屏幕配置变化时触发scale_factor_changed()。这正是MultiScaleBitmaps::load()遍历各档位加载资源所依赖的机制也对应实施计划步骤 12 的WindowServer 拥有缩放概念并可动态切换。物理像素只在最末端出现Screen.cpp中把逻辑矩形换算成硬件坐标时才对 x/y/宽/高统一乘以scale_factor例如flush_rect.x * scale_factor这与文档物理坐标尽量不跨 API 边界的治理原则一致。应用层 opt-in 的架构基础Bitmap::allocate_backing_store等接口已支持scale_factor参数见 Bitmap.h为步骤 4 的应用逐个选择加入高分辨率窗口帧缓冲提供了底层能力。总结SerenityOS 的 HighDPI 设计以整数倍缩放优先、先做 2x为起点最终形成了一套自洽的体系坐标系上严格区分逻辑坐标与physical_物理坐标资源组织上采用-2x文件名后缀并配合优先加载高分辨率、缺失回退 1x的加载链运行时由 WindowServer 汇总各屏缩放因子通过MultiScaleBitmaps这类集合在绘制时选取正确档位。这套设计既保持了 1x 时代的编程模型简单性也为未来多缩放档位、多屏幕异构缩放乃至更多资源维度预留了扩展空间。读者若想深入验证可重点阅读 Userland/Libraries/LibGfx/Bitmap.cpp 的load_scaled_bitmap实现、Userland/Services/WindowServer/MultiScaleBitmaps.cpp 的多档位加载逻辑以及 Base/res/cursor-themes/Default 下成对出现的 1x 与-2x资源文件。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表