Recreating RhinoCommon ViewportControl in C++

Hello,

I’m attempting to recreate the RhinoCommon ViewportControl in C++ as a Qt Widget using RhinoInside. However, the view simply appears as a black screen. I’ve spent a few days tinkering trying all kinds of different things but I just can’t get anything other than a black image. I’m thinking I’m probably missing something with the way I’m launching RhinoInside.

Attached is a bare bones console app project that simply attempts to launch Rhino, add an object to the document, zoom extents, then capture the viewport. The resulting image is always black.

Any thoughts on what I’m doing wrong and what I need to do differently to make this work?

Thanks!

ConsoleRhinoInside.zip (4.4 KB)

Hi @lukeniwranski,

Unless Rhino is visible, and has a chance to initialize it’s display pipeline, OpenGL in this case, you’ll not be able to “capture” any viewport.

– Dale

Hi @dale,

How does the RhinoCommon Viewport Control work around not having Rhino visible? Ultimately that’s what I’m trying to do, recreate the Viewport Control as a Qt Widget. I was getting nothing but black when attempting to copy the CRhinoView display context to my Qt Widget. I figured because I had the same result for capturing the viewport the issue would be the same.

I’m working off this sample. It’s pretty much exactly what I need but in C# RhinoCommon.

Thanks,

Hi @dale,

I’ve modified my code so that Rhino isn’t hidden. I can now successfully capture the viewport. Furthermore, my Qt widget is also working as expected with all the same controls as the WinFormsApp sample project!

The only remaining hurdle is the need for Rhino to be open. How does the WinFormsApp manage to display a Rhino viewport control without the need for Rhino to be open? Is there a way for me to initialize the Rhino display pipeline while it’s hidden?

Thanks!

Wow, that’s really cool. It would be nice to see this working example.

Rhino does open, but it uses Rhino.Runtime.InProcess.WindowStyle.Hidden to have it open hidden

Hi @stevebaer,

I will for sure post the code once I have it working!

RhinoCommon must be doing some stuff behind the scenes. If I open Rhino with Rhino.Runtime.InProcess.WindowStyle.Hidden, the viewport is just black. I’m making progress though. I’m now able to render a single frame to my control with the Rhino window hidden, but I’m getting an exception on the second frame.

Currently I create my new view like this.

CRhinoDoc* pDoc = RhinoApp().ObsoleteActiveDoc();
CRhinoView* pView = CRhinoView::FromRuntimeSerialNumber(pDoc->CreateRhinoView(nullptr, true))
if (nullptr == m_pView)
	return;

// Setup the display pipeline
m_pView->SetupDisplayPipeline();

// Display attributes
CDisplayPipelineAttributes* pDP = new CDisplayPipelineAttributes(*CRhinoDisplayAttrsMgr::StdShadedAttrs());

I set up my render device context like this.

// Create a DC for rendering
HDC displayDC = ::GetDC(NULL);
m_RenderDC = ::CreateCompatibleDC(displayDC);
::ReleaseDC(NULL, displayDC);

// Create a bitmap buffer
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -height; // yup, negative for top-down order
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
uchar* data = nullptr;
m_RenderBitmap = ::CreateDIBSection(m_RenderDC, &bmi, DIB_RGB_COLORS, reinterpret_cast<void**>(&data), NULL, 0);

// Create a Qt wrapper over the bitmap data
m_RenderImage = QImage(data, width, height, QImage::Format_RGB32);

Then for my paint event, I do this. This works fine the first time it is called, then throws an exception somewhere down in DrawToDC the second time my paint event is triggered.

HGDIOBJ prevObj = ::SelectObject(m_RenderDC, m_RenderBitmap);
pPipeline->DrawToDC(m_RenderDC, w, h, *pDP);
::SelectObject(m_RenderDC, prevObj);

// Qt stuff
QPainter painter(this);
painter.drawImage(0, 0, m_RenderImage);

I admit, device contexts are not my specialty and I may not be handling them correctly. I’m not sure what the best practices are for display pipelines. Is there anything I need to be doing to manage the display pipeline?

This is the stack trace I get when the exception is thrown.

