//-----------------------------------------------------------------------------
// rt_Gamma.cpp -- Gamma Correction Sample
//
// Instructions
// ------------
// The program has two main displays: measurement and test.  The program
// starts with the measurement display to characterize the monitor by a
// single gamma.  When you have two displayed areas of equal intensity,
// try testing out your gamma value with 'T'.
//
// The test display consists of an upper and lower portion.  The upper
// portion displays bars of constant grayscale intensity at 0%, 25%, 75%,
// and 100% of full intensity.  The bottom portion displays an interpolated
// grayscale ramp.  The ramp should match in intensity the constant bar
// right in the middle of the constant bar, approximately where the text
// is drawn.  You will find that they probably don't match in windowed
// mode, but do match in exclusive mode when your device supports a
// gamma ramp.  This is normal behavior for this situation.  The best
// quality renderings will be obtained when you can use linear math and
// gamma correct at the video scanout stage.
//
// General keyboard commands:
// T        Toggle between test and measurement.
// 1        Toggle between grayscale gamma and RGB gamma.
//
// Measurement keyboard commands:
// Y        measure grayscale gamma
// R        measure red gamma
// G        measure green gamma
// B        measure blue gamma
// Up/Down  increase/decrease gamma by 1 step
// PageUp/  increase/decrease gamma by 5 steps
// PageDown
//
// Standard D3DFrame keyboard commands:
// F2       Change Device
// ESC      Quit
//
// Background
// ----------
// This sample shows how to measure the gamma of the monitor, with a little
// help from the user.  You can measure a single gamma value that you use
// for all three primaries, or you can measure the gamma of each primary
// separately.
//
// The basic idea behind this technique is as follows.  First you draw a
// rectangle with alternating scanlines of white and black.  When viewed
// from a distance, your eye should average these white and black lines
// together and see the area as a shade of gray.  In fact, because this is
// 50% of the total brightness we can draw into a rectangle, it should be
// half the maximum intensity that we can display.
//
// If you draw a rectangle adjacent to these lines with the color
// (128, 128, 128) approximating 50% gray as linear RGB, you'll find that
// two areas don't appear to have the same intensity at all.  The answer
// to this problem comes from the CRT monitor itself.  The brightness of
// the monitor is nonlinear with respect to the input RGB signals.  The
// intensity of a CRT, with input voltage V in [0,1], is characterized as:
//
//             gamma
//      I = c V     
//
// The phrase "gamma correction" derives from the use of the greek letter
// gamma in this equation.  This equation lets us compensate for the non-
// linear characteristics of a CRT if we can measure its gamma.  (The 'c'
// above is just a proportionality constant.  In graphics we just need
// the gamma because the CRT monitor is manufactured to deal with the 'c'.)
// You can use a piece of dedicated equipment to measure a CRT's gamma
// accurately and directly.  These are often used in the printing press
// industry to ensure color fidelity, but most people don't have one.
//
// In this sample, we effectively measure the CRT's gamma by using the
// human eyeball as the measuring device!  A little feedback from the user
// is all we need to determine the gamma within the resolution of an 8-bit
// color channel.
//
// For best results, run the sample on an 24 or 32 bit desktop or use a
// full screen mode with 8-bits per channel.  The quantization in 16-bit
// formats when changing the solid area's color give a less accurate
// reading, and it may be impossible for the user to match up the
// corresponding sides as close as they could in an 8-bit per channel
// format.
//
// You may also find that the measured gamma of your device varies
// between windowed mode and exclusive mode.  In windowed mode, the device
// is owned by GDI and GDI controls how and if the device's gamma
// correction hardware is used.  For information about gamma correction
// under GDI look at the Platform SDK documentation on Image Color
// Management (ICM).
//
// A Direct3D application can compensate for a monitor's non-linearity
// by using the D3DGAMMARAMP with the SetGammaRamp method on the device.
// However, this only works in exclusive applications and the device must
// have the D3DCAPS2_FULLSCREENGAMMA capability.  The gamma ramp defines
// the mapping from the linear RGB space in which Direct3D computes colors
// into the device-dependent space described by the gamma equation above.
// The gamma ramp is applied during video scanout and is the very last
// thing applied to the pixels before they are sent to the monitor for
// display in full-screen mode.  (The gamma ramp doesn't apply in windowed
// mode.  GDI gamma correction mechanisms are in force then.)
//
// The gamma value can vary for the different phosphors in a monitor, so
// you may want to measure the gamma for the intensity of the red, green,
// and blue phosphors separately.  In this case you use the separate red,
// green and blue gamma values to compute the red, green and blue arrays
// in the D3DGAMMARAMP structure.
//
// Without device gamma ramp support or in windowed mode, an application
// must manually compensate for device gamma.  The easiest way to achieve
// this is to supply the rendering pipeline with gamma-corrected colors,
// combine them as if they were linear and live with the resulting error.
// You have probably been writing your 3D programs this way, blissfully
// ignorant of the CRT's gamma.  At the same time, if you try your program
// out on multiple monitors you may find out that it looks "darker" on
// some monitors and "brighter" on other monitors.  This is the device's
// non-linear response biting you.
//
// This is not the only approach to manual gamma correction, merely the
// easiest.  Another approach would be to use a pixel shader and gamma
// correct in the shader.  You can probably think of some more ways to
// apply gamma correction for your specific needs.
//
// For a more detailed explanation of gamma and its consequences, please
// read the excellent technical memo on gamma correction by Alvy Ray Smith
// at <http://www.alvyray.com/Memos/MemosMicrosoft.htm>.  See also
// "Dirty Pixels", Jim Blinn, IEEE Computer Graphics & Applications,
// July 1989.
//
// Copyright (c) 1997-2000 Microsoft Corporation. All rights reserved.
// Portions Copyright (C) 2001 Rich Thomson.  All rights reserved.
//-----------------------------------------------------------------------------

