Java AWT drawImage 竞争条件 - 如何使用同步来避免它

发布于 2024-10-16 20:39:45 字数 2787 浏览 8 评论 0原文

经过几个小时的调试和分析,我终于成功地找出了竞争条件的原因。解决掉就是另外一回事了!

为了查看竞争条件的实际情况,我在调试过程中以某种方式录制了一段视频。此后我进一步了解了情况,因此请原谅糟糕的评论和调试过程中实现的愚蠢机制。

http://screencast.com/t/aTAk1NOVanjR

所以,情况是:我们有一个双缓冲实现一个表面(即 java.awt.Frame 或 Window),其中有一个正在进行的线程,该线程本质上是连续循环的,调用渲染进程(执行 UI 布局并将其渲染到后备缓冲区),然后在渲染后,位图传输渲染区域从后台缓冲区到屏幕。

这是伪代码版本(完整版本 Surface.java) 双缓冲渲染:

public RenderedRegions render() {
    // pseudo code
    RenderedRegions r = super.render();
    if (r==null) // nothing rendered
        return
    for (region in r)
        establish max bounds
    blit(max bounds)
    return r;
}

与任何 AWT 表面实现一样,它也实现(第 507 行)在 AWT.java 中 - 链接限制 :( - 使用 Surface.java 链接,用 plat/AWT.java 替换 core/Surface.java)绘制/更新覆盖也从后缓冲区传输到屏幕:

        public void paint(Graphics gr) {
            Rectangle r = gr.getClipBounds();
            refreshFromBackbuffer(r.x - leftInset, r.y - topInset, r.width, r.height);
        }

使用drawImage()函数实现Blitting(AWT.java中的第371行):(

    /** synchronized as otherwise it is possible to blit before images have been rendered to the backbuffer */
    public synchronized void blit(PixelBuffer s, int sx, int sy, int dx, int dy, int dx2, int dy2) {
        discoverInsets();
        try {
            window.getGraphics().drawImage(((AWTPixelBuffer)s).i,
                              dx + leftInset, dy + topInset,     // destination topleft corner
                              dx2 + leftInset, dy2 + topInset,   // destination bottomright corner
                              sx, sy,                            // source topleft corner
                              sx + (dx2 - dx), sy + (dy2 - dy),  // source bottomright corner
                              null);
        } catch (NullPointerException npe) { /* FIXME: handle this gracefully */ }
    }

警告:这是我开始做出假设的地方!)

这里的问题似乎是drawImage是异步的,并且来自refreshBackBuffer()的blit通过首先调用paint/update,然后