KernelBase.dll!RaiseException()	Unknown
vcruntime140.dll!_CxxThrowException(void * pExceptionObject, const _s__ThrowInfo * pThrowInfo) Line 80	C++
mfc140u.dll!AfxThrowResourceException() Line 1347	C++
mfc140u.dll!AfxRegisterWndClass(unsigned int nClassStyle, HICON__ * hCursor, HBRUSH__ * hbrBackground, HICON__ * hIcon) Line 1510	C++
[Inline Frame] RhinoCore.dll!COnScreenBuffer::{ctor}(int) Line 329	C++
RhinoCore.dll!CRhinoDisplayPipeline::CreateOnScreenBuffer(int nW, int nH) Line 1739	C++
RhinoCore.dll!CRhinoDisplayPipeline::CreateEngine() Line 2123	C++
RhinoCore.dll!CRhinoDisplayPipeline::InitializeEngine() Line 12890	C++
RhinoCore.dll!CRhinoDisplayPipeline::ClonePipeline(CRhinoViewport & vp) Line 16319	C++
RhinoCore.dll!CRhinoDisplayPipeline::DrawToDC(HDC__ * pDC, int nWidth, int nHeight, const CDisplayPipelineAttributes & attrs) Line 18360	C++
QtRhinoInsideTest.exe!QRhinoView::paintEvent(QPaintEvent * pEvent) Line 89	C++

Thanks,

The Windows Form sample is directly drawing OpenGL into the HWND and not copying bitmaps around. This is what the ViewportControl is set up for. Here’s the code for the Viewport control if that helps

using System;
using System.Windows.Forms;

namespace RhinoWindows.Forms.Controls
{
  public class ViewportControl : System.Windows.Forms.Control
  {
    const int CS_VREDRAW = 0x1;
    const int CS_HREDRAW = 0x2;
    const int CS_OWNDC = 0x20;

    Rhino.Display.RhinoViewport m_viewport;
    IntPtr m_ptr_viewport = IntPtr.Zero;
    Rhino.Display.DisplayPipeline m_display_pipeline;

    public ViewportControl()
    {
      SetStyle(System.Windows.Forms.ControlStyles.AllPaintingInWmPaint, true);
      SetStyle(ControlStyles.UserMouse, true);
      PreviousMouseLocation = System.Drawing.Point.Empty;
    }

    protected override System.Windows.Forms.CreateParams CreateParams
    {
      get
      {
        // OpenGL likes to have some class styles set
        var cp = base.CreateParams;
        cp.ClassStyle |= CS_VREDRAW | CS_HREDRAW | CS_OWNDC;
        return cp; 
      }
    }
    int m_paint_fails = 0;
    protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
    {
      if( !Rhino.Runtime.HostUtils.RunningInRhino )
      {
        e.Graphics.Clear(System.Drawing.Color.LightGray);
        base.OnPaint(e);
        return;
      }

      IntPtr hwnd = Handle;
      IntPtr hdc = e.Graphics.GetHdc();
      SetupViewport();

      bool rc = UnsafeNativeMethods.CRhinoViewport_Draw(hwnd, hdc, m_ptr_viewport);
      if (!rc)
      {
        m_paint_fails++;
        if( m_paint_fails < 4 )
          Invalidate();
      }
    }
    protected override void OnResize(EventArgs e)
    {
      if( m_ptr_viewport!=IntPtr.Zero )
      {
        var sz = ClientSize;
        UnsafeNativeMethods.CRhinoViewport_SetScreenSize(m_ptr_viewport, sz.Width, sz.Height);
      }
      base.OnResize(e);
    }

    protected override void OnPaintBackground(System.Windows.Forms.PaintEventArgs e){}

    protected override void Dispose(bool disposing)
    {
      if( m_viewport!=null )
      {
        m_ptr_viewport = IntPtr.Zero;
        m_viewport.Dispose();
        m_viewport = null;
      }
      base.Dispose(disposing);
    }