// C++ standard includes
#include <cmath>
#include <iomanip>
#include <sstream>
#include <vector>

// Win32 includes
#define STRICT
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <commdlg.h>
#include <tchar.h>

// ATL includes
#include <atlbase.h>

// DX8 includes
#include <d3dx8.h>

// D3DFrame includes
#include "D3DApp.h"
#include "D3DFont.h"
#include "D3DUtil.h"
#include "DXUtil.h"

// sample includes
#include "resource.h"

// constants for gamma keyboard controls
const int GAMMA_LINE_SIZE = 1;      // up, down
const int GAMMA_PAGE_SIZE = 5;      // page up, page down
const int HOME_GAMMA = 1;           // home
const int END_GAMMA = 254;          // end

const int INITIAL_GAMMA = 190;      // arbitrary but close for my monitor ;-)

//-----------------------------------------------------------------------------
// C++ standard string library classes in the spirit of <tchar.h>
// An extended implementation of this idea for general Win32 C++ programming
// is in my RT utility library in the include file <rt/tstring.h>.
//
// Fortunately we can get what we need from the C++ standard library
// with a couple of judicious typedefs.
//
// See "The C++ Standard Library", Nicolai Josuttis, 1999 for an
// excellent reference to the C++ Standard Library, including strings,
// streams and all the STL classes.
typedef std::basic_string<TCHAR> tstring;
typedef std::basic_ostringstream<TCHAR> tostringstream;

//-----------------------------------------------------------------------------
// s_vertex
//
// Vertex structure for the lines drawn.
//
struct s_vertex
{
    float m_pos[4];
    D3DCOLOR m_diffuse;
    void set(float x, float y, D3DCOLOR diffuse)
    {
        m_pos[0] = x;
        m_pos[1] = y;
        m_pos[2] = 0.5f;
        m_pos[3] = 2.0f;
        m_diffuse = diffuse;
    }
};
const DWORD VERTEX_FVF = D3DFVF_XYZRHW | D3DFVF_DIFFUSE;

//-----------------------------------------------------------------------------
// NUM_OF
//
// expression evaluating to the number of elements in a fixed-size array,
// usually an array initialized with an aggregate initializer.  Can also
// be the number of characters in TCHARs, not bytes, with a TCHAR array.
//
#define NUM_OF(ary_) (sizeof(ary_)/sizeof(ary_[0]))

//-----------------------------------------------------------------------------
// check_hr
//
// sanity check on all calls specific to this sample
//
inline HRESULT
check_hr(LPCTSTR file, unsigned line, HRESULT hr)
{
    if (FAILED(hr))
    {
        using namespace std;
        tostringstream buff;
        TCHAR error_text[256] = { 0 };
        ::D3DXGetErrorString(hr, error_text, NUM_OF(error_text));
        buff << file << _T(":") << line << _T(" 0x") << hex << setw(8)
            << hr << _T(" = ") << error_text << endl;
        ::OutputDebugString(buff.str().c_str());

        // Put a breakpoint on this ATLASSERT and you'll hit your
        // breakpoint every time your program encounters an unexpected
        // failed HRESULT.
        ATLASSERT(false);
    }
    return hr;
}

//-----------------------------------------------------------------------------
// THR
//
// this is similar to the THR() in my RT utility library, only this
// flavor doesn't throw() an exception, it prints to the debug stream
// and ATLASSERT()s false.
//
#define THR(hr_) check_hr(_T(__FILE__), __LINE__, hr_)

//-----------------------------------------------------------------------------
// s_enum_value<Enum>
// s_rs
// s_tss
//
// struct for storing a render state and its value
//
template <typename Enum>
struct s_enum_value
{
    Enum m_state;
    DWORD m_value;
};
typedef s_enum_value<D3DRENDERSTATETYPE> s_rs;
typedef s_enum_value<D3DTEXTURESTAGESTATETYPE> s_tss;

//-----------------------------------------------------------------------------
// set_states
//
// Calls SetRenderState() on an array of state/value pairs.
//
inline void
set_states(IDirect3DDevice8 *device, const s_rs *states, UINT num_states)
{
    for (UINT i = 0; i < num_states; i++)
    {
        THR(device->SetRenderState(states[i].m_state, states[i].m_value));
    }
}