再调用blit。因此,防止竞争条件的明显方法不起作用:(

到目前为止,我已经提出了两种解决方案 。 ,但它们都不理想:

  1. 在下一个渲染通道上重新位图
    缺点:性能受到影响,在遇到竞争条件时仍然会出现一点闪烁(有效屏幕 -> 无效屏幕 -> 有效屏幕)

  2. 不要在绘制/更新时进行 blit,而是设置刷新边界并使用这些边界在下一个渲染通道
    缺点:当屏幕失效且主应用程序线程正在追赶时,会出现黑色闪烁

这里(1)似乎是两害相权取其轻。 编辑:(2)不起作用,出现空白屏幕...(1)工作正常,但只是掩盖了可能仍然存在的问题。

我所希望的,似乎由于我对synchronized及其使用方法的理解薄弱,无法想象出一种锁定机制,它以某种方式解释了drawImage()的异步性质。

或者也许使用 ImageObserver?

请注意,由于应用程序的性质(Vexi,对于那些感兴趣的人来说,网站已过时,我只能使用 2 个超链接)渲染线程必须位于绘制/更新之外 - 它具有单线程脚本模型和布局过程(渲染的子过程)调用脚本。

After many hours of debugging and analysis, I have finally managed to isolate the cause of a race condition. Solving it is another matter!

To see the race condition in action, I recorded a video some way in to the debugging process. I have since furthered my understanding of the situation so please forgive the poor commentary and the silly mechanisms implemented as part of the debugging process.

http://screencast.com/t/aTAk1NOVanjR

So, the situation: we have a double buffered implementation of a surface (i.e. java.awt.Frame or Window) where there is an ongoing thread that essentially loops continuously, invoking the render process (which performs UI layout and renders it to the backbuffer) and then, post-render, blits the rendered area from backbuffer to screen.

Here's the pseudo-code version (full version line 824 of Surface.java) of the double buffered render:

public RenderedRegions render() {
    // pseudo code
    RenderedRegions r = super.render();
    if (r==null) // nothing rendered
        return
    for (region in r)
        establish max bounds
    blit(max bounds)
    return r;
}

As with any AWT surface implementation, it also implements (line 507 in AWT.java - link limit :( - use Surface.java link, replace core/Surface.java with plat/AWT.java) the paint/update overrides which also blit from the backbuffer to the screen:

        public void paint(Graphics gr) {
            Rectangle r = gr.getClipBounds();
            refreshFromBackbuffer(r.x - leftInset, r.y - topInset, r.width, r.height);
        }

Blitting is implemented (line 371 in AWT.java) using the drawImage() function:

    /** synchronized as otherwise it is possible to blit before images have been rendered to the backbuffer */
    public synchronized void blit(PixelBuffer s, int sx, int sy, int dx, int dy, int dx2, int dy2) {
        discoverInsets();
        try {
            window.getGraphics().drawImage(((AWTPixelBuffer)s).i,
                              dx + leftInset, dy + topInset,     // destination topleft corner
                              dx2 + leftInset, dy2 + topInset,   // destination bottomright corner
                              sx, sy,                            // source topleft corner
                              sx + (dx2 - dx), sy + (dy2 - dy),  // source bottomright corner
                              null);
        } catch (NullPointerException npe) { /* FIXME: handle this gracefully */ }
    }

(Warning: this is where I start making assumptions!)

The problem here seems to be that drawImage is asynchronous and that a blit from refreshBackBuffer() via paint/update is called first but occurs second.

So... blit is already synchronized. The obvious way of preventing the race condition doesn't work. :(

So far I have come up with two solutions, but neither of them are ideal:

  1. re-blit on the next render pass
    cons: performance hit, still get a bit of flicker due when encountering the race condition (valid screen -> invalid screen -> valid screen)

  2. do not blit on paint/update, instead set refresh bounds and use those bounds on next render pass
    cons: get black flicker when the screen is invalidated and the main application thread is catching up

Here (1) seems to be the lesser of two evils. Edit: and (2) doesn't work, getting blank screens... (1) works fine but is just masking the problem which is potentially still there.

What I'm hoping for, and seem unable to conjure up due to my weak understanding of synchronized and how to use it, is a locking mechanism that somehow accounts for the asynchronous nature of drawImage().

Or perhaps use ImageObserver?

Note that due to the nature of the application (Vexi, for those interested, website is out of date and I can only use 2 hyperlinks) the render thread must be outside of paint/update - it has a single-threaded script model and the layout process (a sub-process of render) invokes script.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

浮光之海 2024-10-23 20:39:46

更新:这里有一个好方法:AWT 自定义渲染 - 捕获平滑的调整大小并消除调整大小闪烁


这里的答案是从 paint() 线程中删除所有位块传送,即仅从后台缓冲区中刷新程序线程。这与 Jochen Bedersdorfer 所建议的答案相反,但他的答案永远不会对我们有用,因为该程序有自己的脚本模型,该模型与驱动渲染的布局模型集成在一起,因此这一切都必须按顺序发生。

(推测)一些问题源于 Java 中带有加速图形芯片组的多显示器支持不够出色,正如我遇到的 适配使用BufferStrategy时出现这个问题,这是一个direct3d+Java的差异。

本质上,paint()update() 被简化为阻塞调用。这种方法效果好很多,但有一个缺点——无法平滑地调整大小。

private class InnerFrame extends Frame() {
    public void update(Graphics g) { }
    public void paint(Graphics g) { }
    ....
}

我最终使用了缓冲区策略,尽管我对这种方法不是 100% 满意,因为在我看来,渲染到图像,然后将完整图像复制到 BufferStrategy,然后执行 show 效率很低。 () 到屏幕。

我还实现了一个基于 Swing 的替代方案,但我也不是特别喜欢它。它使用带有 ImageIcon 的 JLabel,由此程序线程(不是 EDT)绘制由 ImageIcon 包装的图像。

我确信当我有更多时间更有目的地研究这个问题时,我会问一个后续问题,但现在我有两个工作实现或多或少解决了此处发布的最初问题 - 我学到了发现它们真是太棒了。

Update: good approach here: AWT custom rendering - capture smooth resizes and eliminate resize flicker


The answer here was to remove all blitting from the paint() thread i.e. only ever refresh from the backbuffer in the program thread. This is the opposite to the answer as suggested by Jochen Bedersdorfer, but his answer was never going to work for us because the program has its own scripting model that is integrated with the layout model which drives rendering, thus it all has to happen sequentially.

(Speculation) Some of the problems stem from a less-than-stellar multiple monitor support in Java with accelerated graphics chipsets, as I ran in to this problem when adapting to use BufferStrategy, which was a direct3d+Java discrepancy.

Essentially paint() and update() are reduced to blocking calls. This works a lot better but has one drawback - there is no smooth resizing.

private class InnerFrame extends Frame() {
    public void update(Graphics g) { }
    public void paint(Graphics g) { }
    ....
}

I ended up using a buffer strategy although I'm not 100% satisfied with this approach as it seems to me that it is inefficient to be rendering to an image, then copying the full image to the BufferStrategy and then performing a show() to screen.

I also implemented a Swing-based alternative, but again I don't particularly like that. It uses a JLabel with an ImageIcon, whereby the program thread (not the EDT) draws to the Image that is wrapped by the ImageIcon.

I'm sure there's a follow up question for me to ask when I have more time to look into this with more purpose, but for now I have two working implementations that more or less address the initial woes as posted here - and I learnt a helluva lot discovering them.

踏月而来 2024-10-23 20:39:46

不确定,但是如果在 AWT 绘制线程中进行 Blit 会发生什么?

Not sure, but what happens if you Blit in the AWT paint thread?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文