    void SetupViewport()
    {
      // 10 Jan 2021 S. Baer (RH-61813)
      // The viewport control currently just tracks the "active document". Add
      // a test to update the control to reference the active doc when the old
      // reference is no longer valid.
      if (m_ptr_viewport != IntPtr.Zero)
      {
        UnsafeNativeMethods.CRhinoViewport_ValidateDoc(m_ptr_viewport, true);
      }

      if (m_viewport == null && Rhino.Runtime.HostUtils.RunningInRhino)
      {
        m_viewport = new Rhino.Display.RhinoViewport();
        m_viewport.SetProjection(Rhino.Display.DefinedViewportProjection.Perspective, "Perspective", true);
        m_viewport.SetCameraLocation(new Rhino.Geometry.Point3d(50, -75, 50), false);
        m_viewport.SetCameraTarget(Rhino.Geometry.Point3d.Origin, false);
        var sz = ClientSize;
        if (sz.IsEmpty)
          sz = new System.Drawing.Size(10, 10);
        m_ptr_viewport = Rhino.Runtime.Interop.NativeNonConstPointer(m_viewport);
        UnsafeNativeMethods.CRhinoViewport_SetScreenSize(m_ptr_viewport, sz.Width, sz.Height);
        if (Rhino.RhinoDoc.ActiveDoc.Objects.ObjectCount(new Rhino.DocObjects.ObjectEnumeratorSettings()) == 0)
          m_viewport.ZoomBoundingBox(new Rhino.Geometry.BoundingBox(-200, -200, -200, 200, 200, 200));
        else
          m_viewport.ZoomExtents();
      }
    }

    /// <summary>
    /// Viewport settings for this control
    /// </summary>
    public Rhino.Display.RhinoViewport Viewport
    {
      get
      {
        SetupViewport();
        return m_viewport;
      }
    }

    /// <summary>
    /// Display pipeline that this control uses for drawing.
    /// </summary>
    public Rhino.Display.DisplayPipeline Display
    {
      get
      {
        if(m_display_pipeline==null)
        {
          IntPtr hwnd = Handle;
          var vp = Viewport;
          if (vp == null)
            return null;
          IntPtr ptrViewport = vp.NonConstPointer();
          IntPtr ptrPipeline = UnsafeNativeMethods.CRhinoViewport_DisplayPipeline(hwnd, ptrViewport);
          if (IntPtr.Zero == ptrPipeline)
            return null;
          m_display_pipeline = new Rhino.Display.DisplayPipeline(ptrPipeline);
        }
        return m_display_pipeline;
      }
    }

    public System.Drawing.Point PreviousMouseLocation
    {
      get;
      private set;
    }

    protected override void OnMouseMove(System.Windows.Forms.MouseEventArgs e)
    {
      if (!Rhino.Runtime.HostUtils.RunningInRhino)
        return;
      var new_loc = e.Location;
      bool refresh = false;
      if (!PreviousMouseLocation.IsEmpty && m_ptr_viewport != IntPtr.Zero)
      {
        if (e.Button == MouseButtons.Right)
        {
          Viewport.MouseRotateAroundTarget(PreviousMouseLocation, new_loc);
          refresh = true;
        }
        if (e.Button == MouseButtons.Left)
        {
          Viewport.MouseLateralDolly(PreviousMouseLocation, new_loc);
          refresh = true;
        }
      }
      base.OnMouseMove(e);
      if (refresh)
        Refresh();
      PreviousMouseLocation = new_loc;
    }

    protected override void OnMouseWheel(MouseEventArgs e)
    {
      if (!Rhino.Runtime.HostUtils.RunningInRhino)
        return;

      double magnification_factor = 1.0 / Rhino.ApplicationSettings.ViewSettings.ZoomScale;
      magnification_factor *= magnification_factor;
      magnification_factor *= e.Delta < 0 ? -1.0 : 1.0;
      if (magnification_factor < 0.0)
        magnification_factor = -1.0 / magnification_factor;

      Viewport.Magnify(magnification_factor, true);
      base.OnMouseWheel(e);
      Refresh();
    }
  }
}

It looks like the magic happens in UnsafeNativeMethods.CRhinoViewport_Draw(hwnd, hdc, m_ptr_viewport);, but I don’t see a function in CRhinoViewport with a similar signature, so I’m not sure what the C++ equivalent would be.

Directly drawing to the HWND would be ideal. I’ll explore the display engines in more depth tomorrow. It looks like my pipeline is using GDI, so I’m probably missing something with initialization.

Thanks!

Alright, I’m hitting a wall here.

Qt widgets don’t expose their device contexts, so I can’t render directly to the control. The best alternative is to render to an intermediate device context or to a bitmap. But I’m running into the same problem as earlier.

My first call to CRhinoDisplayPipeline::DrawToDC (or CRhinoDisplayPipeline::DrawToDib) succeeds and I see the viewport perfectly rendered. Subsequent calls all throw a CResourceException (call stack is below). I’ve tried all kinds of things to prevent this, but I’m not having any luck. The exception seems to originate from within the constructor of COnScreenBuffer when calling AfxRegisterWndClass.