//-----------------------------------------------------------------------------
// check_menu
//
// Makes a menu item's check mark reflect the value of a bool.
//
inline void
check_menu(HMENU menu, UINT item, bool value)
{
    ::CheckMenuItem(menu, item, value ? MF_CHECKED : MF_UNCHECKED);
}

//-----------------------------------------------------------------------------
// CMyD3DApplication::text
//

//-----------------------------------------------------------------------------
// ramp_val
//
// The gamma function with inputs in [0,255], scaled to a range with the
// default range appropriate for D3DGAMMARAMP.
//
inline int
ramp_val(UINT i, float recip_gamma, int scale = 65535)
{
    return static_cast<int>(scale*pow(i/255.f, recip_gamma));
}

//-----------------------------------------------------------------------------
// recip_gamma
//
// Given a gamma corrected i in [0,255], return 1/gamma
//
inline float
recip_gamma(UINT i)
{
    return logf(i/255.f)/logf(0.5f);
}

//-----------------------------------------------------------------------------
// gamma
//
// Given a gamma corrected color channel value in [0,255], return the gamma.
//
inline float
gamma(UINT i)
{
    return logf(0.5f)/logf(i/255.f);
}

//-----------------------------------------------------------------------------
// Name: class CMyD3DApplication
// Desc: Application class. The base class (CD3DApplication) provides the 
//       generic functionality needed in all Direct3D samples. CMyD3DApplication 
//       adds functionality specific to this sample program.
//-----------------------------------------------------------------------------
class CMyD3DApplication : public CD3DApplication
{
public:
    CMyD3DApplication();
    ~CMyD3DApplication() {}

    LRESULT MsgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

protected:
    HRESULT OneTimeSceneInit();
    HRESULT InitDeviceObjects();
    HRESULT RestoreDeviceObjects();
    HRESULT InvalidateDeviceObjects();
    HRESULT DeleteDeviceObjects();
    HRESULT Render();
    HRESULT FrameMove();
    HRESULT FinalCleanup();

private:
	bool m_show_help;
    enum e_edit_gamma
    {
        G_GRAY,
        G_RED,
        G_GREEN,
        G_BLUE
    };

    bool m_single_gamma;
    bool m_can_gamma_ramp;
    UINT m_left, m_center, m_right;
    UINT m_top, m_bottom;
    CD3DFont *m_font;
    bool m_test_gamma;
    e_edit_gamma m_which_gamma;
    D3DCOLOR m_text_fg;

    int m_gamma[4];
    D3DGAMMARAMP m_ramp;

    // redundant label to force class wizard to insert here
private:
    //-------------------------------------------------------------------------
    // text
    //
    // The DrawText method on CD3DFont stupidly takes a pointer to non-const
    // string for an argument that it treats as a const string.  This means
    // that std::basic_string<TCHAR>::c_str(), or any other function which
    // returns LPCTSTR, can't be used as an argument to DrawText without
    // casting away const.  These two helper functions cast away const and
    // extract the underlying C-style string from a tostringstream as a
    // convenience.
    //
    inline void
    text(float x, float y, LPCTSTR str)
    {
        THR(m_font->DrawText(x, y, gamma_color(m_text_fg),
                             const_cast<LPTSTR>(str)));
    }
    inline void
    text(float x, float y, const tostringstream &str)
    {
        text(x, y, str.str().c_str());
    }

    //-------------------------------------------------------------------------
    // gamma_color
    //
    // Inline helper to break out components of a D3DCOLOR and return a
    // color that is gamma corrected as needed.  Used to keep the background
    // and text colors the same intensity regardless of whether or not
    // the gamma ramp is used.
    // 
    D3DCOLOR gamma_color(D3DCOLOR rgb)
    {
        return gamma_color((rgb >> 16) & 0xFF,
                           (rgb >> 8)  & 0xFF,
                            rgb        & 0xFF);
    }

    void inc_gamma(int delta);
	LRESULT on_key_down(HWND window, WPARAM wp, LPARAM lp, bool &handled);
	LRESULT on_command(HWND window, WPARAM wp, LPARAM lp, bool &handled);
	void update_ramp();
	void render_gamma_test();
	void render_measure_gamma();
	D3DCOLOR test_colors(int percent);
    void set_ramp();
	D3DCOLOR gamma_color(BYTE red, BYTE green, BYTE blue);
};

//-----------------------------------------------------------------------------
// Name: WinMain()
// Desc: Entry point to the program. Initializes everything, and goes into a
//       message-processing loop. Idle time is used to render the scene.
//-----------------------------------------------------------------------------
INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, INT)
{
    CMyD3DApplication d3dApp;

    if (FAILED(d3dApp.Create(hInst)))
        return 0;

    return d3dApp.Run();
}

