Files
spice2x.github.io/src/spice2x/games/iidx/local_camera.cpp
T
Will e452734bc1 Build a WinXP compatible version of Spice (#611)
This was a lot more background work than expected, but that is the
nature of unnecessary hobbies I guess. Most things in here are obvious,
so here's the non-obvious thoughts and notes:

1. All the XP stuff is optional, if the toolchain isn't available it
won't even try
2. The docker build is the "known good" build and now has all the
required tools for XP and checking binaries
a. If you're OK with it, can add the XP_MUST_BUILD flag to
build_docker.sh
4. New `SPICE_XP` flag to comment out blocks
5. We could technically build using the Docker image I've made for my XP
compatible LLVM toolchain, but combining the binaries into a single zip
at the end of it is somewhat tedious, so I've chosen to grab the
binaries from my release
6. I also wrote a DLL checker to make sure the final binaries are
_actually_ XP compatible, which found some issues on x86_64 (I don't
know a single game that runs on 64 bit XP, but it wasn't too much effort
so whatever). This also won't run if missing, just to save the trouble.
7. The build script is still pretty shit, I've made some minor
improvements
  a. `/usr/bin/env bash` instead of `/bin/bash` as it's more correct
  b. Adding `-it` to the docker build, so Ctrl+C actually halts it
c. Main issue is that failures don't halt, because of the custom error
catching function. This has caused a lot of frustration, maybe we can
change that

## Testing
It runs on 32 and 64 bit installs of XP. My jubeat cab is on Win10 these
days and I haven't got around to testing an actual game, but I've
distributed test builds of this to people who *have* confirmed it works
as expected.
2026-04-06 01:14:29 -07:00

901 lines
31 KiB
C++

#include "games/iidx/local_camera.h"
#if SPICE64 && !SPICE_XP
#include "util/logging.h"
#include "util/utils.h"
#include "mf_wrappers.h"
std::string CAMERA_CONTROL_LABELS[] = {
"Pan",
"Tilt",
"Roll",
"Zoom",
"Exposure",
"Iris",
"Focus"
};
std::string DRAW_MODE_LABELS[] = {
"Stretch",
"Crop",
"Letterbox",
"Crop to 4:3",
"Letterbox to 4:3",
};
// static HRESULT printTextureLevelDesc(LPDIRECT3DTEXTURE9 texture) {
// HRESULT hr = S_OK;
// D3DSURFACE_DESC desc;
// hr = texture->GetLevelDesc(0, &desc);
// log_info("iidx:camhook", "Texture Desc Size: {}x{} Res Type: {} Format: {} Usage: {}", desc.Width, desc.Height, (int) desc.Type, (int) desc.Format, (int) desc.Usage);
// return hr;
// }
LONG TARGET_SURFACE_WIDTH = 1280;
LONG TARGET_SURFACE_HEIGHT = 720;
double RATIO_16_9 = 16.0 / 9.0;
double RATIO_4_3 = 4.0 / 3.0;
namespace games::iidx {
IIDXLocalCamera::IIDXLocalCamera(
std::string name,
BOOL prefer_16_by_9,
IMFActivate *pActivate,
IDirect3DDeviceManager9 *pD3DManager,
LPDIRECT3DDEVICE9EX device,
LPDIRECT3DTEXTURE9 *camera_texture_target,
LPDIRECT3DTEXTURE9 *preview_texture_target
):
m_nRefCount(1),
m_name(name),
m_prefer_16_by_9(prefer_16_by_9),
m_device(device),
m_camera_texture_target(camera_texture_target),
m_preview_texture_target(preview_texture_target),
m_camera_texture_original(*camera_texture_target),
m_preview_texture_original(*preview_texture_target)
{
InitializeCriticalSection(&m_critsec);
HRESULT hr = S_OK;
IMFAttributes *pAttributes = nullptr;
EnterCriticalSection(&m_critsec);
log_info("iidx:camhook", "[{}] Creating camera", m_name);
// Get friendly name for log purposes
WCHAR *pwszFriendlyName = nullptr;
UINT32 m_cchFriendlyName = 0;
hr = pActivate->GetAllocatedString(
MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME,
&pwszFriendlyName,
&m_cchFriendlyName
);
if (SUCCEEDED(hr) && pwszFriendlyName != nullptr) {
log_misc("iidx:camhook", "[{}] Name: {}", m_name, ws2s(pwszFriendlyName));
m_friendly_name = ws2s(pwszFriendlyName);
CoTaskMemFree(pwszFriendlyName);
pwszFriendlyName = nullptr;
}
// Retrive symlink of Camera for control configurations
hr = pActivate->GetAllocatedString(
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK,
&m_pwszSymbolicLink,
&m_cchSymbolicLink
);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] GetAllocatedString failed with {:#x}", m_name, (ULONG)hr);
goto done;
}
log_misc("iidx:camhook", "[{}] Symlink: {}", m_name, GetSymLink());
// Create the media source object.
hr = pActivate->ActivateObject(IID_PPV_ARGS(&m_pSource));
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] ActivateObject failed with {:#x}", m_name, (ULONG)hr);
goto done;
}
// Retain reference to the camera
m_pSource->AddRef();
log_misc("iidx:camhook", "[{}] Activated", m_name);
// Create an attribute store to hold initialization settings.
hr = WrappedMFCreateAttributes(&pAttributes, 2);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] MFCreateAttributes failed with {:#x}", m_name, (ULONG)hr);
goto done;
}
hr = pAttributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, pD3DManager);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] SetUnknown(MF_SOURCE_READER_D3D_MANAGER) failed with {:#x}", m_name, (ULONG)hr);
goto done;
}
hr = pAttributes->SetUINT32(MF_SOURCE_READER_DISABLE_DXVA, FALSE);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] SetUINT32(MF_SOURCE_READER_DISABLE_DXVA) failed with {:#x}", m_name, (ULONG)hr);
goto done;
}
// TODO: Color space conversion
// if (SUCCEEDED(hr)) {
// hr = pAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, TRUE);
// }
// if (SUCCEEDED(hr)) {
// hr = pAttributes->SetUINT32(MF_READWRITE_DISABLE_CONVERTERS, FALSE);
// }
// Create the source reader.
hr = WrappedMFCreateSourceReaderFromMediaSource(
m_pSource,
pAttributes,
&m_pSourceReader
);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] MFCreateSourceReaderFromMediaSource failed with {:#x}", m_name, (ULONG)hr);
goto done;
}
log_misc("iidx:camhook", "[{}] Created source reader", m_name);
hr = InitTargetTexture();
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] InitTargetTexture failed with {:#x}", m_name, (ULONG)hr);
goto done;
}
// Camera should be still usable even if camera control is not supported
InitCameraControl();
done:
if (SUCCEEDED(hr)) {
m_initialized = true;
log_misc("iidx:camhook", "[{}] Initialized", m_name);
} else {
log_warning("iidx:camhook", "[{}] Failed to create camera: {:#x}", m_name, (ULONG)hr);
}
SafeRelease(&pAttributes);
LeaveCriticalSection(&m_critsec);
}
HRESULT IIDXLocalCamera::StartCapture() {
HRESULT hr = S_OK;
IMFMediaType *pType = nullptr;
if (!m_initialized) {
log_warning("iidx:camhook", "[{}] Camera not initialized", m_name);
return E_FAIL;
}
// Try to find a suitable output type.
log_misc("iidx:camhook", "[{}] Find best media type", m_name);
UINT32 bestWidth = 0;
double bestFrameRate = 0;
// The loop should terminate by MF_E_NO_MORE_TYPES
// Adding a hard limit just in case
for (DWORD i = 0; i < 1000; i++) {
hr = m_pSourceReader->GetNativeMediaType(
(DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
i,
&pType
);
if (FAILED(hr)) {
if (hr != MF_E_NO_MORE_TYPES) {
log_warning("iidx:camhook", "[{}] Cannot get media type {} {:#x}", m_name, i, (ULONG)hr);
}
break;
}
hr = TryMediaType(pType, &bestWidth, &bestFrameRate);
if (SUCCEEDED(hr)) {
MediaTypeInfo info = GetMediaTypeInfo(pType);
m_mediaTypeInfos.push_back(info);
if (hr == S_OK) {
m_pAutoMediaType = pType;
}
} else {
// Invalid media type (e.g. no conversion function)
SafeRelease(&pType);
}
}
// Sort available media types
std::sort(m_mediaTypeInfos.begin(), m_mediaTypeInfos.end(), [](const MediaTypeInfo &a, const MediaTypeInfo &b) {
if (a.width != b.width) {
return a.width > b.width;
}
if (a.height != b.height) {
return a.height > b.height;
}
if (a.frameRate != b.frameRate) {
return (int)a.frameRate > (int)b.frameRate;
}
return a.subtype.Data1 > b.subtype.Data1;
});
if (!m_pAutoMediaType) {
m_pAutoMediaType = m_mediaTypeInfos.front().p_mediaType;
}
IMFMediaType *pSelectedMediaType = nullptr;
// Find media type specified by user configurations
if (!m_useAutoMediaType && m_selectedMediaTypeDescription.length() > 0) {
log_info("iidx:camhook", "[{}] Use media type from config {}", m_name, m_selectedMediaTypeDescription);
auto it = std::find_if(m_mediaTypeInfos.begin(), m_mediaTypeInfos.end(), [this](const MediaTypeInfo &item){
return item.description.compare(this->m_selectedMediaTypeDescription) == 0;
});
if (it != m_mediaTypeInfos.end()) {
pSelectedMediaType = (*it).p_mediaType;
}
}
hr = S_OK;
if (!pSelectedMediaType) {
pSelectedMediaType = m_pAutoMediaType;
}
if (SUCCEEDED(hr)) {
hr = ChangeMediaType(pSelectedMediaType);
}
if (SUCCEEDED(hr)) {
log_info("iidx:camhook", "[{}] Creating thread", m_name);
CreateThread();
}
return hr;
}
HRESULT IIDXLocalCamera::ChangeMediaType(IMFMediaType *pType) {
HRESULT hr = S_OK;
MediaTypeInfo info = GetMediaTypeInfo(pType);
log_info("iidx:camhook", "[{}] Changing media type: {}", m_name, info.description);
auto it = std::find_if(m_mediaTypeInfos.begin(), m_mediaTypeInfos.end(), [pType](const MediaTypeInfo &item) {
return item.p_mediaType == pType;
});
m_selectedMediaTypeIndex = it - m_mediaTypeInfos.begin();
m_selectedMediaTypeDescription = info.description;
if (SUCCEEDED(hr)) {
hr = m_pSourceReader->SetCurrentMediaType(
(DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
NULL,
pType
);
}
if (SUCCEEDED(hr)) {
m_cameraWidth = info.width;
m_cameraHeight = info.height;
UpdateDrawRect();
}
return hr;
}
void IIDXLocalCamera::UpdateDrawRect() {
double cameraRatio = (double)m_cameraWidth / m_cameraHeight;
RECT cameraRect = {0, 0, m_cameraWidth, m_cameraHeight};
RECT targetRect = {0, 0, TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT};
switch (m_drawMode) {
case DrawModeStretch: {
CopyRect(&m_rcSource, &cameraRect);
CopyRect(&m_rcDest, &targetRect);
break;
}
case DrawModeCrop: {
if (cameraRatio > RATIO_16_9) {
// take full source height, crop left/right
LONG croppedWidth = m_cameraHeight * RATIO_16_9;
m_rcSource.left = (LONG)(m_cameraWidth - croppedWidth) / 2;
m_rcSource.top = 0;
m_rcSource.right = m_rcSource.left + croppedWidth;
m_rcSource.bottom = m_cameraHeight;
} else {
// take full source width, crop top/bottom
LONG croppedHeight = m_cameraWidth / RATIO_16_9;
m_rcSource.left = 0;
m_rcSource.top = (LONG)(m_cameraHeight - croppedHeight) / 2;
m_rcSource.right = m_cameraWidth;
m_rcSource.bottom = m_rcSource.top + croppedHeight;
}
CopyRect(&m_rcDest, &targetRect);
break;
}
case DrawModeLetterbox: {
CopyRect(&m_rcSource, &cameraRect);
if (cameraRatio > RATIO_16_9) {
// take full dest width, empty top/bottom
LONG boxedHeight = TARGET_SURFACE_WIDTH / cameraRatio;
m_rcDest.left = 0;
m_rcDest.top = (LONG)(TARGET_SURFACE_HEIGHT - boxedHeight) / 2;
m_rcDest.right = TARGET_SURFACE_WIDTH;
m_rcDest.bottom = m_rcDest.top + boxedHeight;
} else {
// take full dest height, empty top/bottom
LONG boxedWidth = TARGET_SURFACE_HEIGHT * cameraRatio;
m_rcDest.left = (LONG)(TARGET_SURFACE_WIDTH - boxedWidth) / 2;
m_rcDest.top = 0;
m_rcDest.right = m_rcDest.left + boxedWidth;
m_rcDest.bottom = TARGET_SURFACE_HEIGHT;
}
break;
}
case DrawModeCrop4_3: {
if (cameraRatio > RATIO_4_3) {
// take full source height, crop left/right
LONG croppedWidth = m_cameraHeight * RATIO_4_3;
m_rcSource.left = (LONG)(m_cameraWidth - croppedWidth) / 2;
m_rcSource.top = 0;
m_rcSource.right = m_rcSource.left + croppedWidth;
m_rcSource.bottom = m_cameraHeight;
} else {
// take full source width, crop top/bottom
LONG croppedHeight = m_cameraWidth / RATIO_4_3;
m_rcSource.left = 0;
m_rcSource.top = (LONG)(m_cameraHeight - croppedHeight) / 2;
m_rcSource.right = m_cameraWidth;
m_rcSource.bottom = m_rcSource.top + croppedHeight;
}
CopyRect(&m_rcDest, &targetRect);
break;
}
case DrawModeLetterbox4_3: {
CopyRect(&m_rcSource, &cameraRect);
if (cameraRatio > RATIO_4_3) {
// take full dest width, empty top/bottom
LONG boxedHeight = TARGET_SURFACE_HEIGHT / RATIO_4_3;
m_rcDest.left = 0;
m_rcDest.top = (LONG)(TARGET_SURFACE_HEIGHT - boxedHeight) / 2;
m_rcDest.right = TARGET_SURFACE_WIDTH;
m_rcDest.bottom = m_rcDest.top + boxedHeight;
} else {
// take full dest height, empty top/bottom
LONG boxedWidth = TARGET_SURFACE_WIDTH * RATIO_4_3;
m_rcDest.left = (LONG)(TARGET_SURFACE_WIDTH - boxedWidth) / 2;
m_rcDest.top = 0;
m_rcDest.right = m_rcDest.left + boxedWidth;
m_rcDest.bottom = TARGET_SURFACE_HEIGHT;
}
break;
}
}
// ensure the rects are valid
IntersectRect(&m_rcSource, &m_rcSource, &cameraRect);
IntersectRect(&m_rcDest, &m_rcDest, &targetRect);
log_info(
"iidx:camhook", "[{}] Update draw rect mode={} src=({}, {}, {}, {}) dest=({}, {}, {}, {})",
m_name,
DRAW_MODE_LABELS[m_drawMode],
m_rcSource.left,
m_rcSource.top,
m_rcSource.right,
m_rcSource.bottom,
m_rcDest.left,
m_rcDest.top,
m_rcDest.right,
m_rcDest.bottom
);
m_device->ColorFill(m_pDestSurf, &targetRect, D3DCOLOR_XRGB(0, 0, 0));
}
void IIDXLocalCamera::CreateThread() {
// Create thread
m_drawThread = new std::thread([this]() {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
double accumulator = 0.0;
while (this->m_active) {
this->Render();
double frameTimeMicroSec = (1000000.0 / this->m_frameRate);
int floorFrameTimeMicroSec = floor(frameTimeMicroSec);
// This maybe an overkill but who knows
accumulator += (frameTimeMicroSec - floorFrameTimeMicroSec);
if (accumulator > 1.0) {
accumulator -= 1.0;
floorFrameTimeMicroSec += 1;
}
std::this_thread::sleep_for(std::chrono::microseconds(floorFrameTimeMicroSec));
}
});
}
LPDIRECT3DTEXTURE9 IIDXLocalCamera::GetTexture() {
return m_texture;
}
IAMCameraControl* IIDXLocalCamera::GetCameraControl() {
return m_pCameraControl;
}
HRESULT IIDXLocalCamera::InitCameraControl() {
HRESULT hr = S_OK;
log_misc("iidx:camhook", "[{}] Init camera control", m_name);
hr = m_pSource->QueryInterface(IID_IAMCameraControl, (void**)&m_pCameraControl);
if (FAILED(hr)) {
// The device does not support IAMCameraControl
log_warning("iidx:camhook", "[{}] Camera control not supported", m_name);
return E_FAIL;
}
for (size_t i = 0; i < CAMERA_CONTROL_PROP_SIZE; i++) {
long minValue = 0;
long maxValue = 0;
long delta = 0;
long defaultValue = 0;
long defFlags = 0;
long value = 0;
long valueFlags = 0;
m_pCameraControl->GetRange(
i,
&minValue,
&maxValue,
&delta,
&defaultValue,
&defFlags
);
m_pCameraControl->Get(
i,
&value,
&valueFlags
);
m_controlProps.push_back({
minValue,
maxValue,
delta,
defaultValue,
defFlags,
value,
valueFlags,
});
CameraControlProp prop = m_controlProps.at(i);
log_misc(
"iidx:camhook", "[{}] >> {} range=({}, {}) default={} delta={} dFlags={} value={} vFlags={}",
m_name,
CAMERA_CONTROL_LABELS[i],
prop.minValue, prop.maxValue,
prop.defaultValue,
prop.delta,
prop.defFlags,
prop.value,
prop.valueFlags
);
}
m_controlOptionsInitialized = true;
return hr;
}
HRESULT IIDXLocalCamera::GetCameraControlProp(int index, CameraControlProp *pProp) {
if (!m_controlOptionsInitialized) {
return E_FAIL;
}
auto targetProp = m_controlProps.at(index);
pProp->minValue = targetProp.minValue;
pProp->maxValue = targetProp.maxValue;
pProp->defaultValue = targetProp.defaultValue;
pProp->delta = targetProp.delta;
pProp->defFlags = targetProp.defFlags;
pProp->value = targetProp.value;
pProp->valueFlags = targetProp.valueFlags;
return S_OK;
}
HRESULT IIDXLocalCamera::SetCameraControlProp(int index, long value, long flags) {
if (!m_controlOptionsInitialized || !m_allowManualControl) {
return E_FAIL;
}
if (index < 0 || index >= CAMERA_CONTROL_PROP_SIZE) {
return E_INVALIDARG;
}
auto targetProp = &(m_controlProps.at(index));
HRESULT hr = m_pCameraControl->Set(index, value, flags);
if (SUCCEEDED(hr)) {
m_pCameraControl->Get(
index,
&targetProp->value,
&targetProp->valueFlags
);
}
return hr;
}
HRESULT IIDXLocalCamera::ResetCameraControlProps() {
log_info("iidx:camhook", "[{}] Reset camera control", m_name);
for (size_t i = 0; i < CAMERA_CONTROL_PROP_SIZE; i++) {
CameraControlProp prop = m_controlProps.at(i);
SetCameraControlProp(i, prop.defaultValue, prop.defFlags);
}
return S_OK;
}
std::string IIDXLocalCamera::GetName() {
return m_name;
}
std::string IIDXLocalCamera::GetFriendlyName() {
return m_friendly_name;
}
std::string IIDXLocalCamera::GetSymLink() {
if (!m_pwszSymbolicLink) {
return "(unknown)";
}
return ws2s(m_pwszSymbolicLink);
}
MediaTypeInfo IIDXLocalCamera::GetMediaTypeInfo(IMFMediaType *pType) {
MediaTypeInfo info = {};
HRESULT hr = S_OK;
MFRatio frameRate = { 0, 0 };
info.p_mediaType = pType;
// Find the video subtype.
hr = pType->GetGUID(MF_MT_SUBTYPE, &info.subtype);
if (FAILED(hr)) { goto done; }
// Get the frame size.
hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &info.width, &info.height);
if (FAILED(hr)) { goto done; }
// Get frame rate
hr = MFGetAttributeRatio(
pType,
MF_MT_FRAME_RATE,
(UINT32*)&frameRate.Numerator,
(UINT32*)&frameRate.Denominator
);
if (FAILED(hr)) { goto done; }
info.frameRate = frameRate.Numerator / frameRate.Denominator;
info.description = fmt::format(
"{}x{} @{}FPS {}",
info.width,
info.height,
(int)info.frameRate,
GetVideoFormatName(info.subtype)
);
done:
return info;
}
std::string IIDXLocalCamera::GetVideoFormatName(GUID subtype) {
if (subtype == MFVideoFormat_YUY2) {
return "YUY2";
}
if (subtype == MFVideoFormat_NV12) {
return "NV12";
}
if (subtype == MFVideoFormat_MJPG) {
return "MJPG";
}
return "Unknown";
}
/**
* Return values:
* S_OK: this is a "better" media type than the existing one
* S_FALSE: valid media type, but not "better"
* E_*: invalid media type
*/
HRESULT IIDXLocalCamera::TryMediaType(IMFMediaType *pType, UINT32 *pBestWidth, double *pBestFrameRate) {
HRESULT hr = S_OK;
UINT32 width = 0, height = 0;
GUID subtype = { 0, 0, 0, 0 };
MFRatio frameRate = { 0, 0 };
hr = pType->GetGUID(MF_MT_SUBTYPE, &subtype);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] Failed to get subtype: {:#x}", m_name, (ULONG)hr);
return hr;
}
hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &width, &height);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] Failed to get frame size: {:#x}", m_name, (ULONG)hr);
return hr;
}
// Only support format with converters
// TODO: verify conversion support with DXVA
if (subtype != MFVideoFormat_YUY2 && subtype != MFVideoFormat_NV12) {
return E_FAIL;
}
// Frame rate
hr = MFGetAttributeRatio(
pType,
MF_MT_FRAME_RATE,
(UINT32*)&frameRate.Numerator,
(UINT32*)&frameRate.Denominator
);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] Failed to get frame rate: {:#x}", m_name, (ULONG)hr);
return hr;
}
double frameRateValue = frameRate.Numerator / frameRate.Denominator;
// Filter by aspect ratio
auto aspect_ratio = 4.f / 3.f;
if (m_prefer_16_by_9) {
aspect_ratio = 16.f / 9.f;
}
if (fabs((height * aspect_ratio) - width) > 0.01f) {
return S_FALSE;
}
// If we have 1280x720 already, only try for better frame rate
if ((*pBestWidth >= (UINT32)TARGET_SURFACE_WIDTH) && (width > *pBestWidth) && (frameRateValue < *pBestFrameRate)) {
return S_FALSE;
}
// Check if this format has better resolution / frame rate
if ((width > *pBestWidth) || (width >= (UINT32)TARGET_SURFACE_WIDTH && frameRateValue >= *pBestFrameRate)) {
// log_misc(
// "iidx:camhook", "Better media type {} ({}x{}) @({} FPS)",
// GetVideoFormatName(subtype),
// width,
// height,
// (int)frameRateValue
// );
*pBestWidth = width;
*pBestFrameRate = frameRateValue;
return S_OK;
}
return S_FALSE;
}
HRESULT IIDXLocalCamera::InitTargetTexture() {
HRESULT hr = S_OK;
// Create a new destination texture
hr = m_device->CreateTexture(TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT, 1, D3DUSAGE_RENDERTARGET, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &m_texture, NULL);
if (FAILED(hr)) { goto done; }
// Create a D3D9 surface for the destination texture so that camera sample can be drawn onto it
hr = m_texture->GetSurfaceLevel(0, &m_pDestSurf);
if (FAILED(hr)) { goto done; }
// Make the game use this new texture as camera stream source
*m_camera_texture_target = m_texture;
*m_preview_texture_target = m_texture;
m_active = TRUE;
// Create texture for colour conversion
hr = m_device->CreateTexture(TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT, 1, D3DUSAGE_RENDERTARGET, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &m_conversionTexture, NULL);
if (FAILED(hr)) { goto done; }
hr = m_conversionTexture->GetSurfaceLevel(0, &m_pConversionSurf);
if (FAILED(hr)) { goto done; }
// Create texture for transformation
hr = m_device->CreateTexture(TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT, 1, D3DUSAGE_DYNAMIC, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &m_transformTexture, NULL);
if (FAILED(hr)) { goto done; }
hr = m_transformTexture->GetSurfaceLevel(0, &m_pTransformSurf);
if (FAILED(hr)) { goto done; }
// Create texture for transformation result so that we don't have to screw our brain doing in-memory flipping
hr = m_device->CreateTexture(TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT, 1, D3DUSAGE_DYNAMIC, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &m_transformResultTexture, NULL);
if (FAILED(hr)) { goto done; }
hr = m_transformResultTexture->GetSurfaceLevel(0, &m_pTransformResultSurf);
if (FAILED(hr)) { goto done; }
// printTextureLevelDesc(m_texture);
// printTextureLevelDesc(m_texture_original);
done:
if (SUCCEEDED(hr)) {
log_misc("iidx:camhook", "[{}] Created texture", m_name);
} else {
log_warning("iidx:camhook", "[{}] Failed to create texture: {:#x}", m_name, (ULONG)hr);
}
return hr;
}
HRESULT IIDXLocalCamera::FlushDrawCommands() {
IDirect3DQuery9* pEventQuery = nullptr;
// It is necessary to flush the command queue
// or the data is not ready for the receiver to read.
// Adapted from : https://msdn.microsoft.com/en-us/library/windows/desktop/bb172234%28v=vs.85%29.aspx
// Also see : http://www.ogre3d.org/forums/viewtopic.php?f=5&t=50486
m_device->CreateQuery(D3DQUERYTYPE_EVENT, &pEventQuery);
if (pEventQuery) {
pEventQuery->Issue(D3DISSUE_END);
while (S_FALSE == pEventQuery->GetData(NULL, 0, D3DGETDATA_FLUSH));
pEventQuery->Release(); // Must be released or causes a leak and reference count increment
}
return S_OK;
}
HRESULT IIDXLocalCamera::DrawSample(IMFMediaBuffer *pSrcBuffer) {
if (!m_active) {
return E_FAIL;
}
// snap variables now so they don't change while inside the critical section
const auto flip_h = m_flipHorizontal;
const auto flip_v = m_flipVertical;
EnterCriticalSection(&m_critsec);
HRESULT hr = S_OK;
IDirect3DSurface9 *pCameraSurf = NULL;
hr = WrappedMFGetService(pSrcBuffer, MR_BUFFER_SERVICE, IID_PPV_ARGS(&pCameraSurf));
if (flip_h || flip_v) {
// Stretch Camera content to texture and perform color space conversion
hr = m_device->StretchRect(pCameraSurf, &m_rcSource, m_pConversionSurf, &m_rcDest, D3DTEXF_LINEAR);
// Copy converted camera content to dynamic texture for vertical/horizontal flipping
hr = m_device->StretchRect(m_pConversionSurf, NULL, m_pTransformSurf, NULL, D3DTEXF_NONE);
// Transform
D3DLOCKED_RECT srcLockedRect;
hr = m_pTransformSurf->LockRect(&srcLockedRect, NULL, D3DLOCK_NOSYSLOCK | D3DLOCK_READONLY);
D3DLOCKED_RECT destLockedRect;
hr = m_pTransformResultSurf->LockRect(&destLockedRect, NULL, D3DLOCK_NOSYSLOCK);
BYTE* pSrc = (BYTE*)srcLockedRect.pBits;
BYTE* pDest = (BYTE*)destLockedRect.pBits;
const int pixelSize = 4;
if (flip_v) {
pDest += destLockedRect.Pitch * (TARGET_SURFACE_HEIGHT - 1);
}
for (int y = 0; y < TARGET_SURFACE_HEIGHT; y++) {
for (int x = 0; x < TARGET_SURFACE_WIDTH; x++) {
memcpy(
pDest + x * pixelSize,
pSrc + (flip_h ? (TARGET_SURFACE_WIDTH - x - 1) : x) * pixelSize,
pixelSize
);
}
pSrc += srcLockedRect.Pitch;
if (flip_v) {
pDest -= destLockedRect.Pitch;
} else {
pDest += destLockedRect.Pitch;
}
}
m_pTransformSurf->UnlockRect();
m_pTransformResultSurf->UnlockRect();
// Stretch camera texture to transform surface
hr = m_device->StretchRect(m_pTransformResultSurf, NULL, m_pDestSurf, NULL, D3DTEXF_NONE);
} else {
// No transformation needed, stretch to destination texture directly
hr = m_device->StretchRect(pCameraSurf, &m_rcSource, m_pDestSurf, &m_rcDest, D3DTEXF_LINEAR);
}
FlushDrawCommands();
if (FAILED(hr)) {
log_warning("iidx:camhook", "Error in DrawSample {:#x}", (ULONG)hr);
}
SafeRelease(&pCameraSurf);
LeaveCriticalSection(&m_critsec);
return hr;
}
HRESULT IIDXLocalCamera::ReadSample() {
HRESULT hr;
DWORD streamIndex, flags;
LONGLONG llTimeStamp;
IMFSample *pSample = nullptr;
IMFMediaBuffer *pBuffer = nullptr;
hr = m_pSourceReader->ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
0,
&streamIndex,
&flags,
&llTimeStamp,
&pSample
);
if (pSample) {
// Draw to D3D
pSample->GetBufferByIndex(0, &pBuffer);
hr = DrawSample(pBuffer);
}
SafeRelease(&pBuffer);
SafeRelease(&pSample);
return hr;
}
LPDIRECT3DTEXTURE9 IIDXLocalCamera::Render() {
if (!m_active) {
return nullptr;
}
HRESULT hr = ReadSample();
if (FAILED(hr)) {
return nullptr;
}
return m_texture;
}
ULONG IIDXLocalCamera::Release() {
log_info("iidx:camhook", "[{}] Release camera", m_name);
m_active = false;
ULONG uCount = InterlockedDecrement(&m_nRefCount);
*m_camera_texture_target = m_camera_texture_original;
*m_preview_texture_target = m_preview_texture_original;
for (size_t i = 0; i < m_mediaTypeInfos.size(); i++) {
SafeRelease(&(m_mediaTypeInfos.at(i).p_mediaType));
}
SafeRelease(&m_pDestSurf);
SafeRelease(&m_pTransformSurf);
SafeRelease(&m_pConversionSurf);
SafeRelease(&m_pTransformResultSurf);
SafeRelease(&m_texture);
SafeRelease(&m_conversionTexture);
SafeRelease(&m_transformTexture);
SafeRelease(&m_transformResultTexture);
if (m_pSource) {
m_pSource->Shutdown();
m_pSource->Release();
}
CoTaskMemFree(m_pwszSymbolicLink);
m_pwszSymbolicLink = NULL;
m_cchSymbolicLink = 0;
if (uCount == 0) {
delete this;
}
// For thread safety, return a temporary variable.
return uCount;
}
}
#endif