From a Rhino perspective, is there anything I can do to prevent CRhinoDisplayPipeline::DrawToDC eventually resulting in an exception? Are there any requirements for how I prepare my device context or bitmap (I’m using CRhinoDib created on the stack)? If I can work through this then I have a working viewport in C++ as a Qt widget.

KernelBase.dll!RaiseException()	Unknown
vcruntime140.dll!_CxxThrowException(void * pExceptionObject, const _s__ThrowInfo * pThrowInfo) Line 80	C++
mfc140u.dll!AfxThrowResourceException() Line 1347	C++
mfc140u.dll!AfxRegisterWndClass(unsigned int nClassStyle, HICON__ * hCursor, HBRUSH__ * hbrBackground, HICON__ * hIcon) Line 1510	C++
[Inline Frame] RhinoCore.dll!COnScreenBuffer::{ctor}(int) Line 329	C++
RhinoCore.dll!CRhinoDisplayPipeline::CreateOnScreenBuffer(int nW, int nH) Line 1739	C++
RhinoCore.dll!CRhinoDisplayPipeline::CreateEngine() Line 2123	C++
RhinoCore.dll!CRhinoDisplayPipeline::InitializeEngine() Line 12890	C++
RhinoCore.dll!CRhinoDisplayPipeline::ClonePipeline(CRhinoViewport & vp) Line 16319	C++
RhinoCore.dll!CRhinoDisplayPipeline::DrawToDC(HDC__ * pDC, int nWidth, int nHeight, const CDisplayPipelineAttributes & attrs) Line 18360	C++
QtRhinoInsideTest.exe!QRhinoView::paintEvent(QPaintEvent * pEvent) Line 89	C++

Thanks!

@stevebaer I’m trying a different approach. Because Qt doesn’t expose the device context of the control, I need to render to bitmap regardless. I’ve been trying to enable frame capture on the display pipeline, but no matter what I do, the resulting bitmap is just a black 100x100 square. How can I enable frame capture on the pipeline? Or is there a better way to capture a bitmap of the viewport?

Thanks,

It looks like Qt lets you get access to the native HWND using
HWND hWnd = reinterpret_cast<HWND>(widget->winId();

Drawing to bitmap is going to get you very very slow performance. I also don’t know how the code is “flowing” here as I’ve never done this experiment.

Hi @stevebaer

I’ve tried this without success. CRhinoDisplayPipeline::DrawToDC succeeds for the first frame, but always throws a CResourceException on the second frame (Same with DrawToDib). Any thoughts on why that may be happening?

KernelBase.dll!RaiseException()	Unknown
vcruntime140.dll!_CxxThrowException(void * pExceptionObject, const _s__ThrowInfo * pThrowInfo) Line 80	C++
mfc140u.dll!AfxThrowResourceException() Line 1347	C++
mfc140u.dll!AfxRegisterWndClass(unsigned int nClassStyle, HICON__ * hCursor, HBRUSH__ * hbrBackground, HICON__ * hIcon) Line 1510	C++
[Inline Frame] RhinoCore.dll!COnScreenBuffer::{ctor}(int) Line 329	C++
RhinoCore.dll!CRhinoDisplayPipeline::CreateOnScreenBuffer(int nW, int nH) Line 1739	C++
RhinoCore.dll!CRhinoDisplayPipeline::CreateEngine() Line 2123	C++
RhinoCore.dll!CRhinoDisplayPipeline::InitializeEngine() Line 12890	C++
RhinoCore.dll!CRhinoDisplayPipeline::ClonePipeline(CRhinoViewport & vp) Line 16319	C++
RhinoCore.dll!CRhinoDisplayPipeline::DrawToDC(HDC__ * pDC, int nWidth, int nHeight, const CDisplayPipelineAttributes & attrs) Line 18360	C++
QtRhinoInsideTest.exe!QRhinoView::paintEvent(QPaintEvent * pEvent) Line 89	C++

I can provide code if that would help, but compiling it is dependent on having Qt installed.

Thanks,

Sorry, but I don’t have the bandwidth to install Qt and compile a sample like this right now. It is strange that it works the first frame and not on the second frame.

Hi @stevebaer,

Here is a sample console app project that reproduces the exception, no need for Qt. Just compile and run. I’m really looking forward to getting this working!

Thanks,

-Luke

ConsoleRhinoInside.zip (5.6 KB)