//-----------------------------------------------------------------------------
// Name: CMyD3DApplication()
// Desc: Application constructor. Sets attributes for the app.
//-----------------------------------------------------------------------------
CMyD3DApplication::CMyD3DApplication()
    : m_show_help(false),
    m_single_gamma(true),
    m_can_gamma_ramp(false),
    m_left(0), m_right(0), m_top(0), m_bottom(0),
    m_font(new CD3DFont(_T("Arial"), 12, D3DFONT_BOLD)),
    m_test_gamma(false),
    m_which_gamma(G_GRAY),
    m_text_fg(D3DCOLOR_XRGB(202, 202, 0)) // gamma=1.0
{
    m_strWindowTitle = _T("rt_Gamma");
    m_bUseDepthBuffer = FALSE;

    m_gamma[G_GRAY] = m_gamma[G_RED] = m_gamma[G_GREEN] = m_gamma[G_BLUE]
        = INITIAL_GAMMA;

    update_ramp();
}

//-----------------------------------------------------------------------------
// Name: OneTimeSceneInit()
// Desc: Called during initial app startup, this function performs all the
//       permanent initialization.
//-----------------------------------------------------------------------------
HRESULT CMyD3DApplication::OneTimeSceneInit()
{
    return S_OK;
}

//-----------------------------------------------------------------------------
// Name: FrameMove()
// Desc: Called once per frame, the call is the entry point for animating
//       the scene.
//-----------------------------------------------------------------------------
HRESULT CMyD3DApplication::FrameMove()
{
    return S_OK;
}

//-----------------------------------------------------------------------------
// Name: Render()
// Desc: Called once per frame, the call is the entry point for 3d
//       rendering. This function sets up render states, clears the
//       viewport, and renders the scene.
//-----------------------------------------------------------------------------
HRESULT CMyD3DApplication::Render()
{
    // clear the back buffer to dark slate gray.  (I got this color name
    // and value from the rgb.txt file in the X11 Window System distribution
    // <ftp://ftp.x.org/pub/R6.5.1/xc/programs/rgb/rgb.txt>.  The colors in
    // that file should all be assumed to have a gamma of 2.22.)
    //
    // Gamma correct the background color as needed.
    const D3DCOLOR dark_slate_gray = gamma_color(6, 19, 19);
    m_pd3dDevice->Clear(0L, NULL, D3DCLEAR_TARGET, dark_slate_gray, 1.0f, 0L);
    m_pd3dDevice->BeginScene();

    // initial device state
    s_rs states[] =
    {
        D3DRS_COLORVERTEX, TRUE,
        D3DRS_LIGHTING, FALSE,
        D3DRS_ALPHATESTENABLE, FALSE,
        D3DRS_ZENABLE, D3DZB_FALSE,
        D3DRS_STENCILENABLE, FALSE,
        D3DRS_ALPHABLENDENABLE, FALSE,
        D3DRS_SHADEMODE, D3DSHADE_GOURAUD,
        D3DRS_FILLMODE, D3DFILL_SOLID,
        D3DRS_DITHERENABLE, TRUE,
        D3DRS_LASTPIXEL, TRUE
    };
    set_states(m_pd3dDevice, states, NUM_OF(states));

    // either show help or display gamma correction graphics
    if (m_show_help)
    {
        text(20, 20,
            _T("Keyboard commands:\n")
            _T("F1\n")
            _T("T\n")
            _T("1\n")
            _T("Y\n")
            _T("R,G,B\n"));
        text(70, 20,
            _T("\n")
            _T("Toggle help display\n")
            _T("Test or measurement display toggle\n")
            _T("Grayscale or RGB gamma toggle\n")
            _T("Measure grayscale gamma\n")
            _T("Measure red, green, blue gamma\n"));

        text(20, m_d3dsdBackBuffer.Height - 100,
                  _T("See rt_Gamma.cpp for details."));
    }
    else
    {
        // build up a status string
        tostringstream buff;
        using namespace std;
        if (m_can_gamma_ramp)
        {
            buff << _T("SetGammaRamp support.  ");
        }
        buff << _T("Press F1 for help.\n");
        // display gamma value
        if (m_single_gamma)
        {
            buff << _T("Single Gamma = ") << fixed << setprecision(4)
                << setw(6) << gamma(m_gamma[G_GRAY])
                << _T(" (") << m_gamma[G_GRAY] << _T(")");
        }
        else
        {
            buff << _T("RGB Gamma = <") << fixed << setprecision(2)
                << setw(4) << gamma(m_gamma[G_RED])
                << _T(",") << gamma(m_gamma[G_GREEN])
                << _T(",") << gamma(m_gamma[G_BLUE])
                << _T(">  (") << m_gamma[G_RED]
                << _T(",") << m_gamma[G_GREEN]
                << _T(",") << m_gamma[G_BLUE]
                << _T(")");
        }
        buff << std::ends;
        text(2, m_d3dsdBackBuffer.Height - 40, buff);

        // display either the test or measurement graphics
        if (m_test_gamma)
        {
            render_gamma_test();
        }
        else
        {
            render_measure_gamma();
        }
    }

    m_pd3dDevice->EndScene();

    return S_OK;
}

//-----------------------------------------------------------------------------
// Name: InitDeviceObjects()
// Desc: Initialize scene objects.
//-----------------------------------------------------------------------------
HRESULT CMyD3DApplication::InitDeviceObjects()
{
    // Restore the fonts
    m_font->InitDeviceObjects(m_pd3dDevice);
    return S_OK;
}

//-----------------------------------------------------------------------------
// Name: RestoreDeviceObjects()
// Desc: Initialize scene objects.
//-----------------------------------------------------------------------------
HRESULT CMyD3DApplication::RestoreDeviceObjects()
{
    // Restore the fonts
    m_font->RestoreDeviceObjects();

    // compute some layout geometry:
    // the alternating lines and solid area are maximally drawn within
    // an inner rectangle of the client area.  The inner rectangle is
    // inside the client area with a uniform margin on all sides.  Try
    // to keep the margin until the window becomes so small that the
    // margin is larger than 20% of the window dimension, then yield
    // margin space to the gamma correction graphics.  There is no
    // margin between the polyline and solid areas, they should abut
    // with no gap.
    //
    const UINT margin = 50;
    m_top = margin;
    m_bottom = m_d3dsdBackBuffer.Height - margin;
    if (m_d3dsdBackBuffer.Height < 3*margin)
    {
        m_top = 0;
        m_bottom = m_d3dsdBackBuffer.Height;
    }
    else if (m_d3dsdBackBuffer.Height < 5*margin)
    {
        m_top = (m_d3dsdBackBuffer.Height - 3*margin)/2;
        m_bottom = m_d3dsdBackBuffer.Height - m_top;
    }
    // this makes sure there is always an odd number of lines
    if (0 == ((m_bottom - m_top) % 2))
    {
        --m_bottom;
    }

    m_center = m_d3dsdBackBuffer.Width/2;
    m_left = margin;
    m_right = m_d3dsdBackBuffer.Width - margin;
    if (m_d3dsdBackBuffer.Width < 3*margin)
    {
        m_left = 0;
        m_right = m_d3dsdBackBuffer.Width;
    }
    else if (m_d3dsdBackBuffer.Width < 5*margin)
    {
        m_left = m_center - 3*margin/2;
        m_right = m_center + 3*margin/2;
    }

    // can we gamma ramp in full screen?
    m_can_gamma_ramp =
        (m_d3dCaps.Caps2 & D3DCAPS2_FULLSCREENGAMMA) ? true : false;
    update_ramp();
    set_ramp();

    return S_OK;
}

//-----------------------------------------------------------------------------
// Name: InvalidateDeviceObjects()
// Desc: Called when the app is exiting, or the device is being changed,
//       this function deletes any device dependent objects.
//-----------------------------------------------------------------------------
HRESULT CMyD3DApplication::InvalidateDeviceObjects()
{
    m_font->InvalidateDeviceObjects();
    return S_OK;
}

//-----------------------------------------------------------------------------
// Name: DeleteDeviceObjects()
// Desc: Called when the app is exiting, or the device is being changed,
//       this function deletes any device dependent objects.
//-----------------------------------------------------------------------------
HRESULT CMyD3DApplication::DeleteDeviceObjects()
{
    m_font->DeleteDeviceObjects();
    return S_OK;
}

//-----------------------------------------------------------------------------
// Name: FinalCleanup()
// Desc: Called before the app exits, this function gives the app the chance
//       to cleanup after itself.
//-----------------------------------------------------------------------------
HRESULT CMyD3DApplication::FinalCleanup()
{
    SAFE_DELETE(m_font);
    return S_OK;
}

//-----------------------------------------------------------------------------
// Name: MsgProc()
// Desc: Message proc function to handle key and menu input
//-----------------------------------------------------------------------------
LRESULT
CMyD3DApplication::MsgProc(HWND window, UINT msg, WPARAM wp, LPARAM lp)
{
    bool handled = false;
    LRESULT result = 0;

#define HANDLE_MSG(msg_, fn_) \
    case msg_: result = fn_(window, wp, lp, handled); break

    switch (msg)
    {
    HANDLE_MSG(WM_COMMAND, on_command);
    HANDLE_MSG(WM_KEYDOWN, on_key_down);
    }
#undef HANDLE_MSG

    return handled ? result : CD3DApplication::MsgProc(window, msg, wp, lp);
}

//-----------------------------------------------------------------------------
// CMyD3DApplication::on_command
//
// WM_COMMAND handler.  Switch the currently selected gamma ramp being
// computed.
//
LRESULT
CMyD3DApplication::on_command(HWND window, WPARAM wp, LPARAM lp, bool &handled)
{
    const UINT control = LOWORD(wp);
    const HMENU menu = ::GetMenu(window);
    switch (control)
    {
    // menu IDs must be contiguous and increasing in e_edit_gamma order
    case IDM_GRAY_RAMP:
    case IDM_RED_RAMP:
    case IDM_GREEN_RAMP:
    case IDM_BLUE_RAMP:
        check_menu(menu, m_which_gamma + IDM_GRAY_RAMP, false);
        m_which_gamma = e_edit_gamma(control - IDM_GRAY_RAMP);
        check_menu(menu, control, true);
        handled = true;
        break;

    case IDM_TEST_GAMMA:
        m_test_gamma = !m_test_gamma;
        check_menu(menu, IDM_TEST_GAMMA, m_test_gamma);
        handled = true;

        // set gamma ramp if necessary
        update_ramp();
        set_ramp();
        break;

    case IDM_SINGLE_GAMMA:
        m_single_gamma = !m_single_gamma;
        check_menu(menu, IDM_SINGLE_GAMMA, m_single_gamma);
        handled = true;

        // compute new ramp and set on device
        update_ramp();
        set_ramp();
        break;

    case IDM_SHOW_HELP:
        m_show_help = !m_show_help;
        check_menu(menu, IDM_SHOW_HELP, m_show_help);
        handled = true;
        break;
    }
    return 0;
}

//-----------------------------------------------------------------------------
// CMyD3DApplication::on_key_down
//
// WM_KEYDOWN handler.  Adjust the gamma up, down, to the minimum, or to
// the maximum.
//
LRESULT
CMyD3DApplication::on_key_down(HWND window, WPARAM wp, LPARAM lp, bool &handled)
{
    if (!m_test_gamma)
    {
        switch (wp)
        {
        // adjust segments up or down and rebuild mesh
        case VK_UP:     inc_gamma(+GAMMA_LINE_SIZE);  handled = true; break;
        case VK_DOWN:   inc_gamma(-GAMMA_LINE_SIZE);  handled = true; break;
        case VK_PRIOR:  inc_gamma(+GAMMA_PAGE_SIZE);  handled = true; break;
        case VK_NEXT:   inc_gamma(-GAMMA_PAGE_SIZE);  handled = true; break;

        // set segments to specific value and rebuild mesh
        case VK_HOME:
            m_gamma[m_which_gamma] = HOME_GAMMA;
            inc_gamma(0);
            handled = true;
            break;

        case VK_END:
            m_gamma[m_which_gamma] = END_GAMMA;
            inc_gamma(0);
            handled = true;
            break;
        }
    }
    return 0;
}


//-----------------------------------------------------------------------------
// CMyD3DApplication::inc_gamma
//
// Increase the currently selected gamma value, clamping between HOME and
// END values.
//
void 
CMyD3DApplication::inc_gamma(int delta)
{
    m_gamma[m_which_gamma] += delta;
    m_gamma[m_which_gamma] =
        min(END_GAMMA, max(m_gamma[m_which_gamma], HOME_GAMMA));

    update_ramp();
}

//-----------------------------------------------------------------------------
// CMyD3DApplication::set_ramp
//
// Updates the current gamma ramp into m_ramp based on the currently
// selected gamma type.
//
void
CMyD3DApplication::update_ramp()
{
    if (m_test_gamma && m_can_gamma_ramp)
    {
        if (m_single_gamma)
        {
            // compute 1/gamma
            const float recip_gray = recip_gamma(m_gamma[G_GRAY]);
            // compute i**(1/gamma) for all i and scale to range
            for (UINT i = 0; i < 256; i++)
            {
                m_ramp.red[i] = m_ramp.green[i] = m_ramp.blue[i] =
                    ramp_val(i, recip_gray);
            }
        }
        else
        {
            const float recip_red = recip_gamma(m_gamma[G_RED]);
            const float recip_green = recip_gamma(m_gamma[G_GREEN]);
            const float recip_blue = recip_gamma(m_gamma[G_BLUE]);
            for (UINT i = 0; i < 256; i++)
            {
                m_ramp.red[i] = ramp_val(i, recip_red);
                m_ramp.green[i] = ramp_val(i, recip_green);
                m_ramp.blue[i] = ramp_val(i, recip_blue);
            }
        }
    }
    else
    {
        for (UINT i = 0; i < 256; i++)
        {
            m_ramp.red[i] = m_ramp.green[i] = m_ramp.blue[i] = 65535*i/255;
        }
    }
}

//-----------------------------------------------------------------------------
// CMyD3DApplication::render_measure_gamma
//
// Render the graphics used to measure the gamma of the monitor.
//
void
CMyD3DApplication::render_measure_gamma()
{
    // Show instructions
    text(2,  0,
        _T("Adjust the left and right sides to the same intensity.\n")
        _T("Controls: Up/Down, Page Up/Down, Home, End"));

    // determine foreground colors for gamma graphics
    const BYTE corrected = m_gamma[m_which_gamma];
    D3DCOLOR solid_fg, line_fg;
    switch (m_which_gamma)
    {
    case G_GRAY:
        solid_fg = D3DCOLOR_XRGB(corrected, corrected, corrected);
        line_fg = D3DCOLOR_XRGB(255, 255, 255);
        break;

    case G_RED:
        line_fg = D3DCOLOR_XRGB(255, 0, 0);
        solid_fg = D3DCOLOR_XRGB(corrected, 0, 0);
        break;

    case G_GREEN:
        line_fg = D3DCOLOR_XRGB(0, 255, 0);
        solid_fg = D3DCOLOR_XRGB(0, corrected, 0);
        break;

    case G_BLUE:
        line_fg = D3DCOLOR_XRGB(0, 0, 255);
        solid_fg = D3DCOLOR_XRGB(0, 0, corrected);
        break;

    default:
        ATLASSERT(false);
        break;
    }
    
    // first the alternating black and white lines
    const UINT num = (m_bottom - m_top + 1)/2;
    std::vector<s_vertex> vertices(num*2);
    UINT i;
    for (i = 0; i < num; i++)
    {
        vertices[i*2+0].set(m_left,   m_top + i*2, line_fg);
        vertices[i*2+1].set(m_center, m_top + i*2, line_fg);
    }
    THR(m_pd3dDevice->SetVertexShader(VERTEX_FVF));
    THR(m_pd3dDevice->DrawPrimitiveUP(D3DPT_LINELIST, num, &vertices[0],
        sizeof(vertices[0])));
    for (i = 0; i < num-1; i++)
    {
        vertices[i*2+0].set(m_left,   m_top + i*2 + 1, 0L);
        vertices[i*2+1].set(m_center, m_top + i*2 + 1, 0L);
    }
    THR(m_pd3dDevice->DrawPrimitiveUP(D3DPT_LINELIST, num, &vertices[0],
        sizeof(vertices[0])));

    // now the adjacent solid area
    const float half_width = (m_right + m_center)/2;
    const float half_height = (m_top + m_bottom)/2;
    const s_vertex solid[6] =
    {
        half_width, half_height, 0.5f, 2, solid_fg, // triangle fan center
        m_center,   m_top,       0.5f, 2, solid_fg,
        m_right,    m_top,       0.5f, 2, solid_fg,
        m_right,    m_bottom,    0.5f, 2, solid_fg,
        m_center,   m_bottom,    0.5f, 2, solid_fg,
        m_center,   m_top,       0.5f, 2, solid_fg  // 4 corners
    };
    THR(m_pd3dDevice->SetVertexShader(VERTEX_FVF));
    THR(m_pd3dDevice->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, 4, &solid[0],
        sizeof(solid[0])));
}

//-----------------------------------------------------------------------------
// CMyD3DApplication::render_gamma_test
//
// Test the currently set gamma values by rendering a series of 5 vertical
// bars at 0%, 25%, 50%, 75% and 100% full intensity.  Underneath the bars
// is drawn an interpolated quad from 0% to 100%.  With proper gamma
// correction, they will appear equal intensity at the center of the
// constant intensity bars.
//
//          +--+-----+-----+-----+--+
//          |  |     |     |     |  |
//          |  |     |     |     |  |
//          | 0| 25  | 50  | 75  |100
//          |  |     |     |     |  |
//          +--+-----+-----+-----+--+
//          |                       |
//          |                       |
//          |        Gradient       |
//          |                       |
//          +-----------------------+
//
void
CMyD3DApplication::render_gamma_test()
{
    D3DCOLOR quarters[5] =          // 0%, 25%, 50%, 75%, 100%
    {
        D3DCOLOR_XRGB(0, 0, 0),
        test_colors(25),
        test_colors(50),
        test_colors(75),
        D3DCOLOR_XRGB(255, 255, 255)
    };
    const UINT left_quarter = (m_left + m_center)/2;
    const UINT right_quarter = (m_center + m_right)/2;
    const UINT one_eighth = (m_left + left_quarter)/2;
    const UINT three_eighths = (left_quarter + m_center)/2;
    const UINT five_eighths = (m_center + right_quarter)/2;
    const UINT seven_eighths = (right_quarter + m_right)/2;

    const UINT top = m_top > 20 ? m_top-20 : 2;
    const UINT left = m_left > 10 ? m_left-10 : 2;
    const UINT right = m_right > 20 ? m_right-20 : 2;
    text(left,             top, _T("0%"));
    text(left_quarter-20,  top, _T("25%"));
    text(m_center-20,      top, _T("50%"));
    text(right_quarter-20, top, _T("75%"));
    text(right,            top, _T("100%"));

    const UINT half_height = (m_top + m_bottom)/2;
#define VTX(x_, y_, clr_) { x_, y_, 0.5f, 2.0f, quarters[clr_] }
    const s_vertex stripes[] =
    {
        // 0
        VTX(m_left,         m_top, 0),
        VTX(one_eighth,     m_top, 0),
        VTX(one_eighth,     m_top, 1),
        VTX(three_eighths,  m_top, 1),
        VTX(three_eighths,  m_top, 2),
        VTX(five_eighths,   m_top, 2),
        VTX(five_eighths,   m_top, 3),
        VTX(seven_eighths,  m_top, 3),
        VTX(seven_eighths,  m_top, 4),
        VTX(m_right,        m_top, 4),
        // 10
        VTX(m_left,         half_height, 0),
        VTX(one_eighth,     half_height, 0),
        VTX(one_eighth,     half_height, 1),
        VTX(three_eighths,  half_height, 1),
        VTX(three_eighths,  half_height, 2),
        VTX(five_eighths,   half_height, 2),
        VTX(five_eighths,   half_height, 3),
        VTX(seven_eighths,  half_height, 3),
        VTX(seven_eighths,  half_height, 4),
        VTX(m_right,        half_height, 4),
        // 20
        VTX(m_left,         m_bottom, 0),
        VTX(m_right,        m_bottom, 4)
    };
#undef VTX
    const WORD indices[] =
    {
        // the bars
        0, 1, 10,       1, 11, 10,
        2, 3, 12,       3, 13, 12,
        4, 5, 14,       5, 15, 14,
        6, 7, 16,       7, 17, 16,
        8, 9, 18,       9, 19, 18,
        // the interpolated ramp
        10, 19, 20,     19, 21, 20
    };
    THR(m_pd3dDevice->SetVertexShader(VERTEX_FVF));
    THR(m_pd3dDevice->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST,
            0, 22, 12, indices,
            D3DFMT_INDEX16, stripes, sizeof(stripes[0])));
}

//-----------------------------------------------------------------------------
// CMyD3DApplication::test_colors
//
// Given an percentage of white in [0,100], returns the appropriate
// color to use to render test geometry at the current gamma value.
//
D3DCOLOR
CMyD3DApplication::test_colors(int percent)
{
    D3DCOLOR result = ~0L;

    // can't correct with SetGammaRamp, have to hand correct ourselves
    if (m_bWindowed || !m_can_gamma_ramp)
    {
        // do RGB with one gamma value
        if (G_GRAY == m_which_gamma)
        {
            const float recip_gamma = logf(m_gamma[G_GRAY]/255.f)/logf(0.5f);
            const BYTE corrected = ramp_val(percent*255/100, recip_gamma, 255);
            result = D3DCOLOR_XRGB(corrected, corrected, corrected);
        }
        // do RGB with one gamma each for red, green, blue
        else
        {
            const float recip_red = logf(m_gamma[G_RED]/255.f)/logf(0.5f);
            const float recip_green = logf(m_gamma[G_GREEN]/255.f)/logf(0.5f);
            const float recip_blue = logf(m_gamma[G_BLUE]/255.f)/logf(0.5f);
            const BYTE red = ramp_val(percent*255/100, recip_red, 255);
            const BYTE green = ramp_val(percent*255/100, recip_green, 255);
            const BYTE blue = ramp_val(percent*255/100, recip_blue, 255);
            result = D3DCOLOR_XRGB(red, green, blue);
        }
    }
    // SetGammaRamp gamma corrects, give back RGB linear colors
    else
    {
        const BYTE linear = percent*255/100;
        result = D3DCOLOR_XRGB(linear, linear, linear);
    }

    return result;
}

//-----------------------------------------------------------------------------
// CMyD3DApplication::set_ramp
//
// Set the gamma ramp on the device.  This is called when the device is
// being initialized and when we're toggling the test/measure mode.
//
void
CMyD3DApplication::set_ramp()
{
    if (!m_bWindowed && m_can_gamma_ramp)
    {
        m_pd3dDevice->SetGammaRamp(D3DSGR_NO_CALIBRATION, &m_ramp);
    }
}

//-----------------------------------------------------------------------------
// CMyD3DApplication::gamma_color
//
// Used for drawing graphic elements that are not part of the testing or
// measurement.  The text and background colors are in this category.
// Takes an RGB color with an assumed gamma of 1.0 and returns a color
// suitable for use in the program's current mode.  Unless we are displaying
// test graphics in full screen with gamma ramp support, we'll have to gamma
// correct the color.
//
D3DCOLOR
CMyD3DApplication::gamma_color(BYTE red, BYTE green, BYTE blue)
{
    D3DCOLOR result = D3DCOLOR_XRGB(red, green, blue);

    if (m_bWindowed || !m_can_gamma_ramp || !m_test_gamma)
    {
        // device doesn't gamma correct, so we have to
        if (m_single_gamma)
        {
            const float recip_gray = recip_gamma(m_gamma[G_GRAY]);
            result = D3DCOLOR_XRGB(ramp_val(red, recip_gray, 255),
                                   ramp_val(green, recip_gray, 255),
                                   ramp_val(blue, recip_gray, 255));
        }
        else
        {
            const float recip_red = recip_gamma(m_gamma[G_RED]);
            const float recip_green = recip_gamma(m_gamma[G_GREEN]);
            const float recip_blue = recip_gamma(m_gamma[G_BLUE]);
            result = D3DCOLOR_XRGB(ramp_val(red, recip_red, 255),
                                   ramp_val(green, recip_green, 255),
                                   ramp_val(blue, recip_blue, 255));
        }
    }

    return result;